Der Beobachtermodus , auch als Publish/Abonnement-Modus bekannt, wurde von der Vier-Personen-Gruppe (GOF, nämlich Erich Gamma, Richard Helm, Ralph Johnson und John Vlissides) in der Entwurfsmuster von 1994 vorgeschlagen: Die Grundlagen wiederverwendbarer objektorientierter Software "(siehe Seiten 293-313 im Buch für Details). Obwohl dieses Muster eine beträchtliche Geschichte aufweist, ist es immer noch weithin auf eine Vielzahl von Szenarien anwendbar und ist sogar ein wesentlicher Bestandteil der Standard -Java -Bibliothek geworden. Obwohl es bereits viele Artikel über Beobachtermuster gibt, konzentrieren sie sich alle auf die Implementierung in Java, ignorieren jedoch die verschiedenen Probleme, die von Entwicklern auftreten, wenn sie Beobachtermuster in Java verwenden.
Die ursprüngliche Absicht, diesen Artikel zu schreiben, besteht darin, diese Lücke zu schließen: In diesem Artikel wird hauptsächlich die Implementierung des Observer-Musters unter Verwendung der Java8-Architektur eingeführt und komplexe Fragen zu klassischen Mustern auf dieser Basis weiter untersucht, einschließlich anonymer interner Klassen, Lambda-Ausdrücke, Thread-Sicherheit und nicht trivialer Zeitversand. Obwohl der Inhalt dieses Artikels nicht umfassend ist, können viele der komplexen Themen, die an diesem Modell beteiligt sind, nicht in nur einem Artikel erläutert werden. Nach dem Lesen dieses Artikels können die Leser jedoch verstehen, was das Beobachtermuster ist, seine Universalität in Java und wie man mit einigen gemeinsamen Problemen umgeht, wenn sie das Beobachtermuster in Java implementieren.
Beobachtermodus
Gemäß der klassischen Definition von GOF lautet das Thema des Beobachtermusters:
Definiert eine Eins-zu-Viele-Abhängigkeit zwischen Objekten. Wenn sich der Status eines Objekts ändert, werden alle Objekte, die davon abhängen, benachrichtigt und automatisch aktualisiert.
Was bedeutet es? In vielen Softwareanwendungen sind die Zustände zwischen Objekten voneinander abhängig. Wenn sich eine Anwendung beispielsweise auf die numerische Datenverarbeitung konzentriert, können diese Daten über Tabellen oder Diagramme der grafischen Benutzeroberfläche (GUI) angezeigt oder gleichzeitig verwendet werden, dh wenn die zugrunde liegenden Daten aktualisiert werden, müssen die entsprechenden GUI -Komponenten ebenfalls aktualisiert werden. Der Schlüssel zum Problem ist, wie die zugrunde liegenden Daten bei der Aktualisierung der GUI -Komponenten aktualisiert werden und gleichzeitig die Kopplung zwischen den GUI -Komponenten und den zugrunde liegenden Daten minimieren.
Eine einfache und nicht skalierbare Lösung besteht darin, sich auf die Tabellen- und Bild-GUI-Komponenten der Objekte zu beziehen, die diese zugrunde liegenden Daten verwalten, damit die Objekte die GUI-Komponenten bei der Änderung der zugrunde liegenden Daten benachrichtigen können. Offensichtlich zeigte diese einfache Lösung schnell ihre Mängel für komplexe Anwendungen, die mehr GUI -Komponenten verarbeiten. Beispielsweise gibt es 20 GUI -Komponenten, die alle auf zugrunde liegende Daten angewiesen sind. Daher müssen die Objekte, die zugrunde liegende Daten verwalten, Verweise auf diese 20 Komponenten aufrechterhalten. Wenn die Anzahl der von verwandten Daten abhängigen Objekte zunimmt, wird der Grad der Kopplung zwischen Datenverwaltung und Objekten schwer zu kontrollieren.
Eine weitere bessere Lösung besteht darin, Objekte zu registrieren, um Berechtigungen zur Aktualisierung von Daten von Interesse zu erhalten, die der Datenmanager diese Objekte bei der Änderung der Daten benachrichtigt. Lassen Sie das Datenobjekt von Laien den Manager in Laiensgründen sagen: "Bitte benachrichtigen Sie mich, wenn sich die Daten ändert." Darüber hinaus können sich diese Objekte nicht nur registrieren, um Aktualisierungsbenachrichtigungen zu erhalten, sondern auch die Registrierung abzusagen, um sicherzustellen, dass der Datenmanager das Objekt nicht mehr benachrichtigt, wenn sich die Daten ändert. In der ursprünglichen Definition von GOF heißt das Objekt, das als "Beobachter" registriert wird, der entsprechende Datenmanager als "Subjekt" bezeichnet. Wie oben erwähnt, wird der Beobachtermodus auch als Publish-Subscribe-Modus bezeichnet. Es ist zu verstehen, dass ein Kunde den Beobachter über das Ziel abonniert. Wenn der Zielstatus aktualisiert wird, veröffentlicht das Ziel diese Aktualisierungen an den Abonnenten (dieses Entwurfsmuster wird auf eine allgemeine Architektur erweitert, die als Publish-Subscribe-Architektur bezeichnet wird). Diese Konzepte können durch das folgende Klassendiagramm dargestellt werden:
ConcereteObServer verwendet es, um Aktualisierungsstatusänderungen zu erhalten und einen Verweis auf das Conceretesubject an seinen Konstruktor zu übergeben. Dies liefert einen Verweis auf ein bestimmtes Thema für einen bestimmten Beobachter, aus dem Updates bei Änderungen des Zustands erhalten werden können. Einfach ausgedrückt wird der spezifische Beobachter gesagt, er solle das Thema aktualisieren und gleichzeitig die Referenzen in seinem Konstruktor verwenden, um den Zustand des spezifischen Themas zu erhalten, und schließlich diese Suchstatusobjekte unter der Observerstate -Eigenschaft des spezifischen Beobachters zu speichern. Dieser Prozess ist im folgenden Sequenzdiagramm gezeigt:
Spezialisierung klassischer Modelle <BR /> Obwohl das Observer -Modell universell ist, gibt es auch viele spezialisierte Modelle, von denen die häufigsten die folgenden zwei sind:
1. Geben Sie dem Statusobjekt einen Parameter an und geben Sie ihn an die vom Beobachter genannte Aktualisierungsmethode weiter. Im klassischen Modus wird der aktualisierte Zustand direkt vom Subjekt erhalten, wenn der Beobachter mitgeteilt wird, dass sich der Subjektstatus geändert hat. Dies erfordert, dass der Beobachter einen Objektverweis auf den abgerufenen Zustand speichert. Dies bildet eine kreisförmige Referenz, die Referenz des Betonsubjekts zeigt auf seine Beobachterliste und die Referenz von Betonobserver auf das Konkretesubjekt, der den Subjektzustand erhalten kann. Zusätzlich zur Erlangung des aktualisierten Zustands besteht keine Verbindung zwischen dem Beobachter und dem von ihm registrierten Subjekt. Der Beobachter kümmert sich um das Staatsobjekt, nicht um das Thema selbst. Das heißt, in vielen Fällen sind konkretobserver und konkretesubjekts gewaltsam miteinander verbunden. Im Gegenteil, wenn ConcreteSubject die Aktualisierungsfunktion aufruft, wird das Statusobjekt an Betonobserver übergeben und die beiden müssen nicht zugeordnet werden. Der Zusammenhang zwischen konkreterobserver und staatlichem Objekt verringert den Abhängigkeitsgrad zwischen Beobachter und Staat (siehe Martin Fowlers Artikel, um mehr Unterschiede in Bezug auf Assoziation und Abhängigkeit zu erhalten).
2. Zusammenführen der Subjekt abstrakte Klasse und das Konkretesubjekt in eine SingleSubject -Klasse. In den meisten Fällen verbessert die Verwendung abstrakter Klassen im Subjekt die Programmflexibilität und Skalierbarkeit nicht. Daher vereinfacht die Kombination dieser abstrakten Klasse und der konkreten Klasse das Design.
Nachdem diese beiden speziellen Modelle kombiniert wurden, lautet das vereinfachte Klassendiagramm wie folgt:
In diesen speziellen Modellen ist die statische Klassenstruktur stark vereinfacht und die Wechselwirkungen zwischen Klassen werden ebenfalls vereinfacht. Das Sequenzdiagramm zu diesem Zeitpunkt lautet wie folgt:
Ein weiteres Merkmal des Spezialisierungsmodus ist die Entfernung des Beobachtungsvariablen von Mitgliedsbeton von Betonobserver. Manchmal muss der spezifische Beobachter nicht den neuesten Status des Motivs speichern, sondern nur den Status des Subjekts überwachen, wenn der Status aktualisiert wird. Wenn der Beobachter beispielsweise den Wert der Mitgliedsvariablen für die Standardausgabe aktualisiert, kann er den Beobachterstate löschen, wodurch die Assoziation zwischen dem konkretenobserver und der staatlichen Klasse entfernt wird.
Häufigere Namensregeln <BR /> klassische Modi und sogar der oben erwähnte professionelle Modus verwenden Begriffe wie Anhang, Ablösung und Beobachter, während viele Java -Implementierungen verschiedene Wörterbücher verwenden, einschließlich Register, Unregister, Hörer usw. Es ist erwähnenswert, dass der Status ein allgemeiner Begriff für alle Objekte ist, die Hörer benötigt, um Änderungen zu überwachen. Der spezifische Name des Statusobjekts hängt von dem im Beobachtermodus verwendeten Szenario ab. Im Beobachtermodus in der Szene, in der der Hörer das Ereignisvorkommen hört, erhält der registrierte Hörer bei der Ereignis eine Benachrichtigung, wenn das Ereignis auftritt. Das Statusobjekt ist zu diesem Zeitpunkt das Ereignis, das heißt, ob das Ereignis aufgetreten ist.
In den tatsächlichen Anwendungen umfasst die Benennung von Zielen selten ein Thema. Erstellen Sie beispielsweise eine App über einen Zoo, registrieren Sie mehrere Hörer, um die Zoo -Klasse zu beobachten, und erhalten Sie Benachrichtigungen, wenn neue Tiere in den Zoo eintreten. Das Ziel in diesem Fall ist die Zooklasse. Um die Terminologie im Einklang mit der angegebenen Problemdomäne zu halten, wird der Begriff "Subjekt" nicht verwendet, was bedeutet, dass die Zoo -Klasse nicht als Zoosubjekt bezeichnet wird.
Auf die Benennung des Hörers folgt im Allgemeinen das Hörer -Suffix. Zum Beispiel wird der oben erwähnte Hörer, der neue Tiere überwacht, als AnimalDedListener bezeichnet. In ähnlicher Weise werden die Benennung von Funktionen wie Register, Unregister und Benachrichtigung häufig von ihren entsprechenden Hörernamen satt. Beispielsweise werden das Register-, Unregister- und Benachrichtigungsfunktionen von Animedded Listener als RegisteranimaladdedListener, unregisteranimaladdededlistener und notifyAraNaladdededListener ausgezeichnet. Es ist zu beachten, dass der Benachrichtigungsfunktionsname S verwendet wird, da die Benachrichtigungsfunktion mehrere Hörer und eher einen einzelnen Hörer behandelt.
Diese Benennungsmethode erscheint langwierig, und normalerweise registriert ein Thema mehrere Arten von Hörern. In dem oben genannten Beispiel für Zoo im Zoo müssen beispielsweise neben der Registrierung neuer Zuhörer auch die Hörer an Tieren registriert werden, um die Zuhörer zu reduzieren. Zu diesem Zeitpunkt wird es zwei Registerfunktionen geben: (RegisteranimaladdedListener und RegisteranimalRemovedListener. Auf diese Weise wird der Typ des Hörers als Qualifikationsmerkmal verwendet, um die Art von Beobachter anzuzeigen. Eine andere Lösung besteht darin, eine RegisterListener -Funktion zu erstellen und dann zu überlasten, aber Lösung 1 kann mehr wissen, welcher Hörer -Hörer.
Eine andere idiomatische Syntax ist die Verwendung im Präfix anstelle von Update, beispielsweise die Aktualisierungsfunktion, die als Onanimaladded anstelle von Updateanimaladded bezeichnet wird. Diese Situation ist häufiger, wenn der Hörer Benachrichtigungen für eine Sequenz erhält, z. B. ein Tier zur Liste, aber es wird selten verwendet, um separate Daten wie den Namen des Tieres zu aktualisieren.
Als nächstes wird dieser Artikel Javas symbolische Regeln verwenden. Obwohl symbolische Regeln das tatsächliche Design und die Implementierung des Systems nicht ändern, ist es ein wichtiges Entwicklungsprinzip, Begriffe zu verwenden, mit denen andere Entwickler vertraut sind. Das obige Konzept wird unten unter Verwendung eines einfachen Beispiels in der Java 8 -Umgebung erläutert.
Ein einfaches Beispiel
Es ist auch das Beispiel des oben erwähnten Zoo. Verwenden der API -Schnittstelle von Java8 zur Implementierung eines einfachen Systems, wodurch die Grundprinzipien des Observer -Musters erläutert werden. Das Problem wird beschrieben als:
Erstellen Sie einen Systemzoo, sodass Benutzer das Hinzufügen eines neuen Objekttiers anhören und rückgängig machen können, und erstellen Sie einen bestimmten Hörer, der für die Ausgabe des Namens des neuen Tieres verantwortlich ist.
Nach dem vorherigen Erlernen des Beobachtermusters wissen wir, dass wir, um eine solche Anwendung zu implementieren, 4 Klassen erstellen müssen, insbesondere:
Zuerst erstellen wir eine Tierklasse, ein einfaches Java -Objekt, das Namensmitgliedsvariablen, Konstrukteure, Getter und Setter -Methoden enthält. Der Code ist wie folgt:
public Class Animal {privater String -Name; public Animal (String Name) {this.name = name; } public String getName () {return this.name; } public void setName (String -Name) {this.name = name; }}Verwenden Sie diese Klasse, um Tierobjekte darzustellen. Anschließend können Sie die Schnittstelle für AnimalAdded Listener erstellen:
public interface AnimalDdedListener {public void onanimaladded (Tiertier);}Die ersten beiden Klassen sind sehr einfach, daher werde ich sie nicht im Detail vorstellen. Erstellen Sie als nächstes die Zooklasse:
public class zoo {private list <timal> tiere = new ArrayList <> (); private list <TimerAddedListener> listener = new ArrayList <> (); public void addanimal (Tiertier) {// das Tier in die Liste der Tiere hinzufügen. Animals.add (Tier); // die Liste der registrierten Hörer benachrichtigen. } public void RegisteranimalAddedListener (AnimalAddedListener -Listener) {// Fügen Sie den Hörer der Liste der registrierten Hörer this.Listeners.add (Listener) hinzu; } public void unregisteranimaladdedListener (AnimaladdedListener -Listener) {// Entfernen Sie den Hörer aus der Liste der registrierten Hörer this.listeners.remove (Listener); } Protected void musifyAnimaladdedListener (Tiertier) {// Benachrichtigen Sie jeden der Hörer in der Liste der registrierten Hörer -Hörer this.Listeners.foreach (Hörer -> louseer.updateanimaladded (Tier)); }}Diese Analogie ist komplex als die beiden vorherigen. Es enthält zwei Listen, einer wird verwendet, um alle Tiere im Zoo zu speichern, und der andere wird verwendet, um alle Hörer zu speichern. Angesichts der Tatsache, dass die in Tieren und Hörersammlungen gespeicherten Objekte einfach sind, hat dieser Artikel die ArrayList für die Speicherung ausgewählt. Die spezifische Datenstruktur des gespeicherten Hörers hängt vom Problem ab. Zum Beispiel für das Zoo -Problem hier sollten Sie hier, wenn der Hörer Priorität hat, eine andere Datenstruktur auswählen oder den Registeralgorithmus des Hörers umschreiben.
Die Implementierung von Registrierung und Entfernung ist sowohl eine einfache Delegiermethode: Jeder Hörer wird als Parameter aus der Hörliste des Hörers hinzugefügt oder entfernt. Die Implementierung der Benachrichtigungsfunktion erfolgt geringfügig vom Standardformat des Observer -Musters. Es enthält den Eingabeparameter: das neu hinzugefügte Tier, damit die Benachrichtigungsfunktion den neu hinzugefügten Tierverweis an den Hörer übergeben kann. Verwenden Sie die Foreach -Funktion der Streams -API, um die Hörer zu durchqueren und die Theonanimaladded -Funktion für jeden Hörer auszuführen.
In der Addanimal -Funktion werden das neu hinzugefügte Tierobjekt und der Hörer der entsprechenden Liste hinzugefügt. Wenn die Komplexität des Benachrichtigungsprozesses nicht berücksichtigt wird, sollte diese Logik in eine bequeme Anrufmethode enthalten sein. Sie müssen nur in einem Verweis auf das neu hinzugefügte Tierobjekt weitergeben. Aus diesem Grund wird die logische Implementierung des Benachrichtigungshörers in der Funktion "NotifyAnimalAdded Listeners" eingekapselt, die auch in der Implementierung von Addanimal erwähnt wird.
Zusätzlich zu den logischen Fragen der Benachrichtigungsfunktionen ist es erforderlich, das kontroverse Problem bei der Sichtbarkeit von Benachrichtigungsfunktionen zu betonen. In dem klassischen Beobachtermodell ist die Benachrichtigungsfunktion, wie GoF auf Seite 301 der Buchdesignmuster sagte, öffentlich, obwohl dies im klassischen Muster verwendet wird, bedeutet dies nicht, dass es öffentlich sein muss. Die Auswahl der Sichtbarkeit sollte auf der Anwendung basieren. Beispielsweise ist im Zoo -Beispiel dieses Artikels die Benachrichtigungsfunktion vom Typ geschützt und verlangt nicht, dass jedes Objekt eine Benachrichtigung über einen registrierten Beobachter einleitet. Es muss nur sicherstellen, dass das Objekt die Funktion von der übergeordneten Klasse erben kann. Dies ist natürlich nicht genau der Fall. Es ist erforderlich, herauszufinden, welche Klassen die Benachrichtigungsfunktion aktivieren und dann die Sichtbarkeit der Funktion bestimmen können.
Als nächstes müssen Sie die PrintNameAnimaladdedeList -Listener -Klasse implementieren. Diese Klasse verwendet die Methode von System.out.println, um den Namen des neuen Tieres auszugeben. Der spezifische Code lautet wie folgt:
öffentliche Klasse PrintNameAnimalAddedeDlistener implementiert AnimalDdededListener {@Override public void updateanimaladded (Tiertiere) {// Drucken Sie den Namen des neu hinzugefügten Tiersystems. }}Schließlich müssen wir die Hauptfunktion implementieren, die die Anwendung antreibt:
public class main {public static void main (String [] args) {// Erstellen Sie den Zoo zum Speichern von Tieren Zoo Zoo = neuer Zoo (); // Registrieren Sie einen Hörer, der benachrichtigt wird, wenn ein Tier Zoo hinzugefügt wird. // ein Tier hinzufügen, benachrichtigt den registrierten Hörern Zoo.addanimal (neues Tier ("Tiger"); }}Die Hauptfunktion erstellt einfach ein Zooobjekt, registriert einen Hörer, der den Tiernamen ausgibt, und erstellt ein neues Tierobjekt, um den registrierten Hörer auszulösen. Die endgültige Ausgabe ist:
Fügte ein neues Tier mit Namen 'Tiger' hinzu
Hörer hinzugefügt
Die Vorteile des Beobachtermodus werden vollständig angezeigt, wenn der Hörer wiederhergestellt und dem Subjekt hinzugefügt wird. Wenn Sie beispielsweise einen Hörer hinzufügen möchten, der die Gesamtzahl der Tiere in einem Zoo berechnet, müssen Sie nur eine bestimmte Hörerklasse erstellen und sie bei der Zoo -Klasse registrieren, ohne dass die Zoo -Klasse geändert wird. Das Hinzufügen des Counting -Listener -CountinganimaladdedListener -Code ist wie folgt:
öffentliche Klasse countingAnimalAddedListener implementiert AnimalDdedListener {private static int AnimalsaddedCount = 0; @Override public void updateanimaladded (tierisches Tier) {// Inkrementieren Sie die Anzahl der Tiere, die AnimalsaddedCount ++; // Drucken Sie die Anzahl der Tiere system.out.println ("Gesamttiere hinzugefügt:" + tiereaddedCount); }}Die modifizierte Hauptfunktion lautet wie folgt:
public class main {public static void main (String [] args) {// Erstellen Sie den Zoo zum Speichern von Tieren Zoo Zoo = neuer Zoo (); // Hörer registrieren, um benachrichtigt zu werden, wenn ein Tier Zoo hinzugefügt wird. Zoo.registerArAddededListener (neuer countingAnimaladdededListener ()); // ein Tier hinzufügen, benachrichtigt den registrierten Hörern Zoo.addanimal (neues Tier ("Tiger"); zoo.addanimal (neues Tier ("Löwe"); zoo.addanimal (neues Tier ("Bär"); }}Das Ausgangsergebnis ist:
Ein neues Tier mit Namen 'Tiger'total Animals hinzugefügt: 1adiert ein neues Tier mit Namen' Lion'total Animals hinzugefügt: 2adiert ein neues Tier mit Namen 'Bear'total Animals hinzugefügt: 3
Der Benutzer kann einen beliebigen Listener erstellen, wenn nur der Listener -Registrierungscode geändert wird. Diese Skalierbarkeit liegt hauptsächlich daran, dass das Subjekt mit der Observer -Schnittstelle verbunden ist und nicht direkt mit dem Betonobserver verbunden ist. Solange die Schnittstelle nicht geändert wird, müssen das Betreff der Schnittstelle nicht geändert werden.
Anonyme interne Klassen, Lambda -Funktionen und Hörerregistrierung
Eine wesentliche Verbesserung bei Java 8 ist die Zugabe von funktionalen Merkmalen wie die Zugabe von Lambda -Funktionen. Vor der Einführung der Lambda -Funktion lieferte Java ähnliche Funktionen über anonyme interne Klassen, die in vielen vorhandenen Anwendungen weiterhin verwendet werden. Im Observer -Modus kann ein neuer Hörer jederzeit erstellt werden, ohne eine bestimmte Beobachterklasse zu erstellen. Beispielsweise kann die PrintNameAnimaladdededListener -Klasse in der Hauptfunktion mit anonymer interner Klasse implementiert werden. Der spezifische Implementierungscode lautet wie folgt:
public class main {public static void main (String [] args) {// Erstellen Sie den Zoo zum Speichern von Tieren Zoo Zoo = neuer Zoo (); // Hörer registrieren, um benachrichtigt zu werden, wenn ein Tier hinzugefügt wird. // ein Tier hinzufügen, benachrichtigt den registrierten Hörern Zoo.addanimal (neues Tier ("Tiger"); }}In ähnlicher Weise können Lambda -Funktionen auch verwendet werden, um solche Aufgaben zu erledigen:
public class main {public static void main (String [] args) {// Erstellen Sie den Zoo zum Speichern von Tieren Zoo Zoo = neuer Zoo (); // Hörer registrieren, um benachrichtigt zu werden, wenn ein Tier Zoo hinzugefügt wird. // ein Tier hinzufügen, benachrichtigt den registrierten Hörern Zoo.addanimal (neues Tier ("Tiger"); }}Es ist zu beachten, dass die Lambda -Funktion nur für Situationen geeignet ist, in denen nur eine Funktion in der Hörerschnittstelle vorhanden ist. Obwohl diese Anforderung streng erscheint, sind viele Hörer tatsächlich einzelne Funktionen, wie der AnimalDded Listener im Beispiel. Wenn die Schnittstelle mehrere Funktionen hat, können Sie anonyme innere Klassen verwenden.
Es gibt ein solches Problem bei der impliziten Registrierung des erstellten Hörers: Da das Objekt im Rahmen des Registrierungsanrufs erstellt wird, ist es unmöglich, einen Verweis auf einen bestimmten Hörer zu speichern. Dies bedeutet, dass Hörer, die über Lambda -Funktionen oder anonyme interne Klassen registriert sind, nicht widerrufen werden können, da Widerrufsfunktionen einen Verweis auf den registrierten Hörer erfordern. Eine einfache Möglichkeit, dieses Problem zu lösen, besteht darin, einen Verweis auf den registrierten Hörer in der Funktion "RegisteranimalAdded Listener" zurückzugeben. Auf diese Weise können Sie den mit Lambda -Funktionen erstellten Hörer oder anonymen internen Klassen nicht registrieren. Der verbesserte Methodencode lautet wie folgt:
public AnimeddededListener RegisteranimaladdedListener (AnimalAddedListener -Listener) {// Fügen Sie den Hörer der Liste der registrierten Listener this.Listeners.add (Listener) hinzu; Hörer zurückgeben;}Der Client -Code für die neu gestaltete Funktionsinteraktion lautet wie folgt:
public class main {public static void main (String [] args) {// Erstellen Sie den Zoo zum Speichern von Tieren Zoo Zoo = neuer Zoo (); // Hörer registrieren, um benachrichtigt zu werden, wenn ein Tier hinzugefügt wird, AnimaladdededListener Listener = Zoo.registerArAddededListener ((Animal) -> System.out.println ("Ein neues Tier mit Namen" + Animal.GetName () + "'"); // ein Tier hinzufügen, benachrichtigt den registrierten Hörern Zoo.addanimal (neues Tier ("Tiger"); // Registrieren Sie den Hörer Zoo. // fügen Sie ein anderes Tier hinzu, das den Namen nicht druckt, da der Hörer // zuvor nicht registriert wurde .addanimal (neues Tier ("Löwe"); }}Das Ergebnisausgang zu diesem Zeitpunkt wird nur ein neues Tier mit dem Namen 'Tiger' hinzugefügt, da der Hörer abgesagt wurde, bevor das zweite Tier hinzugefügt wird:
Fügte ein neues Tier mit Namen 'Tiger' hinzu
Wenn eine komplexere Lösung übernommen wird, kann die Registerfunktion auch die Empfängerklasse zurückgeben, sodass der nicht register aufgerufene Hörer aufgerufen wird, zum Beispiel:
public class AnimalDdedeNeRreceipt {private letztes AnimalAddedListener -Hörer; public AnimalDdedeIneReceipt (AnimalAddedListener -Listener) {this.Listener = Listener; } public Final AnimalDdedListener getListener () {return this.Listener; }}Der Quittung wird als Rückgabewert der Registrierungsfunktion verwendet und die Eingabeparameter der Registrierungsfunktion werden storniert. Zu diesem Zeitpunkt lautet die Zoo -Implementierung wie folgt:
öffentliche Klasse Zoousingreceipt {// ... vorhandene Attribute und Konstruktor ... public AnimalDedListenerreceipt RegisterImalAddededener (AnimalAddedListener -Listener) {// den Hörer der Liste der registrierten Hörer thisListeners.add (Hörer) hinzufügen; Neue AnimalDdedeLeReceipt (Hörer) zurückgeben; } public void unregisteranimaladdedListener (AnimalAddedeLeReReceipt -Empfang) {// Entfernen Sie den Hörer aus der Liste der registrierten Hörer this.Listeners.remove (quittung.getListener ()); } // ... Bestehende Benachrichtigungsmethode ...}Der oben beschriebene implementierende Implementierungsmechanismus ermöglicht die Speicherung von Informationen für den Anruf an den Hörer beim Widerruf, dh wenn der Widerruf -Registrierungsalgorithmus vom Status des Hörers abhängt, wenn das Betreff den Hörer registriert, wird dieser Status gespeichert. Wenn für die Widerrufsregistrierung nur einen Hinweis auf den vorherigen registrierten Hörer erforderlich ist, erscheint die Empfangstechnologie problematisch und wird nicht empfohlen.
Zusätzlich zu den besonders komplexen spezifischen Zuhörern ist die häufigste Möglichkeit, die Hörer zu registrieren, über Lambda -Funktionen oder über anonyme interne Klassen. Natürlich gibt es Ausnahmen, dh die Klasse, die das Subjekt enthält, implementiert die Observer -Schnittstelle und registriert einen Hörer, der das Referenzziel aufruft. Der Fall wie im folgenden Code gezeigt:
Public Class ZooContainer implementiert AnimalDdedListener {private zoo zoo = new zoo (); public zoocontainer () {// Registrieren Sie dieses Objekt als Hörer This } public Zoo GOtzoo () {return this.zoo; } @Override public void updateanimaladded (Animal Animal) {System.out.println ("Animal mit Namen hinzugefügt" " + Animal.getName () +" '"); } public static void main (String [] args) {// Erstellen Sie den Zoo -Container Zoocontainer ZooContainer = neuer ZooContainer (); // ein Tier hinzufügen, benachrichtigen Sie den inneren benachrichtigten Hörer Zoocontainer.getZoo (). Addanimal (neues Tier ("Tiger")); }}Dieser Ansatz ist nur für einfache Fälle geeignet und der Code scheint nicht professionell genug zu sein, und er ist bei modernen Java -Entwicklern immer noch sehr beliebt. Daher ist es notwendig zu verstehen, wie dieses Beispiel funktioniert. Da Zoocontainer die Schnittstelle für AnimalAdded Listener implementiert, kann eine Instanz (oder ein Objekt) von Zoocontainer als AnimalAddedListener registriert werden. In der Zoocontainer -Klasse stellt diese Referenz eine Instanz des aktuellen Objekts dar, nämlich Zookontainer und kann als AnimalAdded -Listener verwendet werden.
Im Allgemeinen sind nicht alle Containerklassen erforderlich, um solche Funktionen zu implementieren, und die Containerklasse, die die Listener -Schnittstelle implementiert, kann die Subjektregistrierungsfunktion nur aufrufen, sondern lediglich die Verweise auf die Registerfunktion als Hörerobjekt übergeben. In den folgenden Kapiteln werden FAQs und Lösungen für Multithread -Umgebungen eingeführt.
Implementierung von Thread Safety <BR /> Im vorherigen Kapitel wird die Implementierung des Observer -Musters in der modernen Java -Umgebung eingeführt. Obwohl es einfach, aber vollständig ist, ignoriert diese Implementierung ein Schlüsselproblem: die Sicherheit von Thread. Die meisten offenen Java-Anwendungen sind multi-threads, und der Beobachtermodus wird hauptsächlich in Multi-Thread-oder asynchronen Systemen verwendet. Wenn ein externer Dienst beispielsweise ihre Datenbank aktualisiert, empfängt die Anwendung auch eine Nachricht asynchron und benachrichtigt die interne Komponente, um im Observer -Modus zu aktualisieren, anstatt direkt zu registrieren und den externen Dienst zu hören.
Die Gewindesicherheit im Beobachtermodus konzentriert sich hauptsächlich auf den Körper des Modus, da bei der Änderung der registrierten Hörersammlung wahrscheinlich Fadenkonflikte auftreten. Beispielsweise versucht ein Thread, einen neuen Hörer hinzuzufügen, während der andere Thread versucht, ein neues Tierobjekt hinzuzufügen, das allen registrierten Hörern Benachrichtigungen auslöst. Angesichts der Reihenfolge der Sequenz kann der erste Thread die Registrierung des neuen Hörers abgeschlossen haben, bevor der registrierte Hörer die Benachrichtigung über das zusätzliche Tier erhält. Dies ist ein klassischer Fall eines Thread -Ressourcenwettbewerbs, und es ist dieses Phänomen, das den Entwicklern sagt, dass sie einen Mechanismus benötigen, um die Sicherheit der Faden zu gewährleisten.
Die einfachste Lösung für dieses Problem ist: Alle Vorgänge, die auf die Liste der Registrierungshörer zugreifen oder diese ändern, müssen dem Java -Synchronisationsmechanismus folgen, wie z. B.:
public synchronisierte AnimalAddededListener Registeranimaladdededlistener (AnimalAdded Listener) {/*...*/} public synchronisierte void unregisteranimaladdededener (AnimalAddededListener -Hörer) {/*...*/} Synchronized Void NoiDedanimaladdededlagener (Animal) { /}Gleichzeitig kann nur ein Thread die Liste der registrierten Hörer ändern oder zugreifen, wodurch Probleme mit dem Ressourcenwettbewerb erfolgreich vermieden werden können. Es treten jedoch neue Probleme auf, und solche Einschränkungen sind zu streng (weitere Informationen zu synchronisierten Schlüsselwörtern und Java -Parallelitätsmodellen finden Sie in der offiziellen Webseite). Durch die Methodensynchronisation kann jeder gleichzeitige Zugriff auf die Listener -Liste beobachtet werden. Das Registrieren und Widerruf des Hörers ist ein Schreibvorgang für die Listener-Liste, während der Hörer, auf die Listenerliste zuzugreifen, zu benachrichtigen, dass es sich um eine schreibgeschützte Operation handelt. Da der Zugriff durch Benachrichtigung ein Lesevorgang ist, können gleichzeitig mehrere Benachrichtigungsvorgänge durchgeführt werden.
Solange keine Registrierung oder Widerruf des Hörers vorliegt, solange die Registrierung nicht registriert ist, kann eine beliebige Anzahl von gleichzeitigen Benachrichtigungen gleichzeitig ausgeführt werden, ohne den Ressourcenwettbewerb für die registrierte Listenerliste auszulösen. Natürlich gibt es seit langem Ressourcenwettbewerb in anderen Situationen. Um dieses Problem zu lösen, ist das Ressourcenverriegelungen für ReadWriteLock so konzipiert, dass sie Lese- und Schreibvorgänge separat verwalten. Der Thread-Safe-ThreadSafezoo-Implementierungscode der Zooklasse lautet wie folgt:
public class threadSafezoo {private endgültige ReadWriteLock ReadWriteLock = New ReentranTreadWriteLock (); geschütztes landesschloss readlock = readWriteLock.readlock (); geschütztes letztes lock WriteLock = ReadWriteLock.WriteLock (); private list <timal> tiere = new ArrayList <> (); private list <TimerAddedListener> listener = new ArrayList <> (); public void addanimal (Tiertier) {// das Tier in die Liste der Tiere hinzufügen. Animals.add (Tier); // die Liste der registrierten Hörer benachrichtigen. } public AnimeddededListener RegisteranimaladdedListener (AnimalDdededListener -Listener) {// Sperren Sie die Liste der Hörer für das Schreiben dieses.WriteLock.lock (); Versuchen Sie es mit {// den Hörer der Liste der registrierten Hörer this.listeners.add (Listener) hinzuzufügen; } endlich {// den Schriftsteller entsperren. } return louser; } public void unregisteranimaladdedListener (AnimalAdded Listener) {// Sperren Sie die Liste der Hörer, um dies zu schreiben.WriteLock.lock (); Versuchen Sie es mit {// den Hörer aus der Liste der registrierten Hörer This.Listeners.remove (Listener); } endlich {// den Schriftsteller entsperren. }} public void mellifyAnimalAddedListener (Tiertier) {// Sperren Sie die Liste der Hörer für das Lesen dieses.readlocks.lock (); versuche {// jeden der Hörer in der Liste der registrierten Hörer this.Listeners.foreach (Listener -> Listener.UpDateAnimalAdded (Tier)) benachrichtigen; } endlich {// den Leser entsperren.readlock.unlock (); }}}Durch eine solche Bereitstellung kann die Implementierung des Subjekts sicherstellen, dass die Sicherheit der Threads und mehrere Threads gleichzeitig Benachrichtigungen ausstellen kann. Trotzdem gibt es immer noch zwei Probleme mit dem Ressourcenwettbewerb, die nicht ignoriert werden können:
Gleichzeitiger Zugriff auf jeden Hörer. Mehrere Themen können den Hörer darüber informieren, dass neue Tiere benötigt werden, was bedeutet, dass ein Hörer gleichzeitig von mehreren Threads aufgerufen werden kann.
Gleichzeitiger Zugriff auf die Tierliste. Mehrere Threads können gleichzeitig Objekte zur Tierliste hinzufügen. Wenn die Reihenfolge der Benachrichtigungen Auswirkungen hat, kann dies zu einem Ressourcenwettbewerb führen, der einen gleichzeitigen Betriebsbearbeitungsmechanismus erfordert, um dieses Problem zu vermeiden. Wenn die Liste der registrierten Hörer eine Benachrichtigung erhält, um Animal2 hinzuzufügen und dann eine Benachrichtigung zum Hinzufügen von Animal1 erhält, erfolgt der Ressourcenwettbewerb. Wenn jedoch die Zugabe von Animal1 und Animal2 von verschiedenen Fäden durchgeführt wird, ist es auch möglich, die Zugabe von Animal1 vor Animal2 zu vervollständigen. Insbesondere fügt Thread 1 Animal1 hinzu, bevor er den Hörer benachrichtigt und das Modul sperrt, Faden 2 Animal2 hinzufügt und den Hörer benachrichtigt. Dann benachrichtigt Faden 1 den Hörer, dass Animal1 hinzugefügt wurde. Obwohl der Ressourcenwettbewerb ignoriert werden kann, wenn die Reihenfolge der Sequenz nicht berücksichtigt wird, ist das Problem real.
对监听器的并发访问
并发访问监听器可以通过保证监听器的线程安全来实现。秉承着类的“责任自负”精神,监听器有“义务”确保自身的线程安全。例如,对于前面计数的监听器,多线程的递增或递减动物数量可能导致线程安全问题,要避免这一问题,动物数的计算必须是原子操作(原子变量或方法同步),具体解决代码如下:
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需要用来保存已经注册的监听器列表。
其中的一个解决方案是创建一个动物增加和减少时都会通知的监听器类型ZooListener,代码如下所示:
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 实现观察者模式?相信通过这篇文章大家都有了大概的了解了吧!