パブリッシュ/サブスクライブモードとも呼ばれるオブザーバーモードは、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への参照を渡します。これにより、特定の観察者の特定の主題への参照が提供され、状態が変更されたときに更新を取得できます。簡単に言えば、特定のオブザーバーは、トピックを更新するように指示され、同時にそのコンストラクターの参照を使用して特定のトピックの状態を取得し、最後にこれらの検索状態オブジェクトを特定のオブザーバーの観測プロパティの下に保存します。このプロセスは、次のシーケンス図に示されています。
古典的なモデルの専門化
オブザーバーモデルは普遍的ですが、多くの専門モデルがありますが、最も一般的なものは次の2つです。
状態オブジェクトにパラメーターを提供し、オブザーバーが呼び出す更新方法に渡されます。クラシックモードでは、観察者が対象状態が変更されたことを通知されると、その更新状態は被験者から直接取得されます。これには、検索された状態へのオブジェクト参照を保存するには、オブザーバーが必要です。これにより、循環参照が形成され、concreteSubjectの参照がオブザーバーリストを指し、concreteobserverの参照は、被験者状態を取得できるconcretesubjectを指します。更新された状態を取得することに加えて、オブザーバーとそれが登録して耳を傾ける主題との間には関係がありません。オブザーバーは、被験者自体ではなく、状態オブジェクトを気にします。つまり、多くの場合、concreteteobserverとconcretesubjectは強制的にリンクされています。それどころか、ConcreTesubjectが更新関数を呼び出すと、状態オブジェクトはConcreteObserverに渡され、2つを関連付ける必要はありません。 ConcreteObserverとState Objectの関連は、オブザーバーと状態の間の依存度を減らします(関連性と依存の違いについては、Martin Fowlerの記事を参照)。
対象の抽象クラスとconcretesubjectをSinglesubjectクラスにマージします。ほとんどの場合、被験者に抽象クラスを使用しても、プログラムの柔軟性とスケーラビリティが向上しないため、この抽象クラスとコンクリートクラスを組み合わせることで、設計が簡素化されます。
これら2つの特殊なモデルが組み合わされた後、簡略化されたクラス図は次のとおりです。
これらの特殊なモデルでは、静的クラス構造が大幅に簡素化され、クラス間の相互作用も簡素化されます。この時点でのシーケンス図は次のとおりです。
専門モードのもう1つの機能は、concreteobserverのメンバー変数観測材の削除です。特定のオブザーバーは、主題の最新の状態を保存する必要がない場合がありますが、ステータスが更新されたときに主題のステータスを監視するだけで済む必要があります。たとえば、オブザーバーがメンバー変数の値を標準出力に更新すると、ConcreteObserverとStateクラスとの関連を削除するObserverStateを削除できます。
より一般的な命名ルール
古典的なモデルと上記のプロフェッショナルモデルでさえ、アタッチ、デタッチ、オブザーバーなどの用語を使用しますが、多くのJavaの実装はレジスタ、Unregister、リスナーなどを含むさまざまな辞書を使用しています。状態オブジェクトの特定の名前は、オブザーバーモードで使用されるシナリオによって異なります。たとえば、リスナーがイベントの発生を聴くシーンのオブザーバーモードでは、登録されたリスナーがイベントが発生したときに通知を受け取ります。現時点でのステータスオブジェクトは、イベント、つまりイベントが発生したかどうかです。
実際のアプリケーションでは、ターゲットの命名には件名が含まれることはめったにありません。たとえば、動物園に関するアプリを作成し、複数のリスナーを登録して動物園クラスを観察し、新しい動物が動物園に入ったときに通知を受け取ります。この場合の目標は動物園クラスです。指定された問題ドメインと一致する用語を維持するために、「主題」という用語は使用されません。つまり、ZooクラスはZooSubjectと呼ばれません。
リスナーの命名の後に、リスナーの接尾辞が続きます。たとえば、新しい動物を監視するために上記のリスナーは、AnimalAddedListenerと名付けられます。同様に、レジスタ、Unregister、Notifyなどの関数の命名は、対応するリスナー名によってしばしば接尾辞が付いています。たとえば、登録、登録、および通知AnimalAddedListenerの関数は、レジスタアニマラデッドリステナー、UngisteranimaladdedListener、およびNotifyAnimalAddedListenersと呼ばれます。 Notify関数は単一のリスナーではなく複数のリスナーを処理するため、Notify Function Name Sが使用されることに注意する必要があります。
この命名方法は長く表示され、通常、被験者は複数のタイプのリスナーを登録します。たとえば、動物園の動物園の例では、動物を監視するための新しいリスナーを登録することに加えて、リスナーを削減するためにリスナーを動物に登録する必要があります。現時点では、2つのレジスタ関数があります。(レジスタアニマラデッドリステナーとレジスタアニマルレモヴェッドリステナー。このようにして、リスナーのタイプはオブザーバーのタイプを示すために修飾子として使用されます。
別の慣用的な構文は、更新の代わりにプレフィックスで使用することです。たとえば、更新関数は、updateanimaladdedの代わりにonanimaladdedという名前です。この状況は、リスナーが動物をリストに追加するなど、シーケンスの通知を取得するとより一般的ですが、動物の名前などの別のデータを更新するために使用されることはめったにありません。
次に、この記事ではJavaの象徴的なルールを使用します。象徴的なルールはシステムの実際の設計と実装を変更しませんが、他の開発者がよく知っている用語を使用することは重要な開発原則であるため、上記のJavaのオブザーバーパターンのシンボリックルールに精通している必要があります。上記の概念は、Java 8環境の簡単な例を使用して以下で説明します。
簡単な例
また、上記の動物園の例でもあります。 Java8のAPIインターフェイスを使用して、シンプルなシステムを実装し、オブザーバーパターンの基本原則を説明します。問題は次のとおりです。
システム動物園を作成し、ユーザーが新しいオブジェクトアニマルを追加する状態を聞いて元に戻し、新しい動物の名前を出力する責任を負う特定のリスナーを作成できるようにします。
オブザーバーパターンの以前の学習によると、このようなアプリケーションを実装するには、4つのクラスを作成する必要があることがわかっています。
動物園のクラス:つまり、動物園にすべての動物を保管し、新しい動物が参加したときに登録されたすべてのリスナーに通知することを担当するパターンのテーマ。
動物クラス:動物のオブジェクトを表します。
AnimalAddedListenerクラス:つまり、オブザーバーインターフェイス。
printnameanimaladdedlistener:特定のオブザーバークラスは、新しく追加された動物の名前を出力する責任があります。
最初に、名前のメンバー変数、コンストラクター、ゲッター、セッターメソッドを含む単純なJavaオブジェクトである動物クラスを作成します。コードは次のとおりです。
public class animal {private string name; 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つのクラスは非常にシンプルなので、詳細に紹介しません。次に、動物園クラスを作成します。
Public Class Zoo {private list <mional> Animals = new ArrayList <>(); Private List <AnimalAddedListener>リスナー= new ArrayList <>(); public void addAnimal(Animal Animal){//動物を動物のリストに追加します。 void registeranimaladdedlistener(animalAddedListenerリスナー){//登録されたリスナーのリストにリスナーをリスナーに追加します。 listensthis.listeners.remove(リスナー);}保護されたvoid notifyAnimaladdedlisteners(動物動物){//登録されたリスナーのリストの各リスナーに通知するristhersthis.listeners.foreach(リスナー - >リスナー。この類推は、前の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を実装します{@overridepublic updateanimaladded(動物動物){//新しく追加された動物の名前の名前を印刷してください(「「名前付き」新しい動物を追加しました」最後に、アプリケーションを駆動する主な関数を実装する必要があります。
public class main {public static void main(string [] args){//動物園を作成して動物zoo = new zoo(); //動物が追加されたときにリスナーに通知を登録します。動物(「タイガー」);}}メイン関数は、動物園オブジェクトを作成し、動物名を出力するリスナーを登録し、登録されたリスナーをトリガーする新しい動物オブジェクトを作成します。最終出力は次のとおりです。
「タイガー」という名前の新しい動物を追加しました
リスナーを追加しました
オブザーバーモードの利点は、リスナーが再確立され、被験者に追加されたときに完全に表示されます。たとえば、動物園で動物の総数を計算するリスナーを追加する場合は、特定のリスナークラスを作成し、動物園クラスに変更せずにZooクラスに登録するだけです。カウントリスナーCountinganimalAddedListenerコードを追加することは次のとおりです。
Public Class CountinganimalAddedListenerはAnimalAddedListener {private static int AnimalsAddedCount = 0; @OverridePublic boid updateAnimalAdded(動物動物){//動物の数を増やす。変更されたメイン関数は次のとおりです。
public class main {public static void main(string [] args){//動物園を作成して動物zoo = new zoo(); //動物が追加されたときに通知される登録リスナーは、new printnameanimaladdeddedlistener()); zoo.registeranimaladedlistener(new countinganimaladdedlistener()); //動物の追加登録されているristeresszoo.addanimal(new Animal( "Tiger")); Zoo.Addanimal(new Animal( "lion")); Zoo.Addanimal(new Animal( "Bear"));}}};出力の結果は次のとおりです。
「タイガー」という名前の新しい動物を追加しました。
リスナー登録コードのみを変更する場合、ユーザーは任意のリスナーを作成できます。このスケーラビリティは、主に、被験者がコンクリートオブサーバーに直接関連するのではなく、オブザーバーインターフェイスに関連付けられているためです。インターフェイスが変更されていない限り、インターフェイスのサブジェクトを変更する必要はありません。
匿名の内部クラス、ラムダ機能、リスナー登録
Java 8の大幅な改善は、Lambda関数の追加など、機能的特徴の追加です。 Lambda関数を導入する前に、Javaは匿名の内部クラスを通じて同様の機能を提供しました。これは、多くの既存のアプリケーションでまだ使用されています。オブザーバーモードでは、特定のオブザーバークラスを作成することなく、いつでも新しいリスナーを作成できます。たとえば、PrintnameanimalAddedListenerクラスは、匿名の内部クラスを使用してメイン関数に実装できます。特定の実装コードは次のとおりです。
Public Class Main {public static void main(string [] args){//動物園を作成する動物zoo = new Zoo(); //動物が追加されたときに登録リスナーに通知されるように通知されます。 Animalsystem.out.println( "name '" + animal.getName() + "'");}}); new Animalを追加しました。同様に、Lambda関数は、そのようなタスクを完了するためにも使用できます。
public class main {public static void main(string [] args){//動物園を保存する動物園を作成してnew Zoo = new Zoo(); //動物が追加されたときに通知を受け取るために登録されたリスナー((((((((((((((((()))))」 listenszoo.addanimal(new Animal( "Tiger"));}}Lambda関数は、リスナーインターフェイスに1つの関数のみがある状況にのみ適していることに注意する必要があります。この要件は厳格に思えますが、多くのリスナーは実際には、この例のAnimalAddedListenerなどの単一関数です。インターフェイスに複数の関数がある場合、匿名のインナークラスを使用することを選択できます。
作成されたリスナーの暗黙の登録にはそのような問題があります。オブジェクトは登録コールの範囲内で作成されるため、特定のリスナーへの参照を保存することは不可能です。これは、Rambda関数または匿名の内部クラスを介して登録されたリスナーを取り消すことができないことを意味します。この問題を解決する簡単な方法は、登録されたリスナーへの参照をRegisterAnimalAddedListener関数に返すことです。このようにして、Lambda関数または匿名の内部クラスで作成されたリスナーを登録解除できます。改良されたメソッドコードは次のとおりです。
public AnimalAddedListener RegistAnimalAddedListener(AnimalAddedListenerリスナー){//登録された聴取者のリストにリスナーを追加します。リスナーを返します;}再設計された関数インタラクションのクライアントコードは次のとおりです。
public class main {public static void main(string [] args){//動物zoo = new zoo = new zoo(); //動物が追加されたときに通知されるように動物zoo = new Zoo()を保存する動物園を作成するZoo.registeranimaladdedlistener((動物) - > system.Outlintln((new and autn with and autnam) - aintln) "'")この時点での結果出力は、2番目の動物が追加される前にリスナーがキャンセルされたため、「Tiger」という名前の新しい動物のみが追加されます。
「タイガー」という名前の新しい動物を追加しました
より複雑なソリューションが採用されている場合、レジスタ関数はレシーバークラスを返すことができるため、未登録のリスナーは次のように呼ばれます。
Public Class AnimalAddedListenerreceipt {プライベートファイナルアニマルアデッドリステナーリスナー; public AntimalAddedListenerreceipt(AnimalAddedListenerリスナー){this.listener =リスナー;} public final animalAddedListener getListener(){return this.listener;}}}領収書は、登録関数の返品値として使用され、登録関数の入力パラメーターがキャンセルされます。この時点で、動物園の実装は次のとおりです。
Public Class ZoousingReceipt {// ...既存の属性とコンストラクター... public AntimalAddedListenerreceipt RegistanimalAddedListener(AnimalAddedListenerリスナー){//登録リスナーのリストにリスナーを追加します。 UngisteranimaladdedListener(AnimalAddedListenerreceiptレセプション){//登録された聴取者のリストからリスナーを削除します。上記の実装メカニズムを受信すると、リスナーにコールするための情報のストレージが取り消されるとき、つまり、被験者がリスナーを登録するときに取り消し登録アルゴリズムがリスナーのステータスに依存する場合、このステータスは保存されます。取り消し登録が以前の登録リスナーへの参照のみを必要とする場合、レセプションテクノロジーは面倒に見え、推奨されません。
特に複雑な特定のリスナーに加えて、リスナーを登録する最も一般的な方法は、Lambda機能または匿名の内部クラスを使用することです。もちろん、例外があります。つまり、サブジェクトを含むクラスはオブザーバーインターフェイスを実装し、参照ターゲットを呼び出すリスナーを登録します。次のコードに示されているケース:
Public AddedListener {Private Zoo Zoo = new Zoo(); public ZooContainer(){//このオブジェクトを聞き取りZoo.zoo.gisteranimaladdedlistener(this);} public zoo getzoo(){return this.zoo;}@ridepublic uideanim uidepublic uidepublic butublic uidepublic getzoo();} {system.out.println( "name '" + animal.getName() + "'");} public static void main(string [] args){//動物園Containerzoocontainer zoocontainer = new zoocontainer();動物(「タイガー」);}}このアプローチは単純なケースにのみ適しており、コードは十分にプロフェッショナルではないようであり、現代のJava開発者にはまだ非常に人気があるため、この例がどのように機能するかを理解する必要があります。 ZoocontainerはAnimalAddedListenerインターフェイスを実装するため、Zoocontainerのインスタンス(またはオブジェクト)をAnimalAddedListenerとして登録できます。 Zoocontainerクラスでは、このリファレンスは現在のオブジェクトのインスタンス、つまりZoocontainerを表し、AnimalAddedListenerとして使用できます。
一般に、すべてのコンテナクラスがそのような関数を実装するために必要なわけではなく、リスナーインターフェイスを実装するコンテナクラスは、サブジェクト登録関数のみを呼び出すことができますが、リスナーオブジェクトとしてレジスタ関数への参照を渡すだけです。次の章では、マルチスレッド環境のFAQとソリューションが導入されます。
ONEAPMは、エンドツーエンドのJavaアプリケーションパフォーマンスソリューションを提供します。すべての一般的なJavaフレームワークとアプリケーションサーバーをサポートして、システムのボトルネックをすばやく発見し、異常の根本原因を見つけるのに役立ちます。瞬間的に展開して即座に展開すると、Javaの監視はかつてないほど容易になりました。その他の技術記事を読むには、Oneapmの公式テクノロジーブログをご覧ください。
上記のコンテンツでは、Java 8を使用してオブザーバーモード(パート1)を実装することに関連するコンテンツを紹介します。次の記事では、Java 8を使用してオブザーバーモード(パート2)を実装する方法を紹介します。興味のある友達は、それがすべての人に役立つことを望んで、学び続けます!