同僚と話していたところ、サブクラス化の目的について直観が矛盾してしまいました。私の直感は、サブクラスの主な機能が親の可能な値の限られた範囲を表現することである場合、おそらくサブクラスであってはならないということです。彼は反対の直感を主張しました:サブクラス化はオブジェクトがより「特定」であることを表すので、サブクラスの関係がより適切です。
私の直感をより具体的に言えば、親クラスを拡張するサブクラスがあるが、サブクラスがオーバーライドする唯一のコードがコンストラクターである場合(そう、私はコンストラクターが一般に「オーバーライド」しないことを知っているので、私に耐えてください)、本当に必要なのはヘルパーメソッドでした。
たとえば、次の実際のクラスを考えてみます。
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())
{
}
}
だから、質問:
これが簡単にグーグルで答えられたと判明した場合は、謝罪します。グーグルでの私の検索は、ほとんどが望遠鏡のコンストラクタのアンチパターンに関する情報を返しましたが、私が探していたものではありませんでした。
クラスXを特定の引数で作成するだけの場合、クラスと継承によって提供される機能を使用していないため、サブクラス化はその意図を表す奇妙な方法です。これは実際にはアンチパターンではなく、奇妙で少し意味がありません(他の理由がない限り)。この意図を表現するより自然な方法はFactory Methodです。この場合、これは「ヘルパーメソッド」のファンシーな名前です。
一般的な直感については、「より具体的」と「限定された範囲」はどちらも、SquareをRectangleのサブクラスにすることは良い考えであることを暗示しているため、サブクラスについて考える潜在的に有害な方法です。 LSPのような正式なものに依存せずに、サブクラスがベースインターフェースの実装を提供するか、またはインターフェイスを拡張して新しい機能を追加します。
これらの直感のどれが正しいですか?
同僚は正しい(標準型システムを想定)。
考えてみてください。クラスは可能な法的価値を表しています。クラスA
に1バイトフィールドF
がある場合、A
には256の有効な値があると考える傾向があるかもしれませんが、その直感は正しくありません。 A
は、「すべての値の順列」を「フィールドF
が0〜255でなければならない」に制限しています。
別のバイトフィールドA
があるB
でFF
を拡張する場合、それは制限を追加するです。 「F
がバイトである値」の代わりに、「F
がバイトである値&& FF
もバイトである値」があります。古いルールを維持しているので、ベースタイプで機能したものはすべてサブタイプでも機能します。しかし、ルールを追加しているため、「何でもかまいません」からさらに専門化しています。
あるいは、別の言い方をすると、A
にB
、C
、およびD
のサブタイプがたくさんあると想定します。 A
型の変数は、これらのサブタイプのいずれでもかまいませんが、B
型の変数はより具体的です。 (ほとんどの型システムでは)C
またはD
にすることはできません。
これはアンチパターンですか?
え?特殊なオブジェクトを持つことは有用であり、コードに明らかに有害ではありません。サブタイピングを使用してその結果を達成することは、おそらく少し疑問ですが、使用できるツールに依存します。より優れたツールを使用した場合でも、この実装はシンプルでわかりやすく、堅牢です。
これは明らかにどのような状況でも悪くないので、アンチパターンとは見なしません。
いいえ、それはアンチパターンではありません。このためのいくつかの実用的な使用例を考えることができます。
MySQLDao
とSqliteDao
があるが、何らかの理由でコレクションに1つのソースからのデータのみが含まれるようにしたい場合、サブクラスを使用して説明する場合は、コンパイラーにこの特定の正確性を検証させることができます。一方、データソースをフィールドとして保存する場合、これはランタイムチェックになります。AlgorithmConfiguration
+ ProductClass
とAlgorithmInstance
の間に1対1の関係があります。つまり、プロパティファイルでFooAlgorithm
に対してProductClass
を設定した場合、その製品クラスに対して取得できるFooAlgorithm
は1つだけです。特定のFooAlgorithm
に対して2つのProductClass
を取得する唯一の方法は、サブクラスFooBarAlgorithm
を作成することです。これは問題ありません。プロパティファイルを使いやすくし(プロパティファイルの1つのセクションで多くの異なる構成の構文を使用する必要がないため)、特定の製品クラスに複数のインスタンスが存在することは非常にまれです。結論:これを実行しても、実際には害はありません。 アンチパターンは次のように定義されています:
アンチパターン(またはアンチパターン)は、通常は効果がなく、リスクが高い逆効果であるという繰り返し発生する問題に対する一般的な応答です。
ここで非常に逆効果になるリスクはないので、アンチパターンではありません。
何かがパターンであるかアンチパターンであるかは、記述している言語と環境に大きく依存します。たとえば、for
loops are a pattern in Assembly、C、および類似の言語とLISPのアンチパターン。
ずっと前に書いたコードの話をしましょう...
それは [〜#〜] lpc [〜#〜] と呼ばれる言語であり、ゲームで呪文を唱えるためのフレームワークを実装しました。いくつかのチェックを処理する戦闘スペルのサブクラスを持つスペルスーパークラスがあり、その後、個々の呪文(マジックミサイル、ライトニングボルトなど)にサブクラス化される単一ターゲットの直接ダメージスペルのサブクラスがありました。
LPCのしくみの本質は、シングルトンであるマスターオブジェクトがあったことです。あなたが「eww Singleton」に行く前に、それは「eww」ではなかった。 1つは呪文のコピーを作成しませんでしたが、呪文の(効果的に)静的メソッドを呼び出しました。マジックミサイルのコードは次のようになります。
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}";
}
そして、あなたはそれを見ますか?そのコンストラクタのみのクラスです。親の抽象クラスにあるいくつかの値を設定し(いいえ、セッターを使用すべきではありません-不変です)、それはそれでした。うまくいきましたright。コーディングも拡張も理解も簡単でした。
この種のパターンは、抽象クラスを拡張し、独自のいくつかの値を設定するサブクラスがある他の状況に変換されますが、すべての機能は、継承する抽象クラスにあります。 StringBuilder in Javaを一目で確認してください。コンストラクタだけではありませんが、メソッド自体に多くのロジックを見つけるように迫られています(すべてがAbstractStringBuilder)。
これは悪いことではなく、確かにアンチパターンではありません(一部の言語では、それ自体がパターンである場合があります)。
サブクラス関係の最も厳密なオブジェクト指向の定義は、"is-a"として知られています。正方形と長方形の伝統的な例を使用すると、正方形の「is-a」長方形です。
これにより、"is-a"がコードにとって意味のある関係であると感じる状況では、サブクラス化を使用する必要があります。
コンストラクターのみのサブクラスの特定のケースでは、"is-a"の観点から分析する必要があります。 SystemDataHelperBuilderが"is-a"DataHelperBuilderであることは明らかであるため、最初のパスはそれが有効な使用であることを示唆しています。
答えるべき質問は、他のコードのいずれかがこの「is-a」関係を利用しているかどうかです。他のコードは、SystemDataHelperBuildersをDataHelperBuildersの一般的な母集団から区別しようとしますか?もしそうなら、関係はかなり合理的であり、サブクラスを維持する必要があります。
ただし、他のコードでこれを実行しない場合は、"is-a"の関係であるRelationshpか、ファクトリで実装できます。この場合、どちらの方法でもかまいませんが、"is-a"関係ではなくFactoryをお勧めします。別の個人が作成したオブジェクト指向コードを分析する場合、クラス階層は主要な情報源です。これは、コードを理解するために必要な「is-a」関係を提供します。したがって、この動作をサブクラスに入れると、「スーパークラスのメソッドに掘り下げたときに誰かが見つけられるもの」から「コードに飛び込む前に誰もが調べるもの」への動作が促進されます。 Guido van Rossumを引用すると、「コードは書かれたよりも頻繁に読み取られる」ので、今後の開発者にとってコードの可読性を低下させることは、かなりの代償を払うことになります。代わりに工場を使うことができれば、私はそれを払わないことを選びます。
私が考えることができるそのようなサブクラスの最も一般的な使用は例外階層ですが、言語がコンストラクターを継承できる限り、それは通常クラスで何も定義しない退化したケースです。継承を使用して、ReadError
がIOError
の特殊なケースであることを表現しますが、ReadError
はIOError
のメソッドをオーバーライドする必要はありません。
これは、例外がそのタイプをチェックすることによってキャッチされるためです。したがって、必要に応じてReadError
をすべてキャッチすることなく、誰かがIOError
のみをキャッチできるように、特殊化をタイプにエンコードする必要があります。
通常、もののタイプをチェックすることはひどく悪い形であり、私たちはそれを避けようとします。回避に成功した場合、型システムで特殊化を表現する必要はありません。
ただし、たとえば、型の名前がオブジェクトのログに記録された文字列形式で表示される場合は、これは有用かもしれません。これは言語であり、おそらくフレームワーク固有のものです。原則として、そのようなシステムの型の名前をクラスのメソッドの呼び出しに置き換えることができます。その場合、wouldは、SystemDataHelperBuilder
のコンストラクタだけではなく、オーバーライドします。また、loggingname()
もオーバーライドします。したがって、システムにそのようなロギングがある場合、クラスは文字通りコンストラクタをオーバーライドするだけであるが、「道徳的に」それはクラス名もオーバーライドしていると想像できます。したがって、実際には、コンストラクタのみのサブクラスではありません。それは望ましい異なる振る舞いをしており、他の場所でのリフレクションの巧妙な使用のおかげで、メソッドのオーバーライドを記述せずにこの振る舞いを無料で得るだけです。
したがって、ブランチで明示的に(タイプに基づいてブランチをキャッチする例外のように)、または何らかの理由でSystemDataHelperBuilder
のインスタンスを何らかの方法でチェックするコードが他にある場合、彼のコードは良いアイデアになると思います最終的にSystemDataHelperBuilder
という名前を便利な方法で使用する一般的なコード(たとえそれが単なるロギングであっても)。
ただし、特殊な場合に型を使用すると混乱が生じます。ImmutableRectangle(1,1)
とImmutableSquare(1)
の動作が異なる場合何らかの方法でだと、結局誰かがなぜか疑問に思うでしょうそしておそらくそれがしなかったことを望みます。例外階層とは異なり、長方形が正方形かどうかは、height == width
、instanceof
ではありません。あなたの例では、まったく同じ引数で作成されたSystemDataHelperBuilder
とDataHelperBuilder
の間に違いがあり、それが問題を引き起こす可能性があります。したがって、「正しい」引数で作成されたオブジェクトを返す関数は、通常私には正しいことのように見えます。サブクラスは「同じ」ことの2つの異なる表現をもたらし、それはあなたが何かをしている場合にのみ役立ちます通常はしないようにします。
私がnotを設計パターンとアンチパターンの観点から話していることに注意してください。プログラマーがこれまで考えてきたすべての良いアイデアと悪いアイデアの分類法を提供することは不可能だからです。 。したがって、すべてのアイデアがパターンまたはアンチパターンであるとは限りません。誰かが異なるコンテキストで繰り返し発生することを識別してそれに名前を付けると、それだけになります;-)私は、この種のサブクラスの特定の名前を知りません。
スーパータイプのインスタンスを常にサブタイプのインスタンスに置き換えて、すべてが正しく機能する限り、サブタイプが「間違っている」ことは決してありません。これは、サブクラスがスーパータイプの保証を弱めようとしない限りチェックアウトします。より強力な(より具体的な)保証を行うことができるため、その意味で同僚の直感は正しいです。
それを考えると、作成したサブクラスはおそらく間違っていません(私はそれらが何をしているのか正確にわからないので、私は確信が持てません)。壊れやすい基本クラスの問題に注意する必要がありますinterface
があなたに利益をもたらすかどうかを検討してください。しかし、それは継承のすべての使用に当てはまります。
この質問に答える鍵は、この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を自動的に作成することもできます。
このパターンを使用すると、非常に柔軟で便利です。はい、あなたはcanファクトリーメソッドを使用して同じことを行いますが、(少なくともC#では)型システムの能力の一部を失い、DIコンテナーの使用がより面倒になります。
初期化された2つのフィールドは両方ともクライアントコードによって設定可能であるため、コードが記述されるとき、サブクラスは実際には親よりも具体的ではないことに最初に注意してください。
サブクラスのインスタンスは、ダウンキャストを使用してのみ区別できます。
ここで、DatabaseEngine
プロパティとConnectionString
プロパティがサブクラスによって再実装され、Configuration
に再実装されたデザインがある場合、制約があり、サブクラスを持つ感覚。
とはいえ、「アンチパターン」は私の好みには強すぎます。ここには何も問題はありません。
あなたが与えた例では、あなたのパートナーは正しいです。 2つのデータヘルパービルダーは戦略です。 1つは他より具体的ですが、初期化するとどちらも交換可能です。
基本的に、datahelperbuilderのクライアントがsystemdatahelperbuilderを受信する場合、何も壊れません。別のオプションは、systemdatahelperbuilderをdatahelperbuilderクラスの静的インスタンスにすることでした。