web-dev-qa-db-ja.com

大規模なレガシープロジェクトを拡張する際の継承と抑制

私はレガシーJava=多くのコードを含むプロジェクトを持っています。コードはMVCパターンを使用し、よく構造化され、よく書かれています。また、多くのユニットテストがあり、まだアクティブに維持されています(バグ修正、マイナーな機能追加)したがって、私はできるだけ元の構造とコードスタイルを維持したいと考えています。

既存のクラスに新しい動作を注入することを目指しています。ジャンプするように訓練されたアヒルのように。それから私はすべてのアヒルにジャンプするように頼みます。できる人-ジャンプ。何も出来ない私には申し分ないもの。

追加する新機能は概念的なものであるため、コード全体に変更を加える必要があります。変更を最小限に抑えるために、既存のクラスを拡張するのではなく、封じ込めを使用することにしました。

class ExistingClass
{
   // .... existing code


   //  my code adding new functionality
   private ExistingClassExtension extension = new ExistingClassExtension();
   public ExistingClassExtension getExtension() {return extension;}
} 

...
// somewhere in code
ExistingClass instance = new ExistingClass();

...
// when I need a new functionality
instance.getExtension().newMethod1();

追加するすべての機能は、新しいExistingClassExtensionクラス内にあります。実際には、拡張する必要がある各クラスにこれらの2行のみを追加しています。

そうすることで、コード全体で新しい拡張クラスをインスタンス化する必要もなくなり、既存のテストを使用してリグレッションがないことを確認できます。

しかし私の同僚は、この状況ではそうすることは適切なOOPアプローチではなく、新しい機能を追加するためにExistingClassから継承する必要があると主張しています。

どう思いますか?私はここで継承/封じ込めに関する数多くの質問を認識していますが、私の質問は異なると思います。

編集:私の質問に答えるほとんどの人は、包含関係が次のようになっていると想定しています:

class NewClass
{
   OldClass instanceOfOldClass;
   void oldFunctionality() { instanceOfOldClass.oldFunctionality();}

   void newFunctionality() { // new code here }
}

しかし、これも一種の封じ込め関係ではないでしょうか。

class OldClass
{
   void OldFunctionality();
   NewClass instanceOfNewClass;

   void NewFunctionality {instanceOfNewClass.NewFunctionality();}
}
5
Flot2011

あなたのアプローチは、Michael Featherがsproutクラスと呼んでいるものです。 "従来のコードで効果的に動作する" を参照してください。これは、古いクラスで変更しすぎないようにするためのよく知られた手法です(誤って何かを壊してしまう危険性があります)。さらに、(ExistingClassExtensionの単体テストを作成することによって)拡張機能を分離してテストする機能が提供され、古いクラスと新しいクラスの間で責任が分離されます。

このアプローチの(おそらくもっと悪い)代替策は、ExistingClassを直接変更し、そこに新しい機能を直接実装することです(これを5回以上行うと、簡単に大きな泥の玉になるでしょう) )。

継承はおそらくここでは間違ったアプローチです。少なくとも、ExistingClassExtensionExistingClassの内部動作にアクセスする必要がなく、多くの既存のクラスを使用するコードを変更する必要がある場合古いクラス名を継承されたクラス名に置き換えて配置します(おそらく、そのコードが拡張機能を利用していない場合でも)。明らかに、継承は単独でのテストを禁止します。さらに、1つの拡張だけでなく5つの拡張が必要な​​場合にどうなるかを考えてください。 5レベルの深さで継承ツリーを構築しますか?どちらの順番で?

同僚に相談して、継承には正しい使い方と間違った使い方があることを伝えてください。これは、おそらく間違った使用の非常に典型的なケースです。

2
Doc Brown

ここで、構成と継承の議論を誤解しています。 継承ではなく、クラス/モジュール/コンポーネントの異なる動作ではなく、injectそれを含めて、次のようにします:

class Duck {
    private ICanFly flyAbility;
    private ICanSpeak soundAbility;
    private ICanSwim swimAbility;

    public Duck(ICanFly wings, ICanSpeak beak, ICanSwim fins) { ... }
}

それで、翼はJetPackに、くちばしはVoiceCardに、そしてフィンはUnderwaterPropellerに置き換えることができます。

アイデアは、さまざまなオブジェクトをbehaviorsでさらに分解できること、そして泳ぐ鳥であるDuckBatであるということです。飛ぶ哺乳類、そしてPlatypusは泳ぎ、産卵する哺乳類です。


あなたの場合、既存の構造を継続してそれを拡張すること(構造は、必ずしもオブジェクトではない!)が最善の方法です。問題は、物事が今日どのように構築されているかです。慣習を破らないでください。

2
Madara's Ghost

あなたのアプローチは、ExistingClassを変更することと同じだと思います。 ExistingClassExtension内にExistingClassをインライン化すると、内部クラスが宣言され、外部クラスのメソッドを介してアクセスできるようになります。さらに内部クラスのメソッドとパブリックメンバーをインライン化すると(getExtensionへの呼び出しが1つ保存されることを除いて、このコードの呼び出し元に違いはありません)、実際に明らかに追加されましたExistingClassの下にいくつかの新しいメソッド。

現在あなたは持っています

class ExistingClass {
    private ExistingClassExtension extension = new ExistingClassExtension();
    public ExistingClassExtension getExtension() {return extension;}
}

class ExistingClassExtension {
    public boolean extendingMethod() { return false; }
}

クラスをインライン化する

class ExistingClass {
    class ExistingClassExtension {
        // ... extendingMethod ...
    }
    public ExistingClassExtension getExtension() {return new ExistingClassExtension();}
}

メソッドをインライン化する

class ExistingClass {
    public boolean extendingMethod() {
        // ... extendingMethod ...
    }
}

「封じ込め」を「構成」と誤って解釈し、次のようなデザインが見つかると予想しました。

class ExistingClass {
    boolean existingMethod() { return true; }
}

class ExtendingClass {
    private ExistingClass instance = new ExistingClass();
    boolean existingMethod() { return instance.existingMethod();}
    boolean extendingMethod() { return false;}
}

継承オプションは次のように宣言するだけです:

class ExtendingClass extends ExistingClass {
    boolean extendingMethod() { return false; }
}

構成するか継承するかを決定する基準は、スコットマイヤーズによって「 Effective C++ 」に「パブリック継承モデルが「ある」関係であることを確認する」としてまとめられました。

両方のクラスが何を行うか、または表すかを知らなければ、一方が他方の「サブタイプ」であるかどうかを判断することは不可能ですが、「 Effective Java 」では、Joshua Blochは構成よりも構成することをお勧めしますほとんどの状況で継承。継承は、大きすぎる、または制御不能なインターフェイスを新しいクラスに提供するためです。これにより、レガシーコードに対するこれらのマイナーな修正の1つがプライベートメソッドの1つを変更したときに、将来、微妙なバグやビルドの破損につながる可能性がありますnewMethod1に依存しています。

この「封じ込め」戦略を、新しいクラス内のレガシークラスの実際の構成で置き換えることを検討することをお勧めします。質問に記載されているすべての利点に加えて、あなたが思っているものとは異なる可能性のあるインターフェイスを継承しているではない追加された利点、またはあなたの足の下で変わるかもしれません。

1
logc