コンストラクターのみのサブクラス:これはアンチパターンですか?


37

私は同僚と議論をしていましたが、最終的にはサブクラス化の目的に関して直感が矛盾することになりました。私の直感では、サブクラスの主な機能がその親の限られた範囲の値を表現することであれば、おそらくサブクラスであってはならないということです。彼は反対の直観を主張しました:サブクラス化はオブジェクトがより「特定的」であることを表し、したがってサブクラス関係がより適切であるということです。

私の直感をより具体的に言うと、親クラスを拡張するサブクラスがあるが、サブクラスがオーバーライドする唯一のコードがコンストラクターである場合(そう、コンストラクターは一般に「オーバーライド」しないことを知っています。本当に必要だったのは、ヘルパーメソッドです。

たとえば、このやや現実的なクラスを考えてみましょう。

public class DataHelperBuilder
{
    public string DatabaseEngine { get; set; }
    public string ConnectionString { get; set; }

    public DataHelperBuilder(string databaseEngine, string connectionString)
    {
        DatabaseEngine = databaseEngine;
        ConnectionString = connectionString;
    }

    // Other optional "DataHelper" configuration settings omitted

    public DataHelper CreateDataHelper()
    {
        Type dataHelperType = DatabaseEngineTypeHelper.GetType(DatabaseEngine);
        DataHelper dh = (DataHelper)Activator.CreateInstance(dataHelperType);
        dh.SetConnectionString(ConnectionString);

        // Omitted some code that applies decorators to the returned object
        // based on omitted configuration settings

        return dh;
    }
}

彼の主張は、次のようなサブクラスを持つことが完全に適切だということです。

public class SystemDataHelperBuilder
{
    public SystemDataHelperBuilder()
        : base(Configuration.GetSystemDatabaseEngine(),
               Configuration.GetSystemConnectionString())
    {
    }
 }

だから、質問:

  1. 設計パターンについて話す人々のうち、これらの直観のうち正しいものはどれですか?上記のサブクラス化はアンチパターンですか?
  2. アンチパターンの場合、その名前は何ですか?

これが簡単にグーグル検索可能な答えであったことが判明した場合、私は謝罪します。グーグルでの私の検索は、ほとんどが望んでいるものではなく、テレスコープコンストラクターのアンチパターンに関する情報を返しました。


2
名前にある「ヘルパー」という言葉はアンチパターンだと思います。それは、物事とwhatsitへの絶え間ない参照を伴うエッセイを読むようなものです。それ何か言ってください。工場ですか?コンストラクタ?私にとって、それは怠zyな思考プロセスを軽視し、適切なセマンティック名がそれを指示するため、単一の責任原則の違反を奨励します。私は他の有権者に同意し、@ lxrecの答えを受け入れるべきです。ファクトリメソッドのサブクラスを作成することは、ユーザーがドキュメントで指定されている正しい引数を渡すことを信頼できない場合を除き、まったく意味がありません。その場合、新しいユーザーを取得します。
アーロンホール

@Ixrecの答えに完全に同意します。結局のところ、彼の答えは、私はずっと正しかったので、同僚は間違っていたと言っています。;-)しかし、真剣に、それは私がそれを受け入れて奇妙に感じた理由です。私は偏見で告発されると思った。しかし、私はプログラマーStackExchangeが初めてです。エチケットの違反ではないと確信した場合は、喜んで受け入れます。
-HardlyKnowEm

1
いいえ、最も価値があると思われる答えを受け入れることが期待されます。コミュニティがこれまでで最も価値があると考えているのは、Ixrecの3対1以上です。したがって、彼らはあなたがそれを受け入れることを期待するでしょう。このコミュニティでは、間違った答えを受け入れる質問者に表明されるフラストレーションはほとんどありませんが、答えを決して受け入れない質問者には表明されるフラストレーションがたくさんあります。誰もがここで受け入れ答え文句を言っていないことに注意してください、代わりに彼らは自らを正すように見えるの投票、文句を言う:stackoverflow.com/q/2052390/541136
アーロン・ホール

