この記事は非常にシンプルで、明らかなことを説明していますが、プログラミング講義の最初の段落の言葉にしか出会っていない初心者プログラマーにとって興味深いものになることを願っています。 (実際、この記事はプログラミングに関する実践的なレッスンの一部です。)
そのため、オブジェクトを監視するための次のインターフェースを用意しましょう。
interface AListener { public void aChanged(); public void bChanged(); }
そして、オブザーバブルオブジェクトのそのようなクラス。
class AEventSource { private List<AListener> listeners = new ArrayList<AListener>(); public void add(AListener listener) { listeners.add(listener); } private void aChanged() { for (AListener listener : listeners) { listener.aChanged(); } } private void bChanged() { for (AListener listener : listeners) { listener.bChanged(); } } public void f() { ... aChanged(); ... bChanged(); ... } ... }
ここでは、
aChanged()
および
bChanged()
メソッドで、このイベントが発生したことがすべてのリスナーに通知されます。 また、
f()
メソッドでは、これらのメソッドが適切なタイミングで呼び出されます。
これはすべて機能しますが、ある時点で、リスナーに通知するさまざまな方法を用意する機会が必要であることを理解しましょう。 たとえば、異なる順序で生徒に通知したり、一部のリスナーの条件に応じて、変更について通知しないようにします。 この問題には少なくとも2つの解決策があります。 まず、さまざまな通知メソッドと、いずれかの方法を選択するコードを追加して、
AEventSource
クラスを複雑にすることができます。 第二に、生徒への通知に関連するすべてのものを別のクラスに配置できます。 次に、たとえば、目的のオブジェクトをコンストラクターパラメーターとして渡すことで、目的の通知方法を設定できます。
2番目の方法は、2つの理由により優れています。
AEventSource
対象の
AEventSource
オブジェクトのクラスが1つではなく、2つ(またはそれ以上)のクラス
AEventSource1
、
AEventSource2
であり、最初のメソッドを選択する可能性が高い場合、2つのクラスで通知用の新しい複雑なコードを複製する必要があり、これは既にコピーされています-貼り付け、つまり、悪。 2番目の理由は、2番目のアプローチが単一責任の原則 (SRPの原則)を尊重するためです。 実際、
AEventSource
クラスを変更する理由
AEventSource
2つ
AEventSource
通知順序を変更する場合と、通知理由を変更する場合、つまり上記の例の
f()
関数です。
そこで、2番目の方法を選択します。別のクラスの生徒に通知するためのコードを取り出します。 同時に、もう少し興味深いこと、つまりリンカーパターンを使用します。 これにより、リスナーに通知するさまざまな方法を選択できるだけでなく、それらを組み合わせることができます。
class ACompositeListener implements AListener { private List<AListener> listeners = new ArrayList<AListener>(); public void add(AListener listener) { listeners.add(listener); } @Override public void aChanged() { for (AListener listener : listeners) { listener.aChanged(); } } @Override public void bChanged() { for (AListener listener : listeners) { listener.bChanged(); } } }
次に、
AEventSource
クラスは次のようになります。
class AEventSource { private AListener listener; AEventSource(AListener listener) { this.listener = listener; } public void f() { ... listener.aChanged(); ... listener.bChanged(); ... } }
この場合、リスナーから人形全体を作成できます。
ACompositeListener compositeListener1 = new ACompositeListener(); compositeListener1.add(new AConcreteListener1()); ACompositeListener compositeListener2 = new ACompositeListener(); compositeListener1.add(compositeListener2); compositeListener2.add(new AConcreteListener2()); compositeListener2.add(new AConcreteListener3()); AEventSource source = new AEventSource(compositeListener1);
確かに、このソリューションには欠点があります。 監視オブジェクトの集約を作成する際にこのような柔軟性が得られたため、把握するのが非常に困難な順序および通知ルールで集約を作成することができます。
繰り返しますが、このコードはうまく機能しますが、突然判明します(クラス名のどこにでも文字
A
を追加したわけではありません)。また、リスナーを操作できるようにする必要があります。
dChanged()
。 つまり、
DListener
リスナー用の2番目のインターフェースが
DListener
ます。
interface DListener extends AListener { public void dChanged(); }
したがって、何らかの方法でコードを再利用する必要があります。 コードを再利用するには、少なくとも3つの方法があります。 これは、最初にコピーと貼り付け、次に継承、最後に構成+委任です。 イデオロギー上の理由により、最初のものをすぐに破棄します。
DListener
インターフェースが
DListener
インターフェースを拡張しているという事実にもかかわらず、単純な継承が私たちに適さない理由を見てみましょう。 しかし、本当に、私たちが書くとすぐに
class DCompositeListener extends ACompositeListener { ... }
DListener
ではなく、
DListener
インターフェイスを実装できるオブジェクトを追加できるため、
add(AListener listener)
メソッドにはすぐに問題が発生します。 このメソッドを次のように再定義する場合:
@Override public void add(DListener listener) { ... }
コンパイラは慎重に次のように言います。
method does not override or implement a method from a supertype
(
@Override
アノテーションのおかげです)。
さて、なぜこのメソッド内でチェックを行わず、追加されたオブジェクトがDListenerインターフェイスを実装していない場合、例外をスローしますか? つまり、次のようなものを書かないのです。
@Override public void add(final AListener listener) { if (listener instanceof DListener) { listeners.add(listener); } else { throw new IllegalArgumentException(); } }
多少不器用に見えるという事実に加えて、Lisk置換 (LSPの置換 )の原則に違反しています。 次のように定式化できます: 基本クラスへの参照を使用する関数は、それを知らなくても派生クラスのオブジェクトを使用できる必要があります 。 実際、関数
void g(ACompositeListener composite) { composite.add(new AConcreteListener1()); }
それは完全に無害に見え、リスナーを
composite
追加できることを(そして正当に)期待しています。 ただし、ここでは
g()
関数を次のように呼び出します。
g(new DCompositeListener());
そして、例外を除いてすべてが落ちます。 そして、私たちの目標は、これをコンパイルすることさえ不可能にすることです。
はい、
dChanged()
インターフェイスを完全に放棄し、
dChanged()
メソッドを持たないすべてのリスナーに、
dChanged()
メソッドの空の実装または例外をスローする実装を追加する
dChanged()
は、インターフェイス分離 (別名ISP)の原則に違反するため、破棄します 。
したがって、構成+委任は残ります。 まず、
ACompositeListener
クラスから、新しい
ListenerList
クラスへのリスナーの追加、削除、および反復処理を行うすべてのコードを
ACompositeListener
ます。 また、さまざまなタイプのリスナーがあるため、パラメーター化されたクラスになります。
class ListenerList<T> implements Iterable<T> { protected List<T> listeners = new ArrayList<T>(); public void add(T listener) { listeners.add(listener); } public void remove(T listener) { listeners.remove(listener); } public Iterator<T> iterator() { return listeners.iterator(); } }
生徒に通知するクラスは次のようになります。
class ATranslator<T extends AListener> { protected ListenerList<T> listeners; public ATranslator(ListenerList<T> listeners) { this.listeners = listeners; } public void aChanged() { for (AListener listener : listeners) { listener.aChanged(); } } public void bChanged() { for (AListener listener : listeners) { listener.bChanged(); } } } class DTranslator<T extends DListener> extends ATranslator<T> { public DTranslator(ListenerList<T> listeners) { super(listeners); } public void dChanged() { for (DListener listener : listeners) { listener.dChanged(); } } }
ここでは制限タイプ(
<T extends XListener>
)が使用されているため、対応するインターフェースを実装するリスナーのみをリストに追加できます。 目的のインターフェイスを実装しないリスナーを追加しようとすると、コンパイルエラーが発生します。 私たちは何を達成しました。
これで、
ACompositeListener
と
ACompositeListener
は単純に次の
ACompositeListener
:
class ACompositeListener extends ListenerList<AListener> implements AListener { private ATranslator<AListener> aTranslator = new ATranslator<AListener>(this); @Override public void aChanged() { aTranslator.aChanged(); } @Override public void bChanged() { aTranslator.bChanged(); } }
class DCompositeListener extends ListenerList<DListener> implements DListener { private DTranslator<DListener> dTranslator = new DTranslator<DListener>(this); @Override public void aChanged() { dTranslator.aChanged(); } @Override public void bChanged() { dTranslator.bChanged(); } @Override public void dChanged() { dTranslator.dChanged(); } }
それだけです
一般に、パターンとOOPについては、GoFで読む価値があるものが明確です。 habrastatiaのLinker パターンについて読むことができます。spiff habrayuzerのデザインパターン「Linker」/「Composite」 。 HorstmannとCornellによるコアJavaの第1巻の第13章で、Javaのユニバーサルタイプについて読むことができます。 habrastatのBuilderパターンの同様の問題について読むことができます。 拡張可能なクラス-拡張可能なBuilder! Habrauser gvsmirnovとその記事へのコメント。 OOPのアイデアに対する過度の熱意がもたらす可能性はhabrastatiaで説明されています。 そのため、アルゴリズムにファクトリーを組み込むか、階乗をどのように考慮するべきではないかを説明します 。