设计模式学习 - 观察者模式
观察者模式属于设计模式中行为模式的一种,这个设计模式可以说是一个非常好用而且在现实中也有很多地方使用的设计模式,比如: Java Swing, JavaFX,甚至流行的中间件像 Kafka,RabbitMQ 也有观察者模式的影子。
什么是观察者模式
定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知并自动更新。
这里的一对多种的一就是被观察的对象(Subject),多就是各种观察者(监听者)(Observer),状态就是我们(观察者)关心的数据(State/Value)。
工作原理:
通过松耦合设计,将观察者注册到被观察对象内部,然后在被观察对象的状态发生改变时,通知所有观察者。每个观察者根据业务需求在收到通知后进行相应处理,而被观察者并不需要知道观察者都做了什么。从设计的角度来看,观察者模式主要涉及4个不同的角色
- Subject - 被观察对象接口,只约定基本的行为,并不定义每个行为如何实现
- Observer - 观察者接口,同样只约定行为
- ConcreteSubject - 是具体的被观察对象,这个对象必须实现 Subject 中定义的对观察者的注册和删除以及通知功能,当然获取状态是必不可少的(实现方式可能会有差异)
- ConcreteObserver - 具体的观察者对象,每个具体观察者同样必须实现 Observer 接口中定义的行为,具体行为根据每个观察者实例的需求会有所区别
曾经有大牛说过,抽象可以解决一切复杂技术问题。呵呵,可能这话绝对了些,但是在这里来解释这几个概念却是合适的。在经过高度抽象后,观察者模式可以概括为
在被观察者状态发生变化后,被观察者会通知已经注册的观察者,已注册的观察者会在接到通知后进行响应。
被观察者主体在英语里面最抽象的词汇或许就是 Subject 了,观察者很直接就是 Observer,这么抽象的概念在 Java 里面用接口表示最合适不过的了。下面咱们先对这两个抽象概念进行定义,然后在具体化。
抽象定义
被观察者 Subject
被观察者主要的职责有
- 注册观察者
- 移除观察者
- 在状态发生改变时通知所有已经注册的观察者
下面的接口设计就是表示这个3个行为以及获取被观察者最新的状态的:
public interface Subject<T> { void addObserver(Observer<T> observer); boolean removeObserver(Observer<T> observer); void notifyObservers(); T getValue(); }
观察者 Observer
观察者除了自身要做的事情外,还需要:
- 在观察到被观察对象状态变化时,进行相应业务处理操作
下面的 update() 方法就是这个操作
public interface Observer<T> { void update(T value); }
假设:
- 以上的设计基于这样的假设,我们总会有办法把会发生的状态数据封装成一个被观察者 Subject 的属性,而且观察者只对新的数据感兴趣。如果观察者同时需要旧数据或者新数据,我会后续中发帖
- 关于注册观察者,观察者注册可以由主体 Subject 进行,也可以由观察者完成,后者的好处是,只有真正对数据感兴趣的观察者才会将自己注册到列表中去。
单单有了接口定义并不能直接解决问题,我们还需要一个具体的被观察者(ConcreteSubject)和一个或者多个观察者(ConcreteObserver),下面来使用一个关于天气预报的案例来说明观察者模式的使用
案例分析
假设我们需要有一个基于 IoT 的天气观测后台,如果接受到的天气数据发生变化,所有已经注册的观察者都会被通知到。观察者会根据各自业务规则完成定制,比如发送短信、记录日志、甚至预警等。
设计思想
在开始具体代码之前在想一下,另外一个Java设计原则,尽可能复用,每个被观察对象Subject都会有注册/删除被观察者,都会有自己的状态数据,每个观察者都要自行注册。那么有没有什么办法把这些通用的给做一些封装呢?答案是抽象类。然后每个具体的 Subject 和观察者都继承个字的抽象类,在每个具体类中只需要实现一小部分特定功能即可。当然,这个设计的局限是使用了继承,当你的观察者或者主体已经有需要继承的父类时,这个抽象类的方案就不能用了。要解决这个问题就是使用组合,单独写一个类来完成抽象类里面封装的东西,然后在每个具体的类里面调用这个单独的帮助类来达到代码复用的目的。在 JavaFX 中的 ObservableValue 和 ExpressionHelper 就是采用了这种思路(组合代替继承)。
抽象主体 Subject
1 import lombok.extern.slf4j.Slf4j; 2 3 import java.util.ArrayList; 4 import java.util.List; 5 6 @Slf4j 7 public abstract class AbstractSubject<T> implements Subject<T> { 8 private final List<Observer<T>> observers; 9 10 protected AbstractSubject() { 11 this.observers = new ArrayList<>(); 12 } 13 14 @Override 15 public void addObserver(Observer<T> observer) { 16 assert observer != null; 17 observers.add(observer); 18 } 19 20 @Override 21 public boolean removeObserver(Observer<T> observer) { 22 return observers.remove(observer); 23 } 24 25 @Override 26 public final void notifyObservers() { 27 log.info("Notifying all observers"); 28 observers.parallelStream().forEach(o -> o.update(getValue())); 29 log.info("All observers notified."); 30 } 31 32 }
抽象观察者 Observer
public abstract class AbstractObserver<T> implements Observer<T>{ private final Subject<T> subject; public AbstractObserver(Subject<T> subject) { this.subject = subject; this.subject.addObserver(this); } protected Subject<T> getSubject() { return subject; } }
找出具体的被观察者、状态和观察者
这里的被观察者是天气数据接收装置,状态是天气数据,观察者是根据天气变化做出响应的各个规则
状态数据 Weather
我们这里的状态数据 Weather 为天气有关的基本信息,这个 Weather 就是观察者真正感兴趣的数据
import lombok.Data; import lombok.EqualsAndHashCode; @Data @EqualsAndHashCode public class Weather { Integer lowest; Integer highest; String condition; }
天气数据接收装置(Subject)
import com.tng.sandbox.designpatterns.observer.core.AbstractSubject; import com.tng.sandbox.designpatterns.observer.core.Subject; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; @Slf4j @Data @EqualsAndHashCode(callSuper = false) @Component public class WeatherDataReceiver extends AbstractSubject<Weather> implements Subject<Weather> { private Weather weather; public void receive(Weather weather) { if (this.weather==null || !this.weather.equals(weather)) { this.weather = weather; notifyObservers(); } } @Override public Weather getValue() { return weather; } }
观察者(Observer)
为了演示一对多的情况,这里定义了4个观察者类,代码如下
import com.tng.sandbox.designpatterns.observer.core.AbstractObserver; import com.tng.sandbox.designpatterns.observer.core.Observer; import com.tng.sandbox.designpatterns.observer.core.Subject; import com.tng.sandbox.designpatterns.observer.weather.Weather; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; @Slf4j @Service public class HelloObserver extends AbstractObserver<Weather> implements Observer<Weather> { public HelloObserver(Subject<Weather> subject) { super(subject); } @Override public void update(Weather value) { log.info("Hello there. "); } }
import com.tng.sandbox.designpatterns.observer.core.AbstractObserver; import com.tng.sandbox.designpatterns.observer.core.Observer; import com.tng.sandbox.designpatterns.observer.core.Subject; import com.tng.sandbox.designpatterns.observer.weather.Weather; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; @Slf4j @Service public class LogWeatherObserver extends AbstractObserver<Weather> implements Observer<Weather> { public LogWeatherObserver(Subject<Weather> subject) { super(subject); } @Override public void update(Weather value) { log.info("Logging weather change event. {}", value); } }
import com.tng.sandbox.designpatterns.observer.core.AbstractObserver; import com.tng.sandbox.designpatterns.observer.core.Observer; import com.tng.sandbox.designpatterns.observer.core.Subject; import com.tng.sandbox.designpatterns.observer.weather.Weather; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; @Slf4j @Service public class NoOpObserver extends AbstractObserver<Weather> implements Observer<Weather> { public NoOpObserver(Subject<Weather> subject) { super(subject); } @Override public void update(Weather value) { log.warn("I'm doing nothing..."); } }
import com.tng.sandbox.designpatterns.observer.core.AbstractObserver; import com.tng.sandbox.designpatterns.observer.core.Observer; import com.tng.sandbox.designpatterns.observer.core.Subject; import com.tng.sandbox.designpatterns.observer.weather.Weather; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; @Slf4j @Service public class WeatherObserverImpl extends AbstractObserver<Weather> implements Observer<Weather> { public WeatherObserverImpl(Subject<Weather> subject) { super(subject); } @Override public void update(Weather value) { log.warn("Weather has changed to {}", value); } }
集成演示
为了演示方便,这里使用了 Spring Boot,在 Rest Controller 里面集成Subject数据,然后通过 PostMan 发送天气数据来模拟 IoT 设备
RestController
1 import com.tng.sandbox.designpatterns.observer.weather.Weather; 2 import com.tng.sandbox.designpatterns.observer.weather.WeatherSubject; 3 import lombok.extern.slf4j.Slf4j; 4 import org.springframework.web.bind.annotation.PostMapping; 5 import org.springframework.web.bind.annotation.RequestBody; 6 import org.springframework.web.bind.annotation.RequestMapping; 7 import org.springframework.web.bind.annotation.RestController; 8 9 import javax.validation.constraints.NotNull; 10 11 @Slf4j 12 @RestController 13 @RequestMapping("/api/weather") 14 public class WeatherController { 15 private final WeatherDataReceiver subject; 16 17 public WeatherController(WeatherDataReceiver subject) { 18 this.subject = subject; 19 } 20 21 @PostMapping 22 public void forecast(@RequestBody @NotNull Weather weather) { 23 log.info("Receiving weather forecast data"); 24 log.info("Incoming weather data: {}", weather); 25 subject.receive(weather); 26 log.info("Forecast finished...."); 27 } 28 }
HTTP Post
curl --request POST \ --url http://localhost:8080/api/weather \ --header 'Accept: */*' \ --header 'Connection: keep-alive' \ --header 'Content-Type: application/json' \ --header 'Host: localhost:8080' \ --header 'Postman-Token: 7dbb7ef4-c35b-4d23-89aa-3b3659c859b9' \ --header 'User-Agent: PostmanRuntime/7.11.0' \ --header 'accept-encoding: gzip, deflate' \ --header 'cache-control: no-cache' \ --header 'content-length: 56' \ --data '{\n "lowest": 16,\n "highest": 27,\n "condition": "Rainy"\n}'
控制台输出:
. ____ _ __ _ _ /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ \\/ ___)| |_)| | | | | || (_| | ) ) ) ) ' |____| .__|_| |_|_| |_\__, | / / / / =========|_|==============|___/=/_/_/_/ :: Spring Boot :: (v2.1.4.RELEASE) 2019-04-27 20:57:09.046 INFO 48897 --- [ main] c.t.s.d.o.ObserverPatternApplication : No active profile set, falling back to default profiles: default 2019-04-27 20:57:10.157 INFO 48897 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 8080 (http) 2019-04-27 20:57:10.187 INFO 48897 --- [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat] 2019-04-27 20:57:10.187 INFO 48897 --- [ main] org.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/9.0.17] 2019-04-27 20:57:10.331 INFO 48897 --- [ main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext 2019-04-27 20:57:10.331 INFO 48897 --- [ main] o.s.web.context.ContextLoader : Root WebApplicationContext: initialization completed in 1238 ms 2019-04-27 20:57:10.625 INFO 48897 --- [ main] o.s.s.concurrent.ThreadPoolTaskExecutor : Initializing ExecutorService 'applicationTaskExecutor' 2019-04-27 20:57:10.901 INFO 48897 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path '' 2019-04-27 20:57:10.906 INFO 48897 --- [ main] c.t.s.d.o.ObserverPatternApplication : Started ObserverPatternApplication in 2.723 seconds (JVM running for 7.82) 2019-04-27 20:57:14.866 INFO 48897 --- [nio-8080-exec-2] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring DispatcherServlet 'dispatcherServlet' 2019-04-27 20:57:14.866 INFO 48897 --- [nio-8080-exec-2] o.s.web.servlet.DispatcherServlet : Initializing Servlet 'dispatcherServlet' 2019-04-27 20:57:14.877 INFO 48897 --- [nio-8080-exec-2] o.s.web.servlet.DispatcherServlet : Completed initialization in 11 ms 2019-04-27 20:57:15.016 INFO 48897 --- [nio-8080-exec-2] c.t.s.d.observer.web.WeatherController : Receiving weather forecast data 2019-04-27 20:57:15.017 INFO 48897 --- [nio-8080-exec-2] c.t.s.d.observer.web.WeatherController : Incoming weather data: Weather(lowest=13, highest=27, condition=Rainy) 2019-04-27 20:57:15.018 INFO 48897 --- [nio-8080-exec-2] c.t.s.d.observer.core.AbstractSubject : Notifying all observers 2019-04-27 20:57:15.023 WARN 48897 --- [nio-8080-exec-2] c.t.s.d.o.w.observers.NoOpObserver : I'm doing nothing... 2019-04-27 20:57:15.023 WARN 48897 --- [onPool-worker-2] c.t.s.d.o.w.o.WeatherObserverImpl : Weather has changed to Weather(lowest=13, highest=27, condition=Rainy) 2019-04-27 20:57:15.023 INFO 48897 --- [onPool-worker-1] c.t.s.d.o.w.o.LogWeatherObserver : Logging weather change event. Weather(lowest=13, highest=27, condition=Rainy) 2019-04-27 20:57:15.023 INFO 48897 --- [onPool-worker-3] c.t.s.d.o.w.observers.HelloObserver : Hello there. 2019-04-27 20:57:15.023 INFO 48897 --- [nio-8080-exec-2] c.t.s.d.observer.web.WeatherController : Forecast finished.... 2019-04-27 20:57:21.670 INFO 48897 --- [nio-8080-exec-3] c.t.s.d.observer.web.WeatherController : Receiving weather forecast data 2019-04-27 20:57:21.670 INFO 48897 --- [nio-8080-exec-3] c.t.s.d.observer.web.WeatherController : Incoming weather data: Weather(lowest=16, highest=27, condition=Rainy) 2019-04-27 20:57:21.670 INFO 48897 --- [nio-8080-exec-3] c.t.s.d.observer.core.AbstractSubject : Notifying all observers 2019-04-27 20:57:21.671 WARN 48897 --- [nio-8080-exec-3] c.t.s.d.o.w.observers.NoOpObserver : I'm doing nothing... 2019-04-27 20:57:21.671 WARN 48897 --- [nio-8080-exec-3] c.t.s.d.o.w.o.WeatherObserverImpl : Weather has changed to Weather(lowest=16, highest=27, condition=Rainy) 2019-04-27 20:57:21.672 INFO 48897 --- [onPool-worker-2] c.t.s.d.o.w.o.LogWeatherObserver : Logging weather change event. Weather(lowest=16, highest=27, condition=Rainy) 2019-04-27 20:57:21.672 INFO 48897 --- [onPool-worker-2] c.t.s.d.o.w.observers.HelloObserver : Hello there. 2019-04-27 20:57:21.672 INFO 48897 --- [nio-8080-exec-3] c.t.s.d.observer.web.WeatherController : Forecast finished.... 2019-04-27 20:57:27.246 INFO 48897 --- [nio-8080-exec-4] c.t.s.d.observer.web.WeatherController : Receiving weather forecast data 2019-04-27 20:57:27.246 INFO 48897 --- [nio-8080-exec-4] c.t.s.d.observer.web.WeatherController : Incoming weather data: Weather(lowest=16, highest=27, condition=Rainy) 2019-04-27 20:57:27.246 INFO 48897 --- [nio-8080-exec-4] c.t.s.d.observer.web.WeatherController : Forecast finished....
完整代码请参考:https://github.com/david-foss/design-patterns-observer