非常によく、私はそれを受け入れます。ここでコミュニティの標準について教えてくれてありがとう。
-HardlyKnowEm

回答:


54

特定の引数でクラスXを作成するだけであれば、クラスと継承が提供する機能を使用していないため、サブクラス化はその意図を表現する奇妙な方法です。それは本当にアンチパターンではありません、それはただ奇妙であり、少し無意味です(他の何らかの理由がない限り)。この意図を表現するより自然な方法はFactory Methodで、この場合は「ヘルパーメソッド」の空想的な名前です。

一般的な直観に関して、「より具体的」と「限定された範囲」の両方が、サブクラスについて有害な考え方である可能性があります。どちらも、SquareをRectangleのサブクラスにすることが良い考えだからです。LSPのような形式的なものに依存することなく、サブクラスが基本インターフェースの実装提供する、インターフェース拡張していくつかの新しい機能を追加することは、より良い直観だと思います。


2
ただし、ファクトリメソッドでは、コードベースで特定の動作(引数で制御)を強制することはできません。また、このクラス/メソッド/フィールドがSystemDataHelperBuilder具体的に受け取ることを明示的に注釈することが役立つ場合があります。
テラスティン

3
ああ、正方形と長方形の引数はまさに私が探していたものだったと思う。ありがとう!
HardlyKnowEm

7
もちろん、正方形が長方形のサブクラスであっても、不変であれば大丈夫です。あなたは本当にLSPに目を向ける必要があります。
デビッドコンラッド

10
正方形/長方形の「問題」に関することは、幾何学的形状が不変であることです。長方形が理にかなっているところならいつでも正方形を使用できますが、サイズ変更は正方形や長方形のようなものではありません。
ドーバル

3
@nha LSP = Liskov Substitution Principle
Ixrec

16

これらの直観のうち正しいのはどれですか?

あなたの同僚は正しいです(標準タイプのシステムを想定しています)。

考えてみてください、クラスは可能な法的な値を表します。クラスAに1バイトフィールドがある場合、256の正当な値があるFと思うかもしれませんがA、その直感は間違っています。A「これまでの値のすべての置換」を「フィールドFは0-255にする必要があります」に制限しています。

を拡張ABて別のバイトフィールドFFがある場合、制限追加されます。「Fバイトの値」の代わりに、「バイトの値F&& FFもバイト」があります。古いルールを保持しているため、ベースタイプで機能するすべてのものがサブタイプでも機能します。しかし、ルールを追加しているので、「何でも可能」からさらに離れて専門化しています。

それとも別の方法を考えるために、その前提とA:サブタイプの束を持っていたBCD。タイプの変数はAこれらのサブタイプのいずれかである可能性がありますが、タイプの変数Bはより具体的です。(ほとんどの型システムでは)a CまたはaにはできませんD

これはアンチパターンですか?

え?特殊なオブジェクトを持つことは有用であり、コードに明らかに有害ではありません。サブタイピングを使用してその結果を達成することは、おそらく少し疑わしいですが、使用できるツールに依存します。より優れたツールを使用しても、この実装はシンプルで簡単で堅牢です。

これはアンチパターンとは見なしません。どのような状況でも明らかに間違っていて悪くないからです。


5
「より多くのルールは、可能な値が少ないことを意味し、より具体的であることを意味します。」私は本当にこれに従いません。タイプbyte and byteは、単にタイプよりも多くの値を持っていますbyte。256倍。それはさておき、ルールを要素の数(または正確にしたい場合はカーディナリティー)と混同することはできないと思います。無限の集合を考えると壊れます。整数と同じ数の偶数がありますが、数値が偶数であることを知ることは制限であり、整数であることだけを知るよりも多くの情報を与えてくれます!自然数についても同じことが言えます。
ドーバル

6
@Telastynトピックから外れていますが、偶数の整数の集合のカーディナリティー(大まかに言うと「サイズ」)がすべての整数の集合またはすべての有理数と同じであることが証明できます。それは本当にクールなものです。math.grinnell.edu/~miletijo/museum/infinite.html
HardlyKnowEm

