コードを一般的なコードにリファクタリングする方法は?


16

バックグラウンド

現在進行中のC#プロジェクトに取り組んでいます。私はC#プログラマではなく、主にC ++プログラマです。そのため、基本的に簡単でリファクタリングするタスクが割り当てられました。

コードはめちゃくちゃです。それは巨大なプロジェクトです。お客様が新機能とバグ修正を含む頻繁なリリースを要求したため、他のすべての開発者はコーディング中にブルートフォースアプローチをとることを余儀なくされました。コードはメンテナンスが非常に難しく、他のすべての開発者はそれに同意します。

私は彼らがそれを正しくしたかどうかを議論するためにここにいるのではありません。私はリファクタリングしているので、リファクタリングされたコードが複雑に思えるので、正しい方法でそれをやっているかどうか疑問に思っています!これが簡単な例としての私の仕事です。

問題

6つのクラスがありますABCDEF。すべてのクラスには関数がありますExecJob()。6つの実装はすべて非常に似ています。基本的に、最初A::ExecJob()は書かれました。次に、のB::ExecJob()copy-paste-modificationによって実装されたわずかに異なるバージョンが必要でしたA::ExecJob()。別のわずかに異なるバージョンが必要になったとき、C::ExecJob()書かれたなど。6つの実装すべてに共通のコードがあり、次にいくつかの異なるコード行があり、さらにいくつかの共通コードがあります。実装の簡単な例を次に示します。

A::ExecJob()
{
    S1;
    S2;
    S3;
    S4;
    S5;
}

B::ExecJob()
{
    S1;
    S3;
    S4;
    S5;
}

C::ExecJob()
{
    S1;
    S3;
    S4;
}

SNまったく同じ文のグループはどこにありますか。

それらを共通にするために、別のクラスを作成し、関数内の共通コードを移動しました。パラメータを使用して、実行するステートメントのグループを制御します。

Base::CommonTask(param)
{
    S1;
    if (param.s2) S2;
    S3;
    S4;
    if (param.s5) S5;
}

A::ExecJob() // A inherits Base
{
    param.s2 = true;
    param.s5 = true;
    CommonTask(param);
}

B::ExecJob() // B inherits Base
{
    param.s2 = false;
    param.s5 = true;
    CommonTask(param);
}

C::ExecJob() // C inherits Base
{
    param.s2 = false;
    param.s5 = false;
    CommonTask(param);
}

この例では、3つのクラスと単純化されたステートメントのみを使用していることに注意してください。実際には、CommonTask()関数はこれらすべてのパラメーターチェックで非常に複雑に見え、さらに多くのステートメントがあります。また、実際のコードには、いくつかのCommonTask()見た目の関数があります。

すべての実装は共通のコードを共有しており、ExecJob()機能はよりキュートに見えますが、次の2つの問題があります。

  • の変更についてはCommonTask()、テストするために6つすべての(将来的にはさらに増える可能性がある)機能が必要です。
  • CommonTask()すでに複雑です。時間とともに複雑になります。

私はそれを正しい方法でやっていますか?


Martin Fowlerのリファクタリングの本には、役に立つコードをリファクタリングするための多くの特定のテクニックがあります。
アラン

回答:


14

はい、あなたは絶対に正しい道にいます!

私の経験では、物事が複雑な場合、小さなステップで変化が起こることに気付きました。これまでに行ったことは、進化プロセス(またはリファクタリングプロセス)のステップ1です。これがステップ2とステップ3です。

ステップ2

