オブザーバーパターン:リスナーからのリストとネストされた人形

このhabrastatieでは、ObserverパターンとLinkerパターンの例を使用して、オブジェクト指向プログラミングの原理を適用する方法、構成を使用するタイミング、および継承するタイミングを検討します。 また、コピーと貼り付けを除く、コードの再利用方法が存在することも考慮されます。



この記事は非常にシンプルで、明らかなことを説明していますが、プログラミング講義の最初の段落の言葉にしか出会っていない初心者プログラマーにとって興味深いものになることを願っています。 (実際、この記事はプログラミングに関する実践的なレッスンの一部です。)





そのため、オブジェクトを監視するための次のインターフェースを用意しましょう。



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で説明されています。 そのため、アルゴリズムにファクトリーを組み込むか、階乗をどのように考慮するべきではないかを説明します



All Articles