2
集合論は非常に直感的ではありません。整数と偶数を1対1で対応させることができます(たとえば、関数を使用n -> 2n)。これは常に可能とは限りません。整数を実数にマッピングできないため、実数のセットは整数よりも「大きく」なります。ただし、(実)区間[0、1]をすべての実数のセットにマッピングして、同じ「サイズ」にすることができます。サブセットが「すべての要素Aも要素」であると定義されるのはB、セットが無限になると、それらが同じカーディナリティであるが異なる要素を持つ場合があるからです。
ドーバル

1
手元のトピックにさらに関連して、あなたが与える例は、オブジェクト指向プログラミングの観点から、実際には追加のフィールドの観点から-機能性-を拡張する制約です。機能を拡張しないサブクラス(円楕円問題など)について説明しています。
HardlyKnowEm

1
@Doval:「より少ない」という意味は複数あります。つまり、セットに複数の順序関係があります。「のサブセット」関係は、セットに対して明確に定義された(部分的な)順序付けを提供します。さらに、「より多くの規則」は、「セットのすべての要素が(特定のセットからの)より多くの(つまり、スーパーセットの)命題を満たす」ことを意味します。これらの定義では、「より多くのルール」は確かに「より少ない価値」を意味します。これは、おおまかに言って、コンピュータープログラムの多くのセマンティックモデル、たとえばスコットドメインが採用したアプローチです。
psmears

12

いいえ、アンチパターンではありません。私はこのための多くの実用的なユースケースを考えることができます:

  1. コンパイル時チェックを使用して、オブジェクトのコレクションが1つの特定のサブクラスのみに準拠していることを確認する場合。たとえば、あなたが持っている場合MySQLDaoと、SqliteDaoお使いのシステムでは、何らかの理由であなたはあなたが、コンパイラは、この特定の正しさを検証することができます説明するように、あなたがサブクラスを使用する場合は必ずコレクションは唯一、一つのソースからのデータを含むようにしたいです。一方、データソースをフィールドとして保存すると、これは実行時チェックになります。
  2. 現在のアプリケーションでは、AlgorithmConfiguration+ ProductClassとの間に1対1の関係がありAlgorithmInstanceます。つまり、プロパティファイルでを構成FooAlgorithmする場合、その製品クラスに対してProductClass1つしか取得できませんFooAlgorithmFooAlgorithm特定の2つのを取得する唯一の方法ProductClassは、サブクラスを作成することFooBarAlgorithmです。これは、プロパティファイルを使いやすくするためです(プロパティファイルの1つのセクションに多くの異なる構成の構文は必要ありません)。また、特定の製品クラスに複数のインスタンスが存在することは非常にまれです。
  3. @Telastynの答えは、別の良い例を示しています。

結論:これを実行しても実際には害はありません。アンチパターンは、なるように定義されています。

アンチパターン(またはアンチパターン)は、通常は効果がなく、非常に非生産的なリスクが繰り返される問題に対する一般的な対応です。

ここでは非常に逆効果になるリスクはないため、アンチパターンではありません。


2
ええ、もし質問をもう一度言い聞かせなければならないとしたら、「コード臭」と言うでしょう。それを避けるために次世代の若い開発者に警告することを保証するのに十分ではありませんが、それを見ると、全体的な設計に問題があることを示しているかもしれませんが、最終的には正しいかもしれません。
-HardlyKnowEm

ポイント1はターゲット上にあります。私はポイント2を本当に理解していませんでしたが、それは同じくらい良いと確信しています... :)
Phil

9

何かがパターンやアンチパターンであればあなたが書いているどのような言語や環境に大きく依存します。例えば、forループはパターンあるアセンブリで、C、および同様の言語とLispでアンチパターン。

私がずっと前に書いたコードの話を話しましょう...

LPCと呼ばれる言語で、ゲームで呪文を唱えるためのフレームワークを実装しました。いくつかのチェックを処理する戦闘呪文のサブクラスを持つ呪文スーパークラスがあり、それから個別の呪文(魔法ミサイル、稲妻など)にサブクラス化された単一ターゲットの直接ダメージ呪文のサブクラスがありました。