class Base {
  method ExecJob() {
    S1();
    S2();
    S3();
    S4();
    S5();
  }
  method S1() { //concrete implementation }
  method S3() { //concrete implementation }
  method S4() { //concrete implementation}
  abstract method S2();
  abstract method S5();
}

class A::Base {
  method S2() {//concrete implementation}
  method S5() {//concrete implementation}
}

class B::Base {
  method S2() { // empty implementation}
  method S5() {//concrete implementation}
}

class C::Base {
  method S2() { // empty implementation}
  method S5() { // empty implementation}
}

これは「テンプレート設計パターン」であり、リファクタリングプロセスの一歩先を行っています。基本クラスが変更された場合、サブクラス(A、B、C)に影響を与える必要はありません。新しいサブクラスを比較的簡単に追加できます。ただし、上の図からすぐに、抽象化が壊れていることがわかります。「空の実装」の必要性は良い指標です。それは抽象化に何か問題があることを示しています。短期的には許容できる解決策だったかもしれませんが、より良い解決策があるようです。

ステップ3

interface JobExecuter {
  void executeJob();
}
class A::JobExecuter {
  void executeJob(){
     helper = new Helper();
     helper->S1();
     helper->S2();
     helper->S3();
     helper->S4();
     helper->S5();
  }
}

class B::JobExecuter {
  void executeJob(){
     helper = new Helper();
     helper->S1();
     helper->S3();
     helper->S4();
     helper->S5();
  }
}

class C::JobExecuter {
  void executeJob(){
     helper = new Helper();
     helper->S1();
     helper->S3();
     helper->S4();
  }
}

class Base{
   void ExecJob(JobExecuter executer){
       executer->executeJob();
   }
}

class Helper{
    void S1(){//Implementation} 
    void S2(){//Implementation}
    void S3(){//Implementation}
    void S4(){//Implementation} 
    void S5(){//Implementation}
}

これは「戦略設計パターン」であり、あなたのケースに適していると思われます。ジョブを実行するためのさまざまな戦略があり、各クラス(A、B、C)はそれを別々に実装します。

このプロセスには、ステップ4またはステップ5、またはより優れたリファクタリングアプローチがあると確信しています。ただし、これにより重複コードを排除し、変更がローカライズされていることを確認できます。


「ステップ2」で説明したソリューションで見られる主な問題は、S5の具体的な実装が2回存在することです。
user281377

1
はい、コードの重複は排除されません!そして、それは抽象化が機能していないことのもう一つの指標です。プロセスについてどう思うかを示すために、ステップ2をそこに置きたかっただけです。より良いものを見つけるための段階的なアプローチ。
グベン

1
+1非常に良い戦略(パターンについては話していない)!
ジョルダン

7

あなたは実際に正しいことをしています。私はこれを言う:

  1. 共通のタスク機能のコードを変更する必要がある場合、共通のクラスで記述しない場合にコードを含む6つのクラスすべてでコードを変更する必要はありません。
  2. コードの行数が減ります。

3

この種のコードは、イベントドリブンデザイン(特に.NET)と多くの点で共通しています。最も保守可能な方法は、共有動作を可能な限り小さなチャンクに保持することです。

高レベルのコードが多数の小さなメソッドを再利用できるようにし、高レベルのコードは共有ベースから除外します。

リーフ/コンクリートの実装には多くのボイラープレートがあります。パニックにならないで、大丈夫です。そのコードはすべて直接的なもので、理解しやすいものです。何かが壊れたときはときどき再配置する必要がありますが、変更は簡単です。

高レベルのコードには多くのパターンがあります。時々彼らは本物ですが、ほとんどの場合そうではありません。上の5つのパラメーターの「構成」は似ていますが、似ていません。これらは、まったく異なる3つの戦略です。

また、これらすべてを構成で行うことができ、継承について心配する必要はありません。カップリングが少なくなります。


3

もし私があなただったら、最初にもう1ステップ、UMLベースの研究を追加するでしょう。

すべての共通部分を一緒にマージするコードをリファクタリングすることは必ずしも最善の方法ではありません。良いアプローチというよりは一時的な解決策のように聞こえます。

UMLスキームを描画し、物事をシンプルかつ効果的に保ち、「このソフトウェアを実行するために何が必要か」など、プロジェクトに関するいくつかの基本的な概念に留意してください。「このソフトウェアの一部を抽象的、モジュール式、拡張可能などに保つ最良の方法は何ですか?」「カプセル化を最大限に実装するにはどうすればよいですか?」

私はこれを言っているだけです:今すぐコードを気にしないでください、あなたはロジックを気にするだけです、あなたがすべての残りが本当に簡単なタスクになることができることを念頭に置いて明確なロジックを持っているときあなたが直面している問題の原因は、単に悪いロジックが原因です。


これは、リファクタリングが行われる前の最初のステップである必要があります。コードがマップされるのに十分理解されるまで(umlまたは他の野生のマップ)、リファクタリングは暗闇の中で設計されます。
クズカイ

3

最初のステップは、これがどこに行っても、明らかに大きな方法A::ExecJobを小さな断片に分割することです。

したがって、代わりに

A::ExecJob()
{
    S1; // many lines of code
    S2; // many lines of code
    S3; // many lines of code
    S4; // many lines of code
    S5; // many lines of code
}

あなたが得る

A::ExecJob()
{
    S1();
    S2();
    S3();
    S4();
    S5();
}

A:S1()
{
   // many lines of code
}

A:S2()
{
   // many lines of code
}

A:S3()
{
   // many lines of code
}

A:S4()
{
   // many lines of code
}

A:S5()
{
   // many lines of code
}

これからは、多くの可能な方法があります。私の考え:Aをクラス階層の基本クラスにし、ExecJobを仮想化し、B、Cを簡単に作成できるようになります。コピーアンドペーストは多すぎません。ExecJob(現在5ライナー)を変更したものに置き換えるだけです。バージョン。

B::ExecJob()
{
    S1();
    S3();
    S4();
    S5();
}

しかし、なぜこれほど多くのクラスがあるのでしょうか?たぶん、あなたはそれらをすべて、どのアクションがで必要であるかを伝えることができるコンストラクタを持つ単一のクラスで置き換えることができますExecJob


2

継承が一般的なコードを実装するための最良の方法であるとは思わないが、あなたのアプローチは正しい線に沿っているという他の答えに同意する-私は合成を好む。私がこれまで説明できなかったはるかに優れたC ++ FAQから:http : //www.parashift.com/c++-faq/priv-inherit-vs-compos.html


1

あなたのクラスで使用する機能のための共通の場所必要があるという理由だけで-まず、あなたは継承が本当に仕事のために、ここで適切なツールであることを確認する必要がありますAF、時には別のヘルパー-共通の基底クラスはここに正しいことであることを意味するものではありませんがクラスは仕事をより良くします。そうかもしれないし、そうでないかもしれない。それは、AからFとあなたの共通の基底クラスとの間に「is-a」関係があることに依存しています。ここで、このトピックを扱ったブログ記事を見つけます。

あなたの場合、共通の基本クラスが正しいことだと決めたとしましょう。次に、2番目に行うことは、コードフラグメントS1からS5がそれぞれ基本クラスの個別のメソッドS1()に実装されていることを確認することS5()です。その後、「ExecJob」関数は次のようになります。

A::ExecJob()
{
    S1();
    S2();
    S3();
    S4();
    S5();
}

B::ExecJob()
{
    S1();
    S3();
    S4();
    S5();
}

C::ExecJob()
{
    S1();
    S3();
    S4();
}

ご覧のとおり、S1からS5は単なるメソッド呼び出しであり、コードブロックはもうないため、コードの重複はほぼ完全に削除され、パラメーターのチェックは不要になり、複雑さが増すという問題を回避できますさもないと。

最後に、3番目のステップ(!)としてのみ、これらすべてのExecJobメソッドをベースクラスの1つに結合することを検討するかもしれません。これらの部分の実行は、パラメータ、提案した方法、またはテンプレートメソッドパターン。実際のコードに基づいて、それがあなたのケースで努力する価値があるかどうかを自分で決める必要があります。

しかし、IMHOは、大きなメソッドを小さなメソッドに分解する基本的な手法であり、パターンの適用よりもコードの重複を避けるためにはるかに重要です。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.