パブリッシュ/サブスクライブモードとも呼ばれるオブザーバーモードは、1994年の「デザインパターン:再利用可能なオブジェクト指向ソフトウェアの基本」(詳細については293-313ページを参照)で、4人のグループ(GOF、Erich Gamma、Richard Helm、Ralph Johnson、John Vlissides)によって提案されました。このパターンにはかなりの歴史がありますが、依然としてさまざまなシナリオに広く適用されており、標準のJavaライブラリの不可欠な部分にさえなりました。オブザーバーパターンに関する記事はすでにたくさんありますが、それらはすべてJavaでの実装に焦点を当てていますが、Javaでオブザーバーパターンを使用する際に開発者が発生するさまざまな問題を無視します。
この記事を書くという当初の意図は、このギャップを埋めることです。この記事では、主にJava8アーキテクチャを使用してオブザーバーパターンの実装を紹介し、匿名の内部クラス、ラムダ式、糸の安全性、非些細な時間をかけるオブザーバーの実装など、この基礎に関する古典的なパターンに関する複雑な問題をさらに調査します。この記事の内容は包括的ではありませんが、このモデルに関係する複雑な問題の多くは、1つの記事でのみ説明することはできません。しかし、この記事を読んだ後、読者はオブザーバーのパターンが何であるか、Javaの普遍性、Javaでオブザーバーパターンを実装する際にいくつかの一般的な問題に対処する方法を理解できます。
オブザーバーモード
GOFによって提案された古典的な定義によると、オブザーバーパターンのテーマは次のとおりです。
オブジェクト間の1対多くの依存関係を定義します。オブジェクトの状態が変更されると、それに依存するすべてのオブジェクトは通知され、自動的に更新されます。
それはどういう意味ですか?多くのソフトウェアアプリケーションでは、オブジェクト間の状態は相互依存しています。たとえば、アプリケーションが数値データ処理に焦点を当てている場合、このデータはグラフィカルユーザーインターフェイス(GUI)のテーブルまたはチャート(GUI)を介して表示されるか、同時に使用される場合があります。つまり、基礎となるデータが更新されると、対応するGUIコンポーネントも更新する必要があります。問題の鍵は、GUIコンポーネントが更新されたときに基礎となるデータを更新する方法と同時に、GUIコンポーネントと基礎となるデータ間の結合を最小化する方法です。
シンプルで非スケーラブルなソリューションは、これらの基礎となるデータを管理するオブジェクトのテーブルおよび画像GUIコンポーネントを参照して、基礎となるデータが変更されたときにGUIコンポーネントに通知できるようにすることです。明らかに、この単純なソリューションは、より多くのGUIコンポーネントを処理する複雑なアプリケーションの欠点をすぐに示しました。たとえば、すべてが基礎となるデータに依存する20のGUIコンポーネントがあるため、基礎となるデータを管理するオブジェクトは、これらの20のコンポーネントへの参照を維持する必要があります。関連データに依存するオブジェクトの数が増加すると、データ管理とオブジェクトの間の結合の程度を制御が困難になります。
もう1つのより良い解決策は、オブジェクトが登録してアクセス許可を取得して関心のあるデータを更新できるようにすることです。データマネージャーは、データが変更されたときにそれらのオブジェクトに通知します。素人の用語では、関心のあるデータオブジェクトにマネージャーに次のように伝えます。「データが変更されたら通知してください」。さらに、これらのオブジェクトは、更新通知を取得するために登録するだけでなく、登録をキャンセルして、データマネージャーがデータが変更されたときにオブジェクトに通知されなくなったことを確認できます。 GOFの元の定義では、更新を取得するために登録されたオブジェクトは「オブザーバー」と呼ばれ、対応するデータマネージャーは「サブジェクト」と呼ばれ、オブザーバーが関心があるデータは「ターゲット状態」と呼ばれ、登録プロセスは「追加」と呼ばれ、観測の元を元に戻すプロセスは「デタッチ」と呼ばれます。上記のように、オブザーバーモードはパブリッシュサブスクライブモードとも呼ばれます。顧客がターゲットについてオブザーバーに購読することが理解できます。ターゲットステータスが更新されると、ターゲットはこれらの更新をサブスクライバーに公開します(この設計パターンは、パブリッシュサブスクライブアーキテクチャと呼ばれる一般アーキテクチャに拡張されます)。これらの概念は、次のクラス図で表すことができます。
ConcereteObserverは、それを使用して更新状態の変更を受信し、そのコンストラクターへのconeretesubjectへの参照を渡します。これにより、特定の観察者の特定の主題への参照が提供され、状態が変更されたときに更新を取得できます。簡単に言えば、特定のオブザーバーは、トピックを更新するように指示され、同時にそのコンストラクターの参照を使用して特定のトピックの状態を取得し、最後にこれらの検索状態オブジェクトを特定のオブザーバーの観測プロパティの下に保存します。このプロセスは、次のシーケンス図に示されています。
古典モデルの専門化<BR />オブザーバーモデルは普遍的ですが、多くの専門モデルもあります。その中で最も一般的なものは次の2つです。
1.状態オブジェクトにパラメーターを提供し、オブザーバーが呼び出す更新メソッドに渡します。クラシックモードでは、観察者が対象状態が変更されたことを通知されると、その更新状態は被験者から直接取得されます。これには、検索された状態へのオブジェクト参照を保存するには、オブザーバーが必要です。これにより、循環参照が形成され、concreteSubjectの参照がオブザーバーリストを指し、concreteobserverの参照は、被験者状態を取得できるconcretesubjectを指します。更新された状態を取得することに加えて、オブザーバーとそれが登録して耳を傾ける主題との間には関係がありません。オブザーバーは、被験者自体ではなく、状態オブジェクトを気にします。つまり、多くの場合、concreteteobserverとconcretesubjectは強制的にリンクされています。それどころか、ConcreTesubjectが更新関数を呼び出すと、状態オブジェクトはConcreteObserverに渡され、2つを関連付ける必要はありません。 ConcreteObserverとState Objectの関連は、オブザーバーと状態の間の依存度を減らします(関連性と依存の違いについては、Martin Fowlerの記事を参照)。
2.主題の抽象クラスとconcretesubjectをSinglesubjectクラスにマージします。ほとんどの場合、被験者に抽象クラスを使用しても、プログラムの柔軟性とスケーラビリティが向上しないため、この抽象クラスとコンクリートクラスを組み合わせることで、設計が簡素化されます。
これら2つの特殊なモデルが組み合わされた後、簡略化されたクラス図は次のとおりです。
これらの特殊なモデルでは、静的クラス構造が大幅に簡素化され、クラス間の相互作用も簡素化されます。この時点でのシーケンス図は次のとおりです。
専門モードのもう1つの機能は、concreteobserverのメンバー変数観測材の削除です。特定のオブザーバーは、主題の最新の状態を保存する必要がない場合がありますが、ステータスが更新されたときに主題のステータスを監視するだけで済む必要があります。たとえば、オブザーバーがメンバー変数の値を標準出力に更新すると、ConcreteObserverとStateクラスとの関連を削除するObserverStateを削除できます。
より一般的なネーミングルール<BR />クラシックモード、さらには上記のプロフェッショナルモードでさえ、添付、デタッチ、オブザーバーなどの用語を使用しますが、多くのJavaの実装はレジスタ、登録、リスナーなどを含むさまざまな辞書を使用しています。状態オブジェクトの特定の名前は、オブザーバーモードで使用されるシナリオによって異なります。たとえば、リスナーがイベントの発生を聴くシーンのオブザーバーモードでは、登録されたリスナーがイベントが発生したときに通知を受け取ります。現時点でのステータスオブジェクトは、イベント、つまりイベントが発生したかどうかです。
実際のアプリケーションでは、ターゲットの命名には件名が含まれることはめったにありません。たとえば、動物園に関するアプリを作成し、複数のリスナーを登録して動物園クラスを観察し、新しい動物が動物園に入ったときに通知を受け取ります。この場合の目標は動物園クラスです。指定された問題ドメインと一致する用語を維持するために、「主題」という用語は使用されません。つまり、ZooクラスはZooSubjectと呼ばれません。
リスナーの命名の後に、リスナーの接尾辞が続きます。たとえば、新しい動物を監視するために上記のリスナーは、AnimalAddedListenerと名付けられます。同様に、レジスタ、Unregister、Notifyなどの関数の命名は、対応するリスナー名によってしばしば接尾辞が付いています。たとえば、登録、登録、および通知AnimalAddedListenerの関数は、レジスタアニマラデッドリステナー、UngisteranimaladdedListener、およびNotifyAnimalAddedListenersと呼ばれます。 Notify関数は単一のリスナーではなく複数のリスナーを処理するため、Notify Function Name Sが使用されることに注意する必要があります。
この命名方法は長く表示され、通常、被験者は複数のタイプのリスナーを登録します。たとえば、動物園の動物園の例では、動物を監視するための新しいリスナーを登録することに加えて、リスナーを削減するためにリスナーを動物に登録する必要があります。現時点では、2つのレジスタ関数があります。(レジスタアニマラデッドリステナーとレジスタアニマルレモヴェッドリステナー。このようにして、リスナーのタイプはオブザーバーのタイプを示すために修飾子として使用されます。
別の慣用的な構文は、更新の代わりにプレフィックスで使用することです。たとえば、更新関数は、updateanimaladdedの代わりにonanimaladdedという名前です。この状況は、リスナーが動物をリストに追加するなど、シーケンスの通知を取得するとより一般的ですが、動物の名前などの別のデータを更新するために使用されることはめったにありません。
次に、この記事ではJavaの象徴的なルールを使用します。象徴的なルールはシステムの実際の設計と実装を変更しませんが、他の開発者がよく知っている用語を使用することは重要な開発原則であるため、上記のJavaのオブザーバーパターンのシンボリックルールに精通している必要があります。上記の概念は、Java 8環境の簡単な例を使用して以下で説明します。
簡単な例
また、上記の動物園の例でもあります。 Java8のAPIインターフェイスを使用して、シンプルなシステムを実装し、オブザーバーパターンの基本原則を説明します。問題は次のとおりです。
システム動物園を作成し、ユーザーが新しいオブジェクトアニマルを追加する状態を聞いて元に戻し、新しい動物の名前を出力する責任を負う特定のリスナーを作成できるようにします。
オブザーバーパターンの以前の学習によると、このようなアプリケーションを実装するには、4つのクラスを作成する必要があることがわかっています。
最初に、名前のメンバー変数、コンストラクター、ゲッター、セッターメソッドを含む単純なJavaオブジェクトである動物クラスを作成します。コードは次のとおりです。
Public Class Animal {プライベート文字列名; public Animal(string name){this.name = name; } public string getname(){return this.name; } public void setName(string name){this.name = name; }}このクラスを使用して動物のオブジェクトを表してから、AnimalAddedListenerインターフェイスを作成できます。
パブリックインターフェイスAnimalAddedListener {public void onanimaladded(動物動物);}最初の2つのクラスは非常にシンプルなので、詳細に紹介しません。次に、動物園クラスを作成します。
パブリッククラスZoo {プライベートリスト<動物>動物= new ArrayList <>();プライベートリスト<AnimalAddedListener>リスナー= new ArrayList <>(); public void addanimal(動物動物){//動物を動物のリストに追加するthis.animals.add(動物); //登録されたリスナーのリストに通知します。 } public void RegistAnimalAddedListener(AnimalAddedListenerリスナー){//登録リスナーのリストにリスナーを追加します。Listeners.Add(リスナー); } public void unregisteranimaladdedlistener(AnimalAddedListenerリスナー){//登録されたリスナーのリストからリスナーを削除します。 }保護されたvoid notifyAnimaladdedlisteners(動物動物){//登録されたリスナーのリストにある各リスナーに通知するthis.listeners.foreach(リスナー - >リスナー.updateanimaladded(動物); }}この類推は、前の2つよりも複雑です。 2つのリストが含まれています。1つは動物園にすべての動物を保管するために使用され、もう1つはすべてのリスナーを保存するために使用されます。動物とリスナーコレクションに保存されているオブジェクトが簡単であることを考えると、この記事はストレージ用のArrayListを選択しました。保存されたリスナーの特定のデータ構造は、問題に依存します。たとえば、ここでの動物園の問題の場合、リスナーが優先される場合は、別のデータ構造を選択するか、リスナーのレジスタアルゴリズムを書き直す必要があります。
登録と削除の実装は、単純なデリゲート方法の両方です。各リスナーは、リスナーのリスニングリストからパラメーターとして追加または削除されます。 Notify関数の実装は、オブザーバーパターンの標準形式からわずかにオフです。入力パラメーター:新しく追加された動物が含まれているため、Notify機能がリスナーに新しく追加された動物の参照を渡すことができます。 Streams APIのFOREACH関数を使用して、リスナーをトラバースし、各リスナーでTheonAnimalAdded関数を実行します。
AddAnimal関数では、新しく追加された動物オブジェクトとリスナーが対応するリストに追加されます。通知プロセスの複雑さが考慮されていない場合、このロジックは便利な通話方法に含める必要があります。新しく追加された動物のオブジェクトへの参照で渡すだけです。これが、通知リスナーの論理的実装がNotifyAnimalAddedListeners関数にカプセル化されている理由であり、これはAddAnimalの実装でも言及されています。
Notify関数の論理的な問題に加えて、Notify関数の可視性に関する物議を醸す問題を強調する必要があります。 GOFが本のデザインパターンの301ページで述べたように、古典的なオブザーバーモデルでは、Notify関数は公開されていますが、古典的なパターンで使用されていますが、これは公開されなければならないという意味ではありません。可視性の選択は、アプリケーションに基づいている必要があります。たとえば、この記事の動物園の例では、Notify関数は保護されたタイプであり、各オブジェクトに登録されたオブザーバーの通知を開始する必要はありません。オブジェクトが親クラスから関数を継承できるようにする必要があります。もちろん、これはまさにそうではありません。どのクラスがNotify関数をアクティブにすることができるかを把握し、関数の可視性を決定する必要があります。
次に、printnameanimaladdedlistenerクラスを実装する必要があります。このクラスでは、System.out.printlnメソッドを使用して、新しい動物の名前を出力します。特定のコードは次のとおりです。
パブリッククラスのprintnameanimaladdedlistenerは、動物addedlistenerを実装します{@Override public boid updateanimaladded(//新しく追加された動物システムの名前を印刷します。 }}最後に、アプリケーションを駆動する主な関数を実装する必要があります。
public class main {public static void main(string [] args){//動物園を作成して動物動物園= new Zoo(); //動物が追加されたときに通知されるリスナーを登録するZoo.registeranimalAddedListener(new PrintnameanimalAddedListener()); //動物の追加登録されたリスナーZoo.Addanimal(新しい動物( "Tiger")); }}メイン関数は、動物園オブジェクトを作成し、動物名を出力するリスナーを登録し、登録されたリスナーをトリガーする新しい動物オブジェクトを作成します。最終出力は次のとおりです。
「タイガー」という名前の新しい動物を追加しました
リスナーを追加しました
オブザーバーモードの利点は、リスナーが再確立され、被験者に追加されたときに完全に表示されます。たとえば、動物園で動物の総数を計算するリスナーを追加する場合は、特定のリスナークラスを作成し、動物園クラスに変更せずにZooクラスに登録するだけです。カウントリスナーCountinganimalAddedListenerコードを追加することは次のとおりです。
パブリッククラスCountingAnimalAddedListenerはAnimalAddedListener {private static int AnimalsAddedCount = 0; @override public void updateanimaladded(動物動物){//動物の数を増やす動物の数を増やします++; //動物の数を印刷しますsystem.out.println( "総動物が追加されました:" + AnimalsAddedCount); }}変更されたメイン関数は次のとおりです。
public class main {public static void main(string [] args){//動物園を作成して動物動物園= new Zoo(); //動物が追加されたときに通知されるリスナーを登録zoo.registeranimaladdedlistener(new printnameanimaladdedlistener()); zoo.registeranimaladdedlistener(new countinganimaladdedlistener()); //動物の追加登録されたリスナーZoo.Addanimal(新しい動物( "Tiger")); zoo.addanimal(new Animal( "lion")); zoo.addanimal(new Animal( "Bear")); }}出力の結果は次のとおりです。
名前付きの新しい動物を追加しました 'tiger'total Animalsを追加:1つの名前の新しい動物を追加しました' lion'total Animalsを追加しました:2つの新しい動物を付けた 'Bear'total Animalsを追加:3
リスナー登録コードのみを変更する場合、ユーザーは任意のリスナーを作成できます。このスケーラビリティは、主に、被験者がコンクリートオブサーバーに直接関連するのではなく、オブザーバーインターフェイスに関連付けられているためです。インターフェイスが変更されていない限り、インターフェイスのサブジェクトを変更する必要はありません。
匿名の内部クラス、ラムダ機能、リスナー登録
Java 8の大幅な改善は、Lambda関数の追加など、機能的特徴の追加です。 Lambda関数を導入する前に、Javaは匿名の内部クラスを通じて同様の機能を提供しました。これは、多くの既存のアプリケーションでまだ使用されています。オブザーバーモードでは、特定のオブザーバークラスを作成することなく、いつでも新しいリスナーを作成できます。たとえば、PrintnameanimalAddedListenerクラスは、匿名の内部クラスを使用してメイン関数に実装できます。特定の実装コードは次のとおりです。
public class main {public static void main(string [] args){//動物園を作成して動物動物園= new Zoo(); //動物が追加されたときに通知されるリスナーを登録するZoo.registeranimalAddedListener(new AnimalAddedListener(){@Override public void upprod updateanimalAdded {//新しく追加された動物システムの名前を印刷します。 //動物の追加登録されたリスナーZoo.Addanimal(新しい動物( "Tiger")); }}同様に、Lambda関数は、そのようなタスクを完了するためにも使用できます。
public class main {public static void main(string [] args){//動物園を作成して動物動物園= new Zoo(); //動物が追加されたときに通知されるリスナー登録//動物の追加登録されたリスナーZoo.Addanimal(新しい動物( "Tiger")); }}Lambda関数は、リスナーインターフェイスに1つの関数のみがある状況にのみ適していることに注意する必要があります。この要件は厳格に思えますが、多くのリスナーは実際には、この例のAnimalAddedListenerなどの単一関数です。インターフェイスに複数の関数がある場合、匿名のインナークラスを使用することを選択できます。
作成されたリスナーの暗黙の登録にはそのような問題があります。オブジェクトは登録コールの範囲内で作成されるため、特定のリスナーへの参照を保存することは不可能です。これは、Rambda関数または匿名の内部クラスを介して登録されたリスナーを取り消すことができないことを意味します。この問題を解決する簡単な方法は、登録されたリスナーへの参照をRegisterAnimalAddedListener関数に返すことです。このようにして、Lambda関数または匿名の内部クラスで作成されたリスナーを登録解除できます。改良されたメソッドコードは次のとおりです。
public AnimalAddedListener RegistAnimalAddedListener(AnimalAddedListenerリスナー){//登録リスナーのリストにリスナーを追加します。リスナーを返します;}再設計された関数インタラクションのクライアントコードは次のとおりです。
public class main {public static void main(string [] args){//動物園を作成して動物動物園= new Zoo(); //動物が追加されたときに通知されるリスナーを登録する動物addedListenerリスナー= zoo.registeranimaladdedlistener((animal) - > system.out.println( "name name '" + animal.getName() + "'")); //動物の追加登録されたリスナーZoo.Addanimal(新しい動物( "Tiger")); //リスナーzoo.unregisteranimaladdedlistener(リスナー)の登録を解除します。 //リスナー//は以前に登録されていなかったため、名前を印刷しない別の動物を追加します。 }}この時点での結果出力は、2番目の動物が追加される前にリスナーがキャンセルされたため、「Tiger」という名前の新しい動物のみが追加されます。
「タイガー」という名前の新しい動物を追加しました
より複雑なソリューションが採用されている場合、レジスタ関数はレシーバークラスを返すことができるため、未登録のリスナーは次のように呼ばれます。
パブリッククラスAnimalAddedListenErreceipt {プライベート最終AntimalAddedListenerリスナー。 public AntimalAddedListenerreceipt(AnimalAddedListenerリスナー){this.listener = listener; } public final AnimalAddedListener getListener(){return this.listener; }}領収書は、登録関数の返品値として使用され、登録関数の入力パラメーターがキャンセルされます。この時点で、動物園の実装は次のとおりです。
Public Class ZoousingReceipt {// ...既存の属性とコンストラクター... public AntimalAddedListenerreceipt RegistanimalAddedListener(AnimalAddedListenerリスナー){//登録リスナーのリストにリスナーにリスナーに追加します。新しい動物AddedListenerreceipt(リスナー)を返します。 } public void unregisteranimaladdedlistener(animaladdedlistenerreceiptレセプション){//登録されたリスナーのリストからリスナーを削除します。 } // ...既存の通知方法...}上記の実装メカニズムを受信すると、リスナーにコールするための情報のストレージが取り消されるとき、つまり、被験者がリスナーを登録するときに取り消し登録アルゴリズムがリスナーのステータスに依存する場合、このステータスは保存されます。取り消し登録が以前の登録リスナーへの参照のみを必要とする場合、レセプションテクノロジーは面倒に見え、推奨されません。
特に複雑な特定のリスナーに加えて、リスナーを登録する最も一般的な方法は、Lambda機能または匿名の内部クラスを使用することです。もちろん、例外があります。つまり、サブジェクトを含むクラスはオブザーバーインターフェイスを実装し、参照ターゲットを呼び出すリスナーを登録します。次のコードに示されているケース:
パブリッククラスのZooContainerは、AnimalAddedListener {private Zoo Zoo = new Zoo(); public zoocontainer(){//このオブジェクトをリスナーとして登録this.zoo.registeranimaladdedlistener(this); } public zoo getzoo(){return this.zoo; } @Override public void updateanimaladded(動物動物){system.out.println( "name '" + animal.getName() + "'"); } public static void main(string [] args){//動物園コンテナzoocontainer zoocontainer = new zoocontainer(); //動物を追加する内側に通知されたリスナーZooContainer.getZoo()。addanimal(new Animal( "Tiger")); }}このアプローチは単純なケースにのみ適しており、コードは十分にプロフェッショナルではないようであり、現代のJava開発者にはまだ非常に人気があるため、この例がどのように機能するかを理解する必要があります。 ZoocontainerはAnimalAddedListenerインターフェイスを実装するため、Zoocontainerのインスタンス(またはオブジェクト)をAnimalAddedListenerとして登録できます。 Zoocontainerクラスでは、このリファレンスは現在のオブジェクトのインスタンス、つまりZoocontainerを表し、AnimalAddedListenerとして使用できます。
一般に、すべてのコンテナクラスがそのような関数を実装するために必要なわけではなく、リスナーインターフェイスを実装するコンテナクラスは、サブジェクト登録関数のみを呼び出すことができますが、リスナーオブジェクトとしてレジスタ関数への参照を渡すだけです。次の章では、マルチスレッド環境のFAQとソリューションが導入されます。
スレッド安全性の実装<br />前の章では、現代のJava環境におけるオブザーバーパターンの実装を紹介します。それは単純ですが、完全ですが、この実装は重要な問題、つまりスレッドの安全性を無視します。ほとんどのオープンJavaアプリケーションはマルチスレッドであり、オブザーバーモードは主にマルチスレッドまたは非同期システムで使用されます。たとえば、外部サービスがデータベースを更新する場合、アプリケーションはメッセージを非同期に受信し、外部サービスを直接登録および聴く代わりに、オブザーバーモードで内部コンポーネントに更新するように通知します。
オブザーバーモードのスレッドの安全性は、主にモードの本体に焦点を当てています。これは、登録されたリスナーコレクションを変更するときにスレッドの競合が発生する可能性が高いためです。たとえば、1つのスレッドは新しいリスナーを追加しようとしますが、もう1つのスレッドは新しい動物オブジェクトを追加しようとします。これにより、登録されたすべてのリスナーに通知がトリガーされます。シーケンスの順序を考えると、最初のスレッドは、登録されたリスナーが追加の動物の通知を受け取る前に、新しいリスナーの登録を完了した場合とそうでない場合があります。これは、スレッドリソース競争の古典的なケースであり、スレッドの安全性を確保するためのメカニズムが必要であることを開発者に伝えるのはこの現象です。
この問題の最も簡単な解決策は、登録リスナーリストにアクセスまたは変更するすべての操作が、次のようなJava同期メカニズムに従う必要があることです。
Public Synchronized AnimalAddedListener RegistanimalAddedListener(AnimalAddedListenerリスナー){/*.....*/} Public Synchronized UnregisteranimalAddedListener(AnimalAddedListenerリスナー){/*...// /*...*/}このようにして、同時に、登録されたリスナーリストを変更またはアクセスできるスレッドのみがリソースの競争の問題を回避できますが、新しい問題が発生し、そのような制約は厳格すぎます(同期されたキーワードとJavaの同意モデルの詳細については、公式のWebページを参照してください)。メソッドの同期により、リスナーリストへの同時アクセスを常に観察できます。リスナーを登録して取り消すことは、リスナーリストの書き込み操作です。リスナーにリスナーリストにアクセスするように通知することは、読み取り専用操作です。通知によるアクセスは読み取り操作であるため、複数の通知操作を同時に実行できます。
したがって、登録されていない限り、登録されているリスナーリストのリソース競争をトリガーすることなく、任意の数の同時通知を同時に実行できる限り、登録が登録されていない限り、リスナーの登録または取り消しがない限り。もちろん、他の状況でのリソース競争は長い間存在してきました。この問題を解決するために、ReadWritelockのリソースロックは、読み取り操作を個別に管理するように設計されています。動物園クラスのスレッドセーフスレッドセーフェズー実装コードは次のとおりです。
public class threadsafezoo {private final readwritelock readwritelock = new reintrantreadwritelock();保護された最終ロックreadlock = readwriteLock.readLock();保護された最終ロックwritelock = readwritelock.writeLock();プライベートリスト<動物>動物= new ArrayList <>();プライベートリスト<AnimalAddedListener>リスナー= new ArrayList <>(); public void addanimal(動物動物){//動物を動物のリストに追加するthis.animals.add(動物); //登録されたリスナーのリストに通知します。 } public AnimalAddedListener RegistAnimalAddedListener(AnimalAddedListenerリスナー){// This.WriteLock.Lock()を書くためにリスナーのリストをロックします。 try {//リスナーを登録リスナーのリストに追加するthis.listeners.add(リスナー); }最後に{//ライターのロック解除this.writeLock.unlock(); }リスナーを返します。 } public void unregisteranimaladdedlistener(AnimalAddedListenerリスナー){//これを書くためにリスナーのリストをロックします。WriteLock.Lock(); try {//登録されたリスナーのリストからリスナーを削除しますthis.listeners.remove(リスナー); }最後に{//ライターのロック解除this.writeLock.unlock(); }} public void notifyAnimaladdedListeners(動物動物){//これを読むためにリスナーのリストをロックします。ReadLock.Lock(); try {//登録されたリスナーのリストにある各リスナーに通知します。 }最後に{//読者のロックを解除しますthis.readlock.unlock(); }}}このような展開を通じて、サブジェクトの実装により、スレッドの安全性と複数のスレッドが同時に通知を発行できるようになります。しかし、それにもかかわらず、無視できない2つのリソース競争の問題がまだあります。
各リスナーへの同時アクセス。複数のスレッドは、リスナーに新しい動物が必要であることを通知できます。つまり、リスナーは複数のスレッドで同時に呼び出される場合があります。
動物リストへの同時アクセス。複数のスレッドは、動物リストにオブジェクトを同時に追加する場合があります。通知の順序が影響を与える場合、リソース競争につながる可能性があります。これには、この問題を回避するために同時の操作処理メカニズムが必要です。登録されたリスナーリストがAnimal2を追加する通知を受け取り、その後、Animal1を追加する通知を受け取ると、リソース競争が発生します。ただし、動物1と動物2の添加が異なるスレッドで実行される場合、動物2の前に動物1の添加を完了することもできます。具体的には、スレッド1はリスナーに通知する前に動物1を追加し、モジュールをロックし、スレッド2は動物2を追加してリスナーに通知し、スレッド1はリスナーにAnimal1が追加されたことを通知します。シーケンスの順序が考慮されない場合、リソース競争は無視できますが、問題は現実です。
リスナーへの同時アクセス
并发访问监听器可以通过保证监听器的线程安全来实现。秉承着类的“责任自负”精神,监听器有“义务”确保自身的线程安全。例如,对于前面计数的监听器,多线程的递增或递减动物数量可能导致线程安全问题,要避免这一问题,动物数的计算必须是原子操作(原子变量或方法同步),具体解决代码如下:
public class ThreadSafeCountingAnimalAddedListener implements AnimalAddedListener { private static AtomicLong animalsAddedCount = new AtomicLong(0); @Override public void updateAnimalAdded (Animal animal) { // Increment the number of animals animalsAddedCount.incrementAndGet(); // Print the number of animals System.out.println("Total animals added: " + animalsAddedCount); }}方法同步解决方案代码如下:
public class CountingAnimalAddedListener implements AnimalAddedListener { private static int animalsAddedCount = 0; @Override public synchronized void updateAnimalAdded (Animal animal) { // Increment the number of animals animalsAddedCount++; // Print the number of animals System.out.println("Total animals added: " + animalsAddedCount); }}要强调的是监听器应该保证自身的线程安全,subject需要理解监听器的内部逻辑,而不是简单确保对监听器的访问和修改的线程安全。否则,如果多个subject共用同一个监听器,那每个subject类都要重写一遍线程安全的代码,显然这样的代码不够简洁,因此需要在监听器类内实现线程安全。
监听器的有序通知当要求监听器有序执行时,读写锁就不能满足需求了,而需要引入一个新的机制,可以保证notify函数的调用顺序和animal添加到zoo的顺序一致。有人尝试过用方法同步来实现,然而根据Oracle文档中的方法同步介绍,可知方法同步并不提供操作执行的顺序管理。它只是保证原子操作,也就是说操作不会被打断,并不能保证先来先执行(FIFO)的线程顺序。ReentrantReadWriteLock可以实现这样的执行顺序,代码如下:
public class OrderedThreadSafeZoo { private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock(true); protected final Lock readLock = readWriteLock.readLock(); protected final Lock writeLock = readWriteLock.writeLock(); private List<Animal> animals = new ArrayList<>(); private List<AnimalAddedListener> listeners = new ArrayList<>(); public void addAnimal (Animal animal) { // Add the animal to the list of animals this.animals.add(animal); // Notify the list of registered listeners this.notifyAnimalAddedListeners(animal); } public AnimalAddedListener registerAnimalAddedListener (AnimalAddedListener listener) { // Lock the list of listeners for writing this.writeLock.lock(); try { // Add the listener to the list of registered listeners this.listeners.add(listener); } finally { // Unlock the writer lock this.writeLock.unlock(); } return listener; } public void unregisterAnimalAddedListener (AnimalAddedListener listener) { // Lock the list of listeners for writing this.writeLock.lock(); try { // Remove the listener from the list of the registered listeners this.listeners.remove(listener); } finally { // Unlock the writer lock this.writeLock.unlock(); } } public void notifyAnimalAddedListeners (Animal animal) { // Lock the list of listeners for reading this.readLock.lock(); try { // Notify each of the listeners in the list of registered listeners this.listeners.forEach(listener -> listener.updateAnimalAdded(animal)); } finally { // Unlock the reader lock this.readLock.unlock(); }}}这样的实现方式,register, unregister和notify函数将按照先进先出(FIFO)的顺序获得读写锁权限。例如,线程1注册一个监听器,线程2在开始执行注册操作后试图通知已注册的监听器,线程3在线程2等待只读锁的时候也试图通知已注册的监听器,采用fair-ordering方式,线程1先完成注册操作,然后线程2可以通知监听器,最后线程3通知监听器。这样保证了action的执行顺序和开始顺序一致。
如果采用方法同步,虽然线程2先排队等待占用资源,线程3仍可能比线程2先获得资源锁,而且不能保证线程2比线程3先通知监听器。问题的关键所在:fair-ordering方式可以保证线程按照申请资源的顺序执行。读写锁的顺序机制很复杂,应参照ReentrantReadWriteLock的官方文档以确保锁的逻辑足够解决问题。
截止目前实现了线程安全,在接下来的章节中将介绍提取主题的逻辑并将其mixin类封装为可重复代码单元的方式优缺点。
主题逻辑封装到Mixin类<br />把上述的观察者模式设计实现封装到目标的mixin类中很具吸引力。通常来说,观察者模式中的观察者包含已注册的监听器的集合;负责注册新的监听器的register函数;负责撤销注册的unregister函数和负责通知监听器的notify函数。对于上述的动物园的例子,zoo类除动物列表是问题所需外,其他所有操作都是为了实现主题的逻辑。
Mixin类的案例如下所示,需要说明的是为使代码更为简洁,此处去掉关于线程安全的代码:
public abstract class ObservableSubjectMixin<ListenerType> { private List<ListenerType> listeners = new ArrayList<>(); public ListenerType registerListener (ListenerType listener) { // Add the listener to the list of registered listeners this.listeners.add(listener); return listener; } public void unregisterAnimalAddedListener (ListenerType listener) { // Remove the listener from the list of the registered listeners this.listeners.remove(listener); } public void notifyListeners (Consumer<? super ListenerType> algorithm) { // Execute some function on each of the listeners this.listeners.forEach(algorithm); }}正因为没有提供正在注册的监听器类型的接口信息,不能直接通知某个特定的监听器,所以正需要保证通知功能的通用性,允许客户端添加一些功能,如接受泛型参数类型的参数匹配,以适用于每个监听器,具体实现代码如下:
public class ZooUsingMixin extends ObservableSubjectMixin<AnimalAddedListener> { private List<Animal> animals = new ArrayList<>(); public void addAnimal (Animal animal) { // Add the animal to the list of animals this.animals.add(animal); // Notify the list of registered listeners this.notifyListeners((listener) -> listener.updateAnimalAdded(animal)); }}Mixin类技术的最大优势是把观察者模式的Subject封装到一个可重复调用的类中,而不是在每个subject类中都重复写这些逻辑。此外,这一方法使得zoo类的实现更为简洁,只需要存储动物信息,而不用再考虑如何存储和通知监听器。
然而,使用mixin类并非只有优点。比如,如果要存储多个类型的监听器怎么办?例如,还需要存储监听器类型AnimalRemovedListener。mixin类是抽象类,Java中不能同时继承多个抽象类,而且mixin类不能改用接口实现,这是因为接口不包含state,而观察者模式中state需要用来保存已经注册的监听器列表。
One solution is to create a listener type ZooListener that will be notified when animals increase and decrease.コードは次のようになります:
public interface ZooListener { public void onAnimalAdded (Animal animal); public void onAnimalRemoved (Animal animal);}这样就可以使用该接口实现利用一个监听器类型对zoo状态各种变化的监听了:
public class ZooUsingMixin extends ObservableSubjectMixin<ZooListener> { private List<Animal> animals = new ArrayList<>(); public void addAnimal (Animal animal) { // Add the animal to the list of animals this.animals.add(animal); // Notify the list of registered listeners this.notifyListeners((listener) -> listener.onAnimalAdded(animal)); } public void removeAnimal (Animal) animal) { // Remove the animal from the list of animals this.animals.remove(animal); // Notify the list of registered listeners this.notifyListeners((listener) -> listener.onAnimalRemoved(animal)); }}将多个监听器类型合并到一个监听器接口中确实解决了上面提到的问题,但仍旧存在不足之处,接下来的章节会详细讨论。
Multi-Method监听器和适配器
在上述方法,监听器的接口中实现的包含太多函数,接口就过于冗长,例如,Swing MouseListener就包含5个必要的函数。尽管可能只会用到其中一个,但是只要用到鼠标点击事件就必须要添加这5个函数,更多可能是用空函数体来实现剩下的函数,这无疑会给代码带来不必要的混乱。
其中一种解决方案是创建适配器(概念来自GoF提出的适配器模式),适配器中以抽象函数的形式实现监听器接口的操作,供具体监听器类继承。这样一来,具体监听器类就可以选择其需要的函数,对adapter不需要的函数采用默认操作即可。例如上面例子中的ZooListener类,创建ZooAdapter(Adapter的命名规则与监听器一致,只需要把类名中的Listener改为Adapter即可),代码如下:
public class ZooAdapter implements ZooListener { @Override public void onAnimalAdded (Animal animal) {} @Override public void onAnimalRemoved (Animal animal) {}}乍一看,这个适配器类微不足道,然而它所带来的便利却是不可小觑的。比如对于下面的具体类,只需选择对其实现有用的函数即可:
public class NamePrinterZooAdapter extends ZooAdapter { @Override public void onAnimalAdded (Animal animal) { // Print the name of the animal that was added System.out.println("Added animal named " + animal.getName()); }}有两种替代方案同样可以实现适配器类的功能:一是使用默认函数;二是把监听器接口和适配器类合并到一个具体类中。默认函数是Java8新提出的,在接口中允许开发者提供默认(防御)的实现方法。
Java库的这一更新主要是方便开发者在不改变老版本代码的情况下,实现程序扩展,因此应该慎用这个方法。部分开发者多次使用后,会感觉这样写的代码不够专业,而又有开发者认为这是Java8的特色,不管怎样,需要明白这个技术提出的初衷是什么,再结合具体问题决定是否要用。使用默认函数实现的ZooListener接口代码如下示:
public interface ZooListener { default public void onAnimalAdded (Animal animal) {} default public void onAnimalRemoved (Animal animal) {}}通过使用默认函数,实现该接口的具体类,无需在接口中实现全部函数,而是选择性实现所需函数。虽然这是接口膨胀问题一个较为简洁的解决方案,开发者在使用时还应多加注意。
第二种方案是简化观察者模式,省略了监听器接口,而是用具体类实现监听器的功能。比如ZooListener接口就变成了下面这样:
public class ZooListener { public void onAnimalAdded (Animal animal) {} public void onAnimalRemoved (Animal animal) {}}这一方案简化了观察者模式的层次结构,但它并非适用于所有情况,因为如果把监听器接口合并到具体类中,具体监听器就不可以实现多个监听接口了。例如,如果AnimalAddedListener和AnimalRemovedListener接口写在同一个具体类中,那么单独一个具体监听器就不可以同时实现这两个接口了。此外,监听器接口的意图比具体类更显而易见,很显然前者就是为其他类提供接口,但后者就并非那么明显了。
如果没有合适的文档说明,开发者并不会知道已经有一个类扮演着接口的角色,实现了其对应的所有函数。此外,类名不包含adapter,因为类并不适配于某一个接口,因此类名并没有特别暗示此意图。综上所述,特定问题需要选择特定的方法,并没有哪个方法是万能的。
在开始下一章前,需要特别提一下,适配器在观察模式中很常见,尤其是在老版本的Java代码中。Swing API正是以适配器为基础实现的,正如很多老应用在Java5和Java6中的观察者模式中所使用的那样。zoo案例中的监听器或许并不需要适配器,但需要了解适配器提出的目的以及其应用,因为我们可以在现有的代码中对其进行使用。下面的章节,将会介绍时间复杂的监听器,该类监听器可能会执行耗时的运算或进行异步调用,不能立即给出返回值。
Complex & Blocking监听器关于观察者模式的一个假设是:执行一个函数时,一系列监听器会被调用,但假定这一过程对调用者而言是完全透明的。例如,客户端代码在Zoo中添加animal时,在返回添加成功之前,并不知道会调用一系列监听器。如果监听器的执行需要时间较长(其时间受监听器的数量、每个监听器执行时间影响),那么客户端代码将会感知这一简单增加动物操作的时间副作用。
本文不能面面俱到的讨论这个话题,下面几条是开发者调用复杂的监听器时应该注意的事项:
监听器启动新线程。新线程启动后,在新线程中执行监听器逻辑的同时,返回监听器函数的处理结果,并运行其他监听器执行。
Subject启动新线程。与传统的线性迭代已注册的监听器列表不同,Subject的notify函数重启一个新的线程,然后在新线程中迭代监听器列表。这样使得notify函数在执行其他监听器操作的同时可以输出其返回值。需要注意的是需要一个线程安全机制来确保监听器列表不会进行并发修改。
队列化监听器调用并采用一组线程执行监听功能。将监听器操作封装在一些函数中并队列化这些函数,而非简单的迭代调用监听器列表。这些监听器存储到队列中后,线程就可以从队列中弹出单个元素并执行其监听逻辑。这类似于生产者-消费者问题,notify过程产生可执行函数队列,然后线程依次从队列中取出并执行这些函数,函数需要存储被创建的时间而非执行的时间供监听器函数调用。例如,监听器被调用时创建的函数,那么该函数就需要存储该时间点,这一功能类似于Java中的如下操作:
public class
如何使用Java8 实现观察者模式?相信通过这篇文章大家都有了大概的了解了吧!