LPCの動作の性質は、シングルトンであるマスターオブジェクトがあったことです。あなたが「eww Singleton」に行く前に-それはあり、それはまったく「eww」ではありませんでした。呪文のコピーを作成するのではなく、呪文の(事実上)静的メソッドを呼び出しました。マジックミサイルのコードは次のようになりました。

inherit "spells/direct";

void init() {
  ::init();
  damage = 10;
  cost = 3;
  delay = 1.0;
  caster_message = "You cast a magic missile at ${target}";
  target_message = "You are hit with a magic missile cast by ${caster}";
}

そして、あなたはそれを見る?そのコンストラクターのみのクラス。親抽象クラスにある値を設定します(いいえ、セッターを使用するべきではありません-不変です)。ちゃんと動いた。コーディング、拡張、理解が簡単でした。

この種のパターンは、抽象クラスを拡張し、独自のいくつかの値を設定するサブクラスがあるが、すべての機能が継承する抽象クラスにある他の状況に変換されます。一目見に行くのStringBuilder本の例については、Javaでの-なく、かなりコンストラクタだけが、私は(すべてがAbstractStringBuilderである)の方法で自分自身を多くのロジックを見つけることが押されています。

これは悪いことではなく、アンチパターンではありません(一部の言語では、パターンそのものである可能性があります)。


2

私が考えることができるそのようなサブクラスの最も一般的な使用は例外階層ですが、それは言語がコンストラクタを継承できる限り、通常クラスで何も定義しない退化したケースです。私たちは、それを表現するために継承を使用ReadErrorする特殊なケースであるIOError、というように、しかしReadError、任意のメソッドをオーバーライドする必要はありませんIOError

これは、型をチェックすることで例外がキャッチされるためです。したがって、必要に応じて誰かがReadErrorallをキャッチせずにのみキャッチできるように、型の特殊化をエンコードする必要がありIOErrorます。

通常、物事の種類をチェックすることはひどく悪い形式であり、それを回避しようとします。回避できた場合、型システムで特殊化を表現する必要はありません。

ただし、たとえば、オブジェクトのログに記録された文字列形式で型の名前が表示される場合など、これはまだ役立つ可能性があります。これは言語であり、フレームワーク固有です。あなたは、原則的に、我々は、その場合には、クラスのメソッドの呼び出し、とどのようなシステムタイプの名前を置き換えることができますより多くの中だけのコンストラクタよりもオーバーライドしSystemDataHelperBuilder、我々はまた、オーバーライドしたいですloggingname()。そのため、システムにそのようなロギングがある場合、クラスは文字通りコンストラクターをオーバーライドするだけですが、「道徳的に」クラス名もオーバーライドするため、実用的な目的ではコンストラクターのみのサブクラスではないことを想像できます。それは望ましい異なる行動をしている、それ '

したがって、SystemDataHelperBuilder明示的にブランチ(タイプに基づいてブランチをキャッチする例外など)のインスタンスをチェックするコードがある場合、または最終的には一般的なコードがあるため、彼のコードは良いアイデアと言えるでしょうSystemDataHelperBuilder便利な方法で名前を使用する(それが単なるログ記録であっても)。

ただし、特殊なケースに型を使用すると、混乱が生じます。これは、何らかの方法で動作が異なる場合ImmutableRectangle(1,1)、最終的に誰かが疑問に思って、そうしないことを望むためです。例外階層とは異なり、ではなくをチェックして、長方形が正方形かどうかをチェックします。あなたの例では、a と作成されたものとはまったく同じ引数で違いがあり、それが問題を引き起こす可能性があります。したがって、「正しい」引数で作成されたオブジェクトを返す関数は、通常私に正しいことです。サブクラスは「同じ」ことの2つの異なる表現をもたらし、あなたが何かをしている場合にのみ役立ちます通常はしないようにします。ImmutableSquare(1)height == widthinstanceofSystemDataHelperBuilderDataHelperBuilder

