Publish/Subscribe Mode라고도하는 관찰자 모드는 1994 년 "디자인 패턴 : 재사용 가능한 객체 중심 소프트웨어의 기본 사항"(세부 사항에 대한 293-313 페이지 참조)에서 4 인 그룹 (GOF, Erich Gamma, Ralph Johnm, Ralph Johnson 및 John Vlissides)에서 제안했습니다. 이 패턴은 상당한 역사를 가지고 있지만 여전히 다양한 시나리오에 널리 적용 가능하며 표준 Java 라이브러리의 필수 부분이되었습니다. 관찰자 패턴에 대한 많은 기사가 있지만 모두 Java의 구현에 중점을 두지 만 Java에서 관찰자 패턴을 사용할 때 개발자가 발생하는 다양한 문제를 무시합니다.
이 기사를 작성하는 원래 의도는이 차이를 메우는 것입니다.이 기사는 주로 Java8 아키텍처를 사용하여 관찰자 패턴의 구현을 소개하고 익명의 내부 클래스, Lambda 표현, 스레드 안전 및 비 트라이브 한 시간 소모적 관찰자 구현을 포함 하여이 기반의 고전적인 패턴에 대한 복잡한 문제를 추가로 탐색합니다. 이 기사의 내용은 포괄적이지 않지만이 모델과 관련된 많은 복잡한 문제는 하나의 기사에서만 설명 할 수 없습니다. 그러나이 기사를 읽은 후 독자는 관찰자 패턴, Java의 보편성 및 Java에서 관찰자 패턴을 구현할 때 몇 가지 일반적인 문제를 처리하는 방법을 이해할 수 있습니다.
관찰자 모드
GOF가 제안한 고전적인 정의에 따르면, 관찰자 패턴의 주제는 다음과 같습니다.
객체 사이의 일대일 의존성을 정의합니다. 객체의 상태가 변경되면 이에 의존하는 모든 객체에 알림을 받고 자동으로 업데이트됩니다.
무슨 뜻입니까? 많은 소프트웨어 응용 프로그램에서 객체 간 상태는 상호 의존적입니다. 예를 들어, 애플리케이션이 숫자 데이터 처리에 중점을 둔 경우이 데이터는 그래픽 사용자 인터페이스 (GUI)의 테이블 또는 차트를 통해 표시되거나 동시에 사용될 수 있습니다. 즉, 기본 데이터가 업데이트 될 때 해당 GUI 구성 요소도 업데이트해야합니다. 문제의 핵심은 GUI 구성 요소가 업데이트 될 때 기본 데이터를 업데이트하는 방법과 동시에 GUI 구성 요소와 기본 데이터 간의 커플 링을 최소화하는 방법입니다.
간단하고 스케일 할 수없는 솔루션은 이러한 기본 데이터를 관리하는 객체의 테이블 및 이미지 GUI 구성 요소를 참조하여 객체가 기본 데이터가 변경 될 때 GUI 구성 요소에 알릴 수 있도록하는 것입니다. 분명히,이 간단한 솔루션은 더 많은 GUI 구성 요소를 처리하는 복잡한 응용 분야에 대한 단점을 빠르게 보여주었습니다. 예를 들어, 기본 데이터에 모두 의존하는 20 개의 GUI 구성 요소가 있으므로 기본 데이터를 관리하는 객체는이 20 개의 구성 요소에 대한 참조를 유지해야합니다. 관련 데이터에 의존하는 객체의 수가 증가함에 따라 데이터 관리와 객체 간의 결합 정도는 제어하기가 어렵게됩니다.
또 다른 더 나은 솔루션은 객체가 관심있는 데이터를 업데이트 할 권한을 얻기 위해 등록 할 수 있도록하는 것입니다. 데이터 관리자는 데이터가 변경 될 때 해당 객체에 알립니다. Layman의 용어로는 관심있는 데이터 개체가 관리자에게 다음과 같이 말합니다. "데이터가 변경되면 저에게 알려주십시오." 또한 이러한 객체는 업데이트 알림을 얻기 위해 등록 할 수있을뿐만 아니라 데이터 관리자가 더 이상 데이터가 변경 될 때 객체에 알리지 않도록 등록을 취소 할 수 있습니다. GOF의 원래 정의에서 업데이트를 얻기 위해 등록 된 객체를 "관찰자"라고하며, 해당 데이터 관리자는 "주제"라고하며, 관찰자가 관심을 갖는 데이터를 "대상 상태"라고하며 등록 프로세스를 "추가"라고하며 관찰을 취소하는 프로세스를 "Detach"라고합니다. 위에서 언급했듯이 옵저버 모드를 게시-서브 스크립 모드라고도합니다. 고객이 목표에 대해 관찰자에게 구독한다는 것을 이해할 수 있습니다. 대상 상태가 업데이트되면 대상은 이러한 업데이트를 가입자에게 게시합니다 (이 설계 패턴은 Publish-Subscribe 아키텍처라고하는 일반 아키텍처로 확장 됨). 이러한 개념은 다음 클래스 다이어그램으로 표시 될 수 있습니다.
ConcereteObserver는이를 사용하여 업데이트 상태 변경을 수신하고 ConcerEteSubject에 대한 참조를 생성자에게 전달합니다. 이는 특정 관찰자에 대한 특정 주제에 대한 참조를 제공하며, 여기서 상태가 변경 될 때 업데이트를 얻을 수 있습니다. 간단히 말해서, 특정 관찰자는 주제를 업데이트하라는 지시를 받고 동시에 생성자의 참조를 사용하여 특정 주제의 상태를 얻고 이러한 검색 상태 객체를 특정 관찰자의 관찰자 속성에 저장합니다. 이 프로세스는 다음 시퀀스 다이어그램에 표시됩니다.
고전 모델의 전문화 <br /> 관찰자 모델은 보편적이지만, 가장 일반적인 모델은 다음과 같습니다.
1. 상태 객체에 매개 변수를 제공하고 관찰자가 불리는 업데이트 메소드로 전달하십시오. 클래식 모드에서 관찰자가 피험자 상태가 변경되었음을 알리면 업데이트 된 상태는 대상으로부터 직접 얻을 것입니다. 이를 위해서는 관찰자가 검색된 상태에 대한 객체 참조를 저장해야합니다. 이것은 원형 참조, 콘크리트 하부의 참조를 관찰자 목록에 가리키며 콘크리트 관찰자의 참조는 대상 상태를 얻을 수있는 콘크리트 하부를 가리킨다. 업데이트 된 상태를 얻는 것 외에도, 관찰자와 그 주제를 듣기 위해 등록하는 주제 사이에는 연결이 없습니다. 관찰자는 주제 자체가 아니라 국가 대상에 관심이 있습니다. 즉, 대부분의 경우 ConcreteObserver와 Concretesubject가 강제로 연결됩니다. 반대로 ConcretesUbject가 업데이트 함수를 호출 할 때 상태 객체는 ConcreteObserver로 전달되며 두 개는 연관 될 필요가 없습니다. ConcreteObserver와 State Object 간의 연관성은 관찰자와 상태 사이의 의존성 정도를 줄입니다 (연관성과 의존성의 더 많은 차이는 Martin Fowler의 기사 참조).
2. 주제 추상 클래스와 ConcretesUbject를 단일 서브 젝트 클래스로 병합하십시오. 대부분의 경우, 주제에서 추상 클래스를 사용한다고해서 프로그램 유연성과 확장 성이 향상되지 않으므로이 추상 클래스와 콘크리트 클래스는 설계를 단순화합니다.
이 두 특수 모델을 결합한 후 단순화 된 클래스 다이어그램은 다음과 같습니다.
이러한 특수 모델에서 정적 클래스 구조는 크게 단순화되고 클래스 간의 상호 작용도 단순화됩니다. 현재 시퀀스 다이어그램은 다음과 같습니다.
전문화 모드의 또 다른 특징은 ConcreteObserver의 멤버 변수 관찰물을 제거하는 것입니다. 때로는 특정 관찰자가 주제의 최신 상태를 저장할 필요가 없지만 상태가 업데이트 될 때 주제의 상태 만 모니터링하면됩니다. 예를 들어, 옵저버가 멤버 변수의 값을 표준 출력으로 업데이트하면 ConcreteObserver와 State Class 간의 연관성을 제거하는 ObserverState를 삭제할 수 있습니다.
보다 일반적인 이름 지정 규칙 <br /> 클래식 모드 및 위에서 언급 한 전문 모드조차도 첨부, 분리 및 관찰자와 같은 용어를 사용하는 반면, 많은 Java 구현은 레지스터, 등록, 청취자 등을 포함한 다양한 사전을 사용합니다. 상태는 청취자가 변경 사항을 모니터링하는 데 필요한 모든 개체에 대한 일반적인 용어라고 언급 할 가치가 있습니다. 상태 객체의 특정 이름은 관찰자 모드에서 사용 된 시나리오에 따라 다릅니다. 예를 들어, 청취자가 이벤트 발생을 듣는 장면의 옵저버 모드에서 등록 된 청취자는 이벤트가 발생할 때 알림을 받게됩니다. 현재 상태 객체는 이벤트, 즉 이벤트가 발생했는지 여부입니다.
실제 응용 분야에서 대상의 이름 지정에는 주제가 거의 포함되지 않습니다. 예를 들어 동물원에 대한 앱을 만들고 여러 청취자를 동물원 수업을 관찰하여 새 동물이 동물원에 들어갈 때 알림을받습니다. 이 경우 목표는 동물원 수업입니다. 주어진 문제 영역과 일치하는 용어를 유지하기 위해 "대상"이라는 용어는 사용되지 않으므로 동물원 클래스의 이름이 Zoosubject로 선정되지 않습니다.
리스너의 이름은 일반적으로 청취자 접미사가 뒤 따릅니다. 예를 들어, 새로운 동물을 모니터링하기 위해 위에서 언급 한 청취자의 이름은 AnimalAddedListener입니다. 마찬가지로, 레지스터, 등록 및 알림과 같은 함수의 이름 지정은 종종 해당 청취자 이름에 의해 접미사됩니다. 예를 들어, emitalAddedListener의 레지스터, 등록 및 알림 기능은 RegisterAnimalAddedListener, UnregisteranimalAddedListener 및 NotifyanimalAddedListeners로 지정됩니다. Notify 함수는 단일 리스너가 아닌 여러 리스너를 처리하기 때문에 Notify 함수 이름 S가 사용됩니다.
이 이름 지정 방법은 길어 보이며 일반적으로 주제는 여러 유형의 청취자를 등록합니다. 예를 들어, 위에서 언급 한 동물원의 예에서 동물원에서 동물 모니터링을위한 새로운 청취자를 등록하는 것 외에도 청취자에게 청취자를 등록하여 청취자를 줄여야합니다. 현재 두 가지 레지스터 기능이 있습니다.
또 다른 관용 구문은 업데이트 대신 Prefix에서 사용하는 것입니다. 예를 들어 업데이트 기능은 UpdateanimalAdded 대신 OnanimalAdded로 명명됩니다. 이 상황은 청취자가 목록에 동물을 추가하는 것과 같은 시퀀스에 대한 알림을받을 때 더 일반적이지만 동물 이름과 같은 별도의 데이터를 업데이트하는 데 거의 사용되지 않습니다.
다음 으로이 기사는 Java의 상징적 규칙을 사용합니다. 기호 규칙이 시스템의 실제 설계 및 구현을 변경하지는 않지만 다른 개발자가 익숙한 용어를 사용하는 것이 중요한 개발 원칙이므로 위에서 설명한 Java의 관찰자 패턴 상징 규칙에 익숙해야합니다. 위의 개념은 Java 8 환경에서 간단한 예를 사용하여 아래에 설명됩니다.
간단한 예
위에서 언급 한 동물원의 예이기도합니다. Java8의 API 인터페이스를 사용하여 간단한 시스템을 구현하여 관찰자 패턴의 기본 원리를 설명합니다. 문제는 다음과 같이 설명됩니다.
시스템 동물원을 만들어 사용자가 새로운 물체 동물을 추가하는 상태를 듣고 취소하고 새로운 동물의 이름을 출력 할 책임이있는 특정 청취자를 만듭니다.
관찰자 패턴에 대한 이전 학습에 따르면, 우리는 이러한 응용 프로그램을 구현하기 위해서는 특히 4 개의 클래스를 만들어야한다는 것을 알고 있습니다.
먼저 이름 멤버 변수, 생성자, 게터 및 세터 메소드가 포함 된 간단한 Java 객체 인 Animal Class를 만듭니다. 코드는 다음과 같습니다.
공개 계급 동물 {개인 문자열 이름; 공개 동물 (문자열 이름) {this.name = 이름; } public String getName () {return this.name; } public void setName (문자열 이름) {this.name = 이름; }}이 클래스를 사용하여 동물 대상을 나타내면 AnimalAddedListener 인터페이스를 만들 수 있습니다.
Public Interface AnimalAddedListener {public void onanimaladded (동물 동물);}처음 두 클래스는 매우 간단하므로 자세히 소개하지 않습니다. 다음으로 동물원 수업을 만듭니다.
공개 클래스 동물원 {private list <emal> 동물 = New Arraylist <> (); 개인 목록 <QualitaddedListener> 리스너 = New ArrayList <> (); 공공 void addanimal (동물 동물) {// 동물 목록에 동물을 추가하십시오. animals.add (동물); // 등록 된 청취자 목록에 알림 this.NotifyanimalAddedListeners (동물); } public void RegisterAnimalAddedListener (AnimalAddedListener Listener) {// 리스너를 등록 된 리스너 목록에 추가 this.listeners.add (리스너); } public void public void unregisteranimaladdedListener (AnimalAddedListener Listener) {// 등록 된 리스너 목록에서 리스너를 제거하십시오. } 보호 된 void notifyAnimalAddedListeners (동물 동물) {// 등록 된 청취자 목록에서 각 청취자에게 알림 this.listeners.foreach (청취자 -> 청취자 .updateanimaladded (동물)); }}이 비유는 이전 두 가지보다 복잡합니다. 여기에는 두 개의 목록이 포함되어 있으며, 하나는 동물원에 모든 동물을 저장하는 데 사용되고 다른 하나는 모든 청취자를 저장하는 데 사용됩니다. 동물과 청취자 컬렉션에 저장된 객체가 간단하다는 것을 감안할 때이 기사는 저장 용 Arraylist를 선택했습니다. 저장된 리스너의 특정 데이터 구조는 문제에 따라 다릅니다. 예를 들어, 동물원 문제의 경우 청취자가 우선 순위가있는 경우 다른 데이터 구조를 선택하거나 리스너의 레지스터 알고리즘을 다시 작성해야합니다.
등록 및 제거의 구현은 간단한 대의원 방법입니다. 각 청취자는 청취자의 청취 목록에서 매개 변수로 추가되거나 제거됩니다. 노트 기능의 구현은 관찰자 패턴의 표준 형식에서 약간 꺼져 있습니다. 여기에는 입력 매개 변수가 포함되어 있습니다 : 새로 추가 된 동물은 알림 기능이 새로 추가 된 동물 참조를 리스너에 전달할 수 있습니다. Streams API의 Foreach 함수를 사용하여 리스너를 가로 지르고 각 청취자에서 Theonanimaladded 기능을 실행하십시오.
AddAnimal 함수에서 새로 추가 된 동물 물체 및 청취자가 해당 목록에 추가됩니다. 알림 프로세스의 복잡성을 고려하지 않으면이 논리는 편리한 통화 방법에 포함되어야합니다. 새로 추가 된 동물 대상을 참조하면 전달하면됩니다. 그렇기 때문에 알림 리스너의 논리적 구현이 addanimal 구현에 언급 된 NotifyanimaladdedListeners 기능에 캡슐화되는 이유입니다.
Notify 함수의 논리적 문제 외에도 Notify 함수의 가시성에 대한 논란의 여지가있는 문제를 강조해야합니다. Classic Observer 모델에서 GOF가 Book Design Patterns의 301 페이지에서 언급했듯이 Notify 기능은 공개적이지만 고전적인 패턴에 사용되지만 공개적이어야한다는 의미는 아닙니다. 가시성 선택은 응용 프로그램을 기반으로해야합니다. 예를 들어,이 기사의 동물원 예제에서, 알림 함수는 유형의 보호이며 각 객체가 등록 된 관찰자의 알림을 시작하도록 요구하지 않습니다. 객체가 부모 클래스에서 함수를 상속받을 수 있는지 확인하면됩니다. 물론 이것은 정확히 그렇지 않습니다. 어떤 클래스가 Notify 함수를 활성화 할 수 있는지 파악한 다음 함수의 가시성을 결정해야합니다.
다음으로 PrintnameanimalAddedListener 클래스를 구현해야합니다. 이 클래스는 System.out.println 메소드를 사용하여 새 동물의 이름을 출력합니다. 특정 코드는 다음과 같습니다.
Public Class PrintnameanimalAddedListener는 AnimalAddedListener를 구현합니다. {@override public void updateanimaladded (동물 동물) {// 새로 추가 된 동물 시스템의 이름을 인쇄합니다. }}마지막으로 응용 프로그램을 구동하는 주요 기능을 구현해야합니다.
Public Class Main {public static void main (String [] args) {// 동물을 저장하기 위해 동물원 생성 동물 동물원 Zoo = New Zoo (); // 동물이 추가 될 때 알림을받을 청취자를 등록하십시오. // 등록 된 청취자에게 알리는 동물 추가 zoo.addanimal (새 동물 ( "Tiger")); }}주요 기능은 단순히 동물원 객체를 생성하고 동물 이름을 출력하는 리스너를 등록하고 등록 된 리스너를 트리거하기 위해 새로운 동물 객체를 만듭니다. 최종 출력은 다음과 같습니다.
'Tiger'라는 이름의 새로운 동물을 추가했습니다.
청취자가 추가되었습니다
리스너가 다시 설립되어 주제에 추가 될 때 관찰자 모드의 장점이 완전히 표시됩니다. 예를 들어 동물원에서 총 동물 수를 계산하는 리스너를 추가하려면 특정 청취자 클래스를 만들고 동물원 수업에 대한 수정없이 동물원 수업에 등록하면됩니다. 카운팅 리스너 추가 CountinganimaladdedListener 코드 추가는 다음과 같습니다.
공개 클래스 CountinganimaladdedListener는 AnimalAddedListener를 구현합니다. @override public void updateanimaladded (동물 동물) {// 동물의 수를 증가시킵니다. // 동물의 수를 인쇄 시스템 .out.println ( "총 동물 추가 :" + emitedAddedCount); }}수정 된 주요 함수는 다음과 같습니다.
Public Class Main {public static void main (String [] args) {// 동물을 저장하기 위해 동물원 생성 동물 동물원 Zoo = New Zoo (); // 동물이 추가 될 때 리스너에게 알릴 수 있도록 리스너를 등록하십시오. zoo.registeranimaladdedListener (New CountinganimaladdedListener ()); // 등록 된 청취자에게 알리는 동물 추가 zoo.addanimal (새 동물 ( "Tiger")); zoo.addanimal (새로운 동물 ( "lion")); zoo.addanimal (새로운 동물 ( "곰")); }}출력 결과는 다음과 같습니다.
이름을 가진 새 동물 추가 'Tiger'total 동물 추가 : 1 이름을 가진 새 동물'lion'toTal 동물 추가 : 2 이름을 가진 새 동물 'BEAR'TOTAL 동물 추가 : 3
사용자는 리스너 등록 코드 만 수정하면 청취자를 만들 수 있습니다. 이 확장 성은 주로 피사체가 콘크리트 포더와 직접 관련이 아니라 관찰자 인터페이스와 관련되기 때문에 주로입니다. 인터페이스가 수정되지 않는 한 인터페이스의 주제를 수정할 필요가 없습니다.
익명의 내부 클래스, Lambda 기능 및 청취자 등록
Java 8의 주요 개선은 Lambda 기능의 추가와 같은 기능적 특징의 추가입니다. Lambda 기능을 도입하기 전에 Java는 익명의 내부 클래스를 통해 유사한 기능을 제공했으며,이 클래스는 여전히 기존의 많은 응용 프로그램에서 여전히 사용됩니다. 관찰자 모드에서는 특정 관찰자 클래스를 만들지 않고 언제든지 새로운 리스너를 만들 수 있습니다. 예를 들어, PrintnameanimalAddedListener 클래스는 익명의 내부 클래스와 함께 기본 기능에서 구현할 수 있습니다. 특정 구현 코드는 다음과 같습니다.
Public Class Main {public static void main (String [] args) {// 동물을 저장하기 위해 동물원 생성 동물 동물원 Zoo = New Zoo (); // 동물이 추가 될 때 청취자에게 알릴 수 있도록 등록자를 등록하십시오. zoo.registeranimalAddedListener (new AnimalAddedListener () {@Override public void updateanimalAdded (동물 동물) {// 새로 추가 된 동물 시스템의 이름을 인쇄합니다. // 등록 된 청취자에게 알리는 동물 추가 zoo.addanimal (새 동물 ( "Tiger")); }}마찬가지로 Lambda 기능은 이러한 작업을 완료하는 데 사용될 수도 있습니다.
Public Class Main {public static void main (String [] args) {// 동물을 저장하기 위해 동물원 생성 동물 동물원 Zoo = New Zoo (); // 동물이 추가 될 때 청취자에게 알릴 수 있도록 리스너를 등록하십시오. // 등록 된 청취자에게 알리는 동물 추가 zoo.addanimal (새 동물 ( "Tiger")); }}Lambda 함수는 리스너 인터페이스에 기능이 하나만있는 상황에만 적합하다는 점에 유의해야합니다. 이 요구 사항은 엄격 해 보이지만 많은 청취자는 실제로 예제의 AnimalAddedListener와 같은 단일 기능입니다. 인터페이스에 여러 기능이있는 경우 익명 내부 클래스를 사용하도록 선택할 수 있습니다.
생성 된 청취자의 암시 적 등록에는 이러한 문제가 있습니다. 등록 통화의 범위 내에서 개체가 생성되므로 특정 리스너에 대한 참조를 저장하는 것은 불가능합니다. 즉, Lambda 기능 또는 익명의 내부 클래스를 통해 등록 된 청취자는 취소 기능이 등록 된 리스너에 대한 참조가 필요하기 때문에 취소 할 수 없습니다. 이 문제를 해결하는 쉬운 방법은 RegisteranimalAddedListener 기능의 등록 된 리스너에 대한 참조를 반환하는 것입니다. 이런 식으로 Lambda 기능 또는 익명의 내부 클래스로 생성 된 청취자를 등록 할 수 있습니다. 개선 된 메소드 코드는 다음과 같습니다.
Public AnimalAddedListener RegisterAnimalAddedListener (AnimalAddedListener Listener) {// 리스너를 등록 된 리스너 목록에 추가 this.listeners.add (리스너); 리턴 리스너;}재 설계된 기능 상호 작용에 대한 클라이언트 코드는 다음과 같습니다.
Public Class Main {public static void main (String [] args) {// 동물을 저장하기 위해 동물원 생성 동물 동물원 Zoo = New Zoo (); // 동물이 추가되었을 때 리스너를 알릴 수 있도록 리스너를 알릴 수 있습니다. AnimalAddedListener Listener = zoo.registeranimalAddedListener ((동물) -> System.out.println ( "이름이있는 새 동물이 추가되었습니다." + 동물성 .getName () + "" "); // 등록 된 청취자에게 알리는 동물 추가 zoo.addanimal (새 동물 ( "Tiger")); // 리스너 동물원을 등록 해제합니다. // 청취자가 이전에 등록되지 않은 zoo.addanimal (new Animal ( "lion")) 이후 이름을 인쇄하지 않는 다른 동물을 추가합니다. }}이 시점의 결과 출력은 두 번째 동물이 추가되기 전에 청취자가 취소 되었기 때문에 'Tiger'라는 이름의 새 동물 만 추가됩니다.
'Tiger'라는 이름의 새로운 동물을 추가했습니다.
보다 복잡한 솔루션이 채택되면 레지스터 함수는 등록되지 않은 리스너가 호출되도록 수신기 클래스를 반환 할 수 있습니다.
공개 클래스 AnimalAddedListenerReceipt {Private Final AnimalAddedListener 청취자; Public AnimalAddedListenerReceipt (AnimalAddedListener Listener) {this.listener = 리스너; } public Final AnimalAddedListener getListener () {return this.listener; }}영수증은 등록 함수의 반환 값으로 사용되며 등록 함수의 입력 매개 변수가 취소됩니다. 현재 동물원 구현은 다음과 같습니다.
공개 클래스 zoousingReceipt {// ... 기존 속성 및 생성자 ... Public AnimalAddedListenerReceipt RegisterAnimalAddedListener (AnimalAddedListener reater) {// 등록 된 리스너 목록에 리스너를 추가하십시오. 새로운 AnimalAddedListenerReceipt (리스너)를 반환합니다. } public void public void unregisteranimalAddedListener (AnimalAddedListenerReceipt Reception) {// 등록 된 리스너 목록에서 리스너를 제거합니다. } // ... 기존 알림 방법 ...}위에서 설명한 수신 구현 메커니즘을 통해 취소 할 때 청취자에게 호출을위한 정보를 저장할 수 있습니다. 취소 등록에 이전 등록 된 청취자에 대한 참조 만 필요하면 리셉션 기술이 번거롭게 보이며 권장되지 않습니다.
특히 복잡한 특정 청취자 외에도 청취자를 등록하는 가장 일반적인 방법은 Lambda 기능이나 익명의 내부 클래스를 통한 것입니다. 물론, 예외, 즉 주제를 포함하는 클래스는 관찰자 인터페이스를 구현하고 참조 대상을 호출하는 리스너를 등록합니다. 다음 코드에 표시된 사례 :
공개 클래스 동물원 컨 컨테이너는 AnimalAddedListener {private Zoo Zoo = New Zoo ()를 구현합니다. public zoocontainer () {//이 개체를 리스너로 등록 this.zoo.zoo.registeranimaladdedListener (this); } public Zoo getzoo () {return this.zoo; } @Override public void updateanimalAdded (동물 동물) {System.out.println ( "이름이 추가 된 동물 '" + 동물성 .getName () + "'"); } public static void main (string [] args) {// 동물원 컨테이너 생성 zoocontainer zetaocontainer = new Zoocontainer (); // 내면의 알림 리 리스너 ZooContainer.getzoo (). addAnimal (새 동물 ( "tiger"))에 알림을 추가합니다. }}이 접근법은 간단한 사례에만 적합하며 코드는 충분히 전문적이지 않으며 현대식 Java 개발자에게 여전히 인기가 높기 때문에이 예제의 작동 방식을 이해해야합니다. Zoocontainer는 AnimalAddedListener 인터페이스를 구현하기 때문에 Zoocontainer의 인스턴스 (또는 객체)를 AnimalAddedListener로 등록 할 수 있습니다. Zoocontainer 클래스 에서이 참조는 현재 객체, 즉 Zoocontainer의 인스턴스를 나타내며 AnimalAddedListener로 사용될 수 있습니다.
일반적으로 모든 컨테이너 클래스가 그러한 기능을 구현하는 데 필요한 것은 아니며, 리스너 인터페이스를 구현하는 컨테이너 클래스는 주제 등록 기능 만 호출 할 수 있지만 단순히 레지스터 기능에 대한 참조를 리스너 개체로 전달합니다. 다음 장에서는 멀티 스레드 환경을위한 FAQ 및 솔루션이 소개됩니다.
스레드 안전 <br /> 이전 장에서는 현대 자바 환경에서 관찰자 패턴의 구현을 소개합니다. 간단하지만 완전하지만이 구현은 스레드 안전 인 주요 문제를 무시합니다. 대부분의 열린 Java 응용 프로그램은 다중 스레드이며 관찰자 모드는 주로 다중 스레드 또는 비동기 시스템에서 사용됩니다. 예를 들어, 외부 서비스가 데이터베이스를 업데이트하는 경우 응용 프로그램은 메시지를 비동기로 수신 한 다음 외부 서비스를 직접 등록하고 듣는 대신 내부 구성 요소에 관찰자 모드에서 업데이트하도록 알립니다.
옵저버 모드의 스레드 안전은 주로 모드의 본문에 중점을 둡니다. 등록 된 리스너 컬렉션을 수정할 때 스레드 충돌이 발생할 수 있기 때문입니다. 예를 들어, 한 스레드는 새로운 리스너를 추가하려고 시도하는 반면, 다른 스레드는 새로운 동물 객체를 추가하려고 시도하여 모든 등록 된 리스너에게 알림을 유발합니다. 시퀀스 순서가 주어지면, 첫 번째 스레드는 등록 된 청취자가 추가 된 동물에 대한 알림을 받기 전에 새로운 청취자의 등록을 완료했을 수도 있고 아닐 수도 있습니다. 이것은 스레드 리소스 경쟁의 전형적인 사례이며,이 현상은 개발자에게 스레드 안전을 보장하기위한 메커니즘이 필요하다는 것을 알려줍니다.
이 문제에 대한 가장 쉬운 해결책은 다음과 같습니다. 등록 리스너 목록에 액세스하거나 수정하는 모든 작업은 다음과 같은 Java 동기화 메커니즘을 따라야합니다.
public synchronized AnimalAddedListener RegisteranimalAddedListener (AnimalAddedListener Listener) {/*.../} 공개 void UnregisteranimalAddedListener (AnimalAddedListener Listener) {/*.../} public synchronized void notifyanimaladdedlisteners (동물) {/*.}.이런 식으로 동시에, 하나의 스레드만이 등록 된 리스너 목록을 수정하거나 액세스 할 수 있는데, 이는 자원 경쟁 문제를 성공적으로 피할 수 있지만 새로운 문제가 발생하고 이러한 제약이 너무 엄격합니다 (동기화 된 키워드 및 Java 동의 모델에 대한 자세한 내용은 공식 웹 페이지를 참조하십시오). 메소드 동기화를 통해 리스너 목록에 대한 동시 액세스가 항상 관찰 될 수 있습니다. 리스너를 등록하고 취소하는 것은 리스너 목록의 쓰기 조작이며, 리스너가 액세스하도록 알리는 것은 읽기 전용 작업입니다. 알림을 통한 액세스는 읽기 작업이므로 다중 알림 작업을 동시에 수행 할 수 있습니다.
따라서, 리스너 등록 또는 취소가없는 한, 등록 된 리스너 목록에 대한 리소스 경쟁을 트리거하지 않고 동시 알림을 동시에 실행할 수있는 한 등록이 등록되지 않는 한, 등록이 등록되지 않는 한. 물론, 다른 상황에서의 자원 경쟁은 오랫동안 존재 해 왔습니다. 이 문제를 해결하기 위해 ReadWritelock의 리소스 잠금은 읽기 및 쓰기 작업을 별도로 관리하도록 설계되었습니다. Zoo Class의 스레드-안전 스레드 STREADSAFEZOO 구현 코드는 다음과 같습니다.
Public Class ThreadSafezoo {Private Final ReadWritelock readWritelock = New ReentRantreadWritelock (); 보호 된 최종 잠금 readlock = readwritelock.readlock (); 보호 된 최종 잠금 Writelock = readWritelock.writelock (); 개인 목록 <Amenti> 동물 = New ArrayList <> (); 개인 목록 <QualitaddedListener> 리스너 = New ArrayList <> (); 공공 void addanimal (동물 동물) {// 동물 목록에 동물을 추가하십시오. animals.add (동물); // 등록 된 청취자 목록에 알림 this.NotifyanimalAddedListeners (동물); } public AnimalAddedListener RegisterAnimalAddedListener (AnimalAddedListener Listener) {// this.writelock.lock ()을 작성하기 위해 리스너 목록을 잠그십시오. 시도 {// 리스너를 등록 된 리스너 목록에 추가 this.listeners.add (리스너); } 마침내 {// Writer 잠금을 잠금 해제 this.writelock.unlock (); } 리턴 리스너; } public void unregisteranimaladdedListener (AnimalAddedListener Listener) {// this.writelock.lock ()을 작성하기 위해 리스너 목록을 잠그십시오. 시도 {// 등록 된 리스너 목록에서 리스너를 제거하십시오. } 마침내 {// Writer 잠금을 잠금 해제 this.writelock.unlock (); }} public void notifyanimaladdedListeners (동물 동물) {// this.readlock.lock ()을 읽는 리스너 목록을 잠그십시오. 시도 {// 등록 된 청취자 목록에서 각 청취자에게 알림 this.listeners.foreach (Lister-> Listener.updateanimalAdded (동물)); } 마침내 {// 리더 잠금 해제 this.readlock.unlock (); }}}이러한 배치를 통해 주제의 구현은 스레드 안전을 보장 할 수 있으며 여러 스레드가 동시에 알림을 발행 할 수 있습니다. 그러나 그럼에도 불구하고 여전히 무시할 수없는 두 가지 자원 경쟁 문제가 있습니다.
각 청취자에 대한 동시 액세스. 여러 스레드가 리스너에게 새로운 동물이 필요하다는 것을 알릴 수 있습니다. 즉, 청취자는 여러 스레드에 의해 동시에 호출 될 수 있습니다.
동물 목록에 동시에 접근 할 수 있습니다. 여러 실이 동물 목록에 동시에 객체를 추가 할 수 있습니다. 알림 순서에 영향을 미치는 경우 자원 경쟁으로 이어질 수 있으며,이 문제는이 문제를 피하기 위해 동시 작동 처리 메커니즘이 필요합니다. 등록 된 청취자 목록이 동물을 추가하라는 알림을받은 다음 Animal1을 추가 할 알림을 받으면 자원 경쟁이 이루어집니다. 그러나, Animal1 및 Animal2의 첨가가 상이한 실에 의해 수행되는 경우, Animal2 전에 Animal1의 첨가를 완료 할 수도있다. 구체적으로, 스레드 1은 리스너에게 알리고 모듈을 잠그기 전에 Animal1을 추가하고, 스레드 2는 Animal2를 추가하고 청취자에게 알리고, Thread 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需要用来保存已经注册的监听器列表。
其中的一个解决方案是创建一个动物增加和减少时都会通知的监听器类型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 实现观察者模式?相信通过这篇文章大家都有了大概的了解了吧!