プログラマーがこれまで考えてきたすべての良いアイデアと悪いアイデアの分類法を提供することはできないと考えているため、デザインパターンやアンチパターンの観点からは話していないことに注意してください。したがって、すべてのアイデアがパターンまたはアンチパターンであるわけではなく、誰かがそれが異なるコンテキストで繰り返し発生することを識別し、それを命名するようになるだけです;-)私はこの種のサブクラス化の特定の名前を知りません。


の使用を確認できましたが、可能であればコンテンツをレンダリングするImmutableSquareMatrix:ImmutableMatrixように要求する効率的な手段があれば提供さImmutableMatrixれましたImmutableSquareMatrix。場合でも、はImmutableSquareMatrix基本的であったImmutableMatrix、そのコンストラクタその高さと幅が一致し、必要なAsSquareMatrix方法単に(むしろ例えば構成より自体を返すImmutableSquareMatrix同じ補助配列をラップれる)は、このような設計は、正方形必要メソッドのコンパイル時のパラメータの検証を可能にしますマトリックス
-supercat

@supercat:良い点、そして質問者の例ではSystemDataHelperBuilder、単に渡すだけでなく、aを渡す必要がある関数がある場合DataHelperBuilderは、そのタイプを定義するのが理にかなっています(そしてDIをそのように扱うと仮定するとインターフェース) 。あなたの例では、行列式のような正方行列にはない演算がいくつかありますが、システムがタイプごとにチェックしたいものを必要としていない場合でも、あなたのポイントは良いです。
スティーブジェソップ

...だから、私が言ったことは完全に真実ではないと思います。「長方形が正方形かどうかは、でチェックすることで確認します」ではありheight == widthませんinstanceof。静的にアサートできるものを好むかもしれません。
スティーブジェソップ

コンストラクタ構文のようなものが静的ファクトリメソッドを呼び出すことを可能にするメカニズムがあればいいのにと思います。式の型は、foo=new ImmutableMatrix(someReadableMatrix)よりも明確でsomeReadableMatrix.AsImmutable()あっても、又はImmutableMatrix.Create(someReadableMatrix)、後者の形態は、前者が欠け有用セマンティック可能性を提供します。コンストラクターがマトリックスが正方であることがわかると、新しいオブジェクトインスタンスを作成するために正方マトリックスを必要とするコードダウンストリームを必要とするよりもクリーンになる可能性のある型を反映させることができます。
supercat

@supercat:Pythonで私が気に入っている小さなことの1つは、new演算子がないことです。クラスは単なる呼び出し可能オブジェクトであり、呼び出されると(デフォルトで)自身のインスタンスが返されます。したがって、インターフェイスの一貫性を維持したい場合は、大文字で始まる名前のファクトリー関数を完全に記述することができます。したがって、コンストラクター呼び出しのように見えます。ファクトリー関数を使用してこれを定期的に行っているわけではありませんが、必要なときにそこにあります。
スティーブジェソップ

2

サブクラス関係の最も厳密なオブジェクト指向定義は、「is-a」として知られています。正方形と長方形の従来の例を使用すると、正方形«is-a»長方形です。

これにより、«is-a»がコードにとって意味のある関係であると感じるあらゆる状況でサブクラス化を使用する必要があります。

コンストラクターのみを持つサブクラスの特定のケースでは、«is-a»の観点から分析する必要があります。SystemDataHelperBuilderは«is-a»DataHelperBuilderであることが明らかであるため、最初のパスはそれが有効な使用であることを示唆しています。

答えるべき質問は、他のコードのいずれかがこの「is-a」関係を活用しているかどうかです。他のコードは、SystemDataHelperBuilderとDataHelperBuilderの一般的な集団を区別しようとしていますか?その場合、関係は非常に合理的であり、サブクラスを保持する必要があります。

ただし、他のコードがこれを行わない場合、あなたが持っているのは«is-a»関係になる可能性のあるrelationshp、またはファクトリで実装される可能性があります。この場合、どちらの方法でもかまいませんが、«is-a»関係ではなくFactoryをお勧めします。別の個人によって記述されたオブジェクト指向コードを分析する場合、クラス階層は主要な情報源です。コードを理解するために必要な「is-a」関係を提供します。したがって、この動作をサブクラスに入れると、「スーパークラスのメソッドを掘り下げた場合に誰かが見つけるもの」から「コードに飛び込む前に誰もが見通すもの」への動作が促進されます。Guido van Rossumを引用して、「コードは書かれているよりも頻繁に読まれます」そのため、今後の開発者がコードの可読性を低下させることは、厳しい代償です。代わりに工場を使用できる場合は、支払わないことを選択します。


1

スーパータイプのインスタンスを常にサブタイプのインスタンスに置き換え、すべてが正常に機能する限り、サブタイプは決して「間違った」ものではありません。サブクラスがスーパータイプの保証を弱めようとしない限り、これはチェックアウトします。より強力な(より具体的な)保証を行うことができるため、その意味で同僚の直感は正しいです。

それを考えると、おそらくあなたが作ったサブクラスは間違っていません(私はそれらが何をするのか正確にはわからないので、確かに言うことはできません。)壊れやすい基本クラスの問題に注意する必要がありますinterfaceがあなたに利益をもたらすかどうかを検討しますが、それは継承のすべての使用に当てはまります。


1

この質問に答える鍵は、この特定の使用シナリオを調べることだと思います。

継承は、接続文字列などのデータベース構成オプションを強制するために使用されます。

クラスが単一責任プリンシパルに違反しているため、これはアンチパターンです。これは、「1つのことを行い、それをうまくやる」のではなく、その構成の特定のソースで自身を構成することです。クラスの構成をクラス自体にベイクしないでください。データベース接続文字列を設定する場合、ほとんどの場合、これはアプリケーションレベルで発生し、アプリケーションの起動時に何らかのイベントが発生します。


1

さまざまな概念を表現するために、コンストラクタのみのサブクラスを非常に定期的に使用しています。

たとえば、次のクラスを考えます。

public class Outputter
{
    private readonly IOutputMethod outputMethod;
    private readonly IDataMassager dataMassager;

    public Outputter(IOutputMethod outputMethod, IDataMassager dataMassager)
    {
        this.outputMethod = outputMethod;
        this.dataMassager = dataMassager;
    }

    public MassageDataAndOutput(string data)
    {
        this.outputMethod.Output(this.dataMassager.Massage(data));
    }
}

次に、サブクラスを作成できます。

public class CapitalizeAndPrintOutputter : Outputter
{
    public CapitalizeAndPrintOutputter()
        : base(new PrintOutputMethod(), new Capitalizer())
    {
    }
}

その後、コード内の任意の場所でCapitalizeAndPrintOutputterを使用でき、どのタイプのアウトプッターかを正確に把握できます。さらに、このパターンにより、DIコンテナーを使用してこのクラスを作成することが実際に非常に簡単になります。DIコンテナがPrintOutputMethodとCapitalizerを知っている場合、CapitalizeAndPrintOutputterを自動的に作成することもできます。

このパターンを使用すると、非常に柔軟で便利です。はい、ファクトリメソッドを使用して同じことを行うことができますが、(少なくともC#では)型システムのパワーの一部を失い、確かにDIコンテナの使用はより面倒になります。


1

最初に、コードが記述されているとき、初期化された2つのフィールドは両方ともクライアントコードによって設定可能であるため、サブクラスは実際には親よりも具体的ではないことに注意してください。

サブクラスのインスタンスは、ダウンキャストを使用してのみ区別できます。

これで、アクセスするサブクラスによってDatabaseEngineand ConnectionStringプロパティが再実装されたデザインがある場合Configuration、サブクラスを持つことをより意味のある制約にできます。

とはいえ、「アンチパターン」は私の好みには強すぎます。ここには本当に悪いことは何もありません。


0

あなたが与えた例では、パートナーは正しい。2つのデータヘルパービルダーは戦略です。一方は他方よりも具体的ですが、初期化されると両方とも交換可能です。

基本的に、datahelperbuilderのクライアントがsystemdatahelperbuilderを受信する場合、何も壊れません。別のオプションは、systemdatahelperbuilderをdatahelperbuilderクラスの静的インスタンスにすることです。

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