好消息,你们公司和气象站签订了一笔500W的项目,老板说只要顺利搞定了这个项目,奖励项目组成员到三亚晒太阳(6月的天…)。
需求很简单,气象站会搜集湿度、温度、气压等气象数据,然后放到一个叫WeatherData对象中,现在需要我们来监控WeatherData对象,当里面的气象数据发生变更后,要将变更显示到布告板上,而布告板目前需要提供的有"目前状态"布告板,"气象统计"布告板,"天气预报"布告板;同时系统要提供布告板的可扩展性,即可以非常容易的添加新的布告板,或删除已有的布告板。
先看看WeatherData类长什么样子,这是由气象站提供的。
/**
* 天气数据
* 由气象站提供的类
*/
public class WeatherData {
private float temperature; //温度
private float humidity; //湿度
private float pressure; //气压
/**
* 当新的气象数据准备妥当时,会调用这个方法
* 此时就需要我们来更新布告板信息。
*/
public void measurementsChanged(){
//这儿就是我们需要去实现的代码。
}
public float getTemperature() {
return temperature;
}
public void setTemperature(float temperature) {
this.temperature = temperature;
}
public float getHumidity() {
return humidity;
}
public void setHumidity(float humidity) {
this.humidity = humidity;
}
public float getPressure() {
return pressure;
}
public void setPressure(float pressure) {
this.pressure = pressure;
}
}
气象数据发生变更后,会调用measurementsChanged()这个方法(我们不用管是怎么调用的),我们需要实现这个方法,来通知布告板更新。
需求清楚后,我们就开始来设计吧。既然是measurementsChanged()方法要变更布告板信息,那我们在里面直接调用布告板的相关方法不就行了吗。
基于这样的思路,我们来实现V1版本。布告板有多种类型,通过OO思想,我们非常容易可以抽象出来一个布告板的抽象出来。
/**
* 布告板抽象
*/
public interface DisplayV1 {
/**
* 更新布告板
* @param data 气象数据
*/
void update(WeatherDTOV1 data);
}
数据对象WeatherDTOV1
/**
* 气象数据封装
*/
public class WeatherDTOV1 {
private float temperature; //温度
private float humidity; //湿度
private float pressure; //气压
public WeatherDTOV1(float temperature, float humidity, float pressure) {
this.temperature = temperature;
this.humidity = humidity;
this.pressure = pressure;
}
public float getTemperature() {
return temperature;
}
public void setTemperature(float temperature) {
this.temperature = temperature;
}
public float getHumidity() {
return humidity;
}
public void setHumidity(float humidity) {
this.humidity = humidity;
}
public float getPressure() {
return pressure;
}
public void setPressure(float pressure) {
this.pressure = pressure;
}
@Override
public String toString() {
final StringBuffer sb = new StringBuffer("WeatherDTOV1{");
sb.append("temperature=").append(temperature);
sb.append(", humidity=").append(humidity);
sb.append(", pressure=").append(pressure);
sb.append('}');
return sb.toString();
}
}
实现需求需要的三个布告板
/**
* 目前状况布告板
*/
public class CurrentConditionsDisplayV1 implements DisplayV1 {
@Override
public void update(WeatherDTOV1 data) {
System.out.println("目标状况布告板更新,新数据:"+data.toString());
}
}
/**
* 气象统计布告板
*/
public class StatisticsDisplayV1 implements DisplayV1 {
@Override
public void update(WeatherDTOV1 data) {
System.out.println("气象统计布告板更新,新数据:"+data.toString());
}
}
/**
* 天气预报布告板
*/
public class ForecastDisplayV1 implements DisplayV1 {
@Override
public void update(WeatherDTOV1 data) {
System.out.println("天气预报布告板更新,新数据:"+data.toString());
}
}
最后,我们在WeatherDataV1的measurementsChanged()方法中,依次调用布告板更新信息。
/**
* 当新的气象数据准备妥当时,会调用这个方法
* 此时就需要我们来更新布告板信息。
*/
public void measurementsChanged(){
//这儿就是我们需要去实现的代码。
WeatherDTOV1 weatherDTOV1 =
new WeatherDTOV1(getTemperature(), getHumidity(), getPressure());
CurrentConditionsDisplayV1 currentConditionsDisplay = new CurrentConditionsDisplayV1();
currentConditionsDisplay.update(weatherDTOV1);
StatisticsDisplayV1 statisticsDisplay = new StatisticsDisplayV1();
statisticsDisplay.update(weatherDTOV1);
ForecastDisplayV1 forecastDisplay = new ForecastDisplayV1();
forecastDisplay.update(weatherDTOV1);
}
实现了我们的需求,可是没福利。
分析V1版本设计会存在什么问题?
这个场景其实是一个典型使用观察者模式的一个场景。
我们举个报纸的订阅例子,来理解下什么是观察者模式(也叫发布订阅模式)。
彭城晚报是一家报社出版的报纸,老周已经订阅了彭城晚报,那么每当有新的晚报出版后,都会送到老周手上。
当老周不再看晚报后,他可以取消订阅,这样报社就不会再给他送报纸了。只要这家报社还在,就一直会有人不断的订阅或者取消订阅报纸。
该场景对应到观察者模式中,报社称之为"主题",老周就是"观察者",观察者(老周)可以订阅(注册)到主题上,这样当主题状态发生变更时(出版新报纸),就会通知到已经订阅的观察者(老周)。观察者(老周)也可以取消订阅。
这样我们就可以抽象出主题对象,里面会有注册、取消注册观察者的方法,以及通知观察者的方法;而观察者有更新自己状态方法。
好了,介绍完观察者是什么之后,用观察者来实现我们的天气项目。
观察者抽象(V1版本一致)
/**
* 布告板抽象(就是观察者抽象,有一个更新自己状态的方法)
*/
public interface DisplayV2 {
/**
* 更新布告板
* @param data 气象数据
*/
void update(WeatherDTOV1 data);
}
观察者的实现同V1版本一样,就不用重复贴代码了。
主题抽象,有注册观察者、移除观察者、通知观察者的方法。
/**
* 主题抽象
*/
public interface Subject {
/**
* 注册观察者(布告板)
* @param display
*/
void registerDisplay(DisplayV2 display);
/**
* 移除观察者(布告板)
* @param display
*/
void removeDisplay(DisplayV2 display);
/**
* 通知观察者(布告板)
*/
void notifyDisplay();
}
主题实现,让WeatherDataV2实现主题接口。
/**
* 天气数据
* 由气象站提供的类
*/
public class WeatherDataV2 implements Subject{
private float temperature; //温度
private float humidity; //湿度
private float pressure; //气压
private List<DisplayV2> displays = new ArrayList<>(5); //布告板列表(观察者列表)
/**
* 当新的气象数据准备妥当时,会调用这个方法
* 此时就需要我们来更新布告板信息。
*/
public void measurementsChanged(){
//这儿就是我们需要去实现的代码。
notifyDisplay();
}
@Override
public void registerDisplay(DisplayV2 display) {
displays.add(display);
}
@Override
public void removeDisplay(DisplayV2 display) {
displays.remove(display);
}
@Override
public void notifyDisplay() {
WeatherDTOV2 weatherDTO =
new WeatherDTOV2(getTemperature(), getHumidity(), getPressure());
displays.forEach(display -> display.update(weatherDTO));
}
//getter setter 略
}
来吧,测试一波看看
/**
* V2版测试
*/
public class WeatherMainV2 {
public static void main(String[] args) {
WeatherDataV2 weatherData = new WeatherDataV2();
//注册观察者
weatherData.registerDisplay(new CurrentConditionsDisplayV2());
weatherData.registerDisplay(new ForecastDisplayV2());
weatherData.registerDisplay(new StatisticsDisplayV2());
//气象数据发生变更
weatherData.setTemperature(34.5f);
weatherData.setHumidity(125.7f);
weatherData.setPressure(32.4f);
//调用我们实现的方法。通知观察者
weatherData.measurementsChanged();
}
}
nice,非常完美。
使用观察者模式能不能解决V1版本的问题呢,首先Subject 和 WeatherDataV2对象只依赖 观察者抽象(DisplayV2),不依赖具体的观察者实现。实际上这就是依赖倒置原则的体现。
其次,现在我们要新增观察者时,只需要新提供一个观察者的实现(扩展类),然后注册到WeatherDataV2中即可,同理移除观察者,只需要取消注册即可,符合开闭原则。
nice,三亚半月游,妥妥的。
对于气象站这个项目,我们没有测试移除观察者,你可以自行实现体验下看看(注意的移除实现可能存在问题哦,你可以自己发现并解决)。
观察者模式的定义,观察者模式定义了对象之前的一对多的关系,这样,当一的对象状态发生变化时,多的对象会接受到通知并更新。
我们用代码实现下
抽象观察者
/**
* 抽象观察者
*/
public interface Observer {
/**
* 更新方法
* @param event 事件信息
*/
void update(Event event);
}
它通常会依赖一个数据对象,而且我们也常常叫它事件对象。
/**
* 事件(封装数据)
*/
public class Event {
}
具体观察者实现
/**
* 具体观察者A
*/
public class ConcreteObserverA implements Observer{
@Override
public void update(Event event) {
System.out.println("具体观察者A接收到事件,更新......");
}
}
/**
* 具体观察者B
*/
public class ConcreteObserverB implements Observer{
@Override
public void update(Event event) {
System.out.println("具体观察者B接收到事件,更新......");
}
}
抽象主题,依赖抽象观察者,提供注册、移除、通知接口。
/**
* 抽象主题
*/
public interface Subject {
/**
* 注册观察者
* @param observer
*/
void registerObserver(Observer observer);
/**
* 移除观察者
* @param observer
*/
void removeObserver(Observer observer);
/**
* 通知观察者
*/
void notifyObservers();
}
具体主题
/**
* 具体主题
*/
public class ConcreteSubject implements Subject {
private List<Observer> observers = new ArrayList<>();
@Override
public void registerObserver(Observer observer) {
observers.add(observer);
}
@Override
public void removeObserver(Observer observer) {
observers.remove(observer);
}
@Override
public void notifyObservers() {
Event event = new Event();
observers.forEach(observer -> observer.update(event));
}
}
测试一波
/**
* 观察者测试
*/
public class ObserverMain {
public static void main(String[] args) {
Subject subject = new ConcreteSubject();
subject.registerObserver(new ConcreteObserverA());
subject.registerObserver(new ConcreteObserverB());
subject.notifyObservers();
}
}
观察者模式的UML图
因为观察者模式非常的常用,JDK已经内置了观察者模式。
其抽象主题是java.util.Observable,它是一个抽象类,实现了添加、删除、通知 观察者的实现。
抽象观察者是java.util.Observer,它是一个接口,定义了更新方法。
你可以改造我们的V2版本,使用JDK内置的观察者模式来实现。
但通常我们很少使用内置的观察者模式,这是因为它的主题对象是一个抽象类,要使用它的功能时,必须继承它,
因为java语法不支持多继承,当类已经继承其他超类时就无能为力了。另外Observable将一些关键的方法设置为protected,这意味着只能通过继承来复用,而不能通过组合Observable,来使用其中的一些关键方法,违反了多用组合,少用继承的原则。
为了加深理解,我们再来实现一个微信公众号的场景。(https://codepumpkin.com/observer-design-pattern/)
有一个叫"设计模式"的微信公众号,假如有张三,李四,王五三个同学关注,那么每当公众号推送新的文章时,都会及时通知这3位观察者。
某天,张三取消关注,那么之后推送的新文章,张三不会再收到。
同样的,定义抽象观察者(粉丝)
/**
* 抽象观察者
*/
public interface Observer {
/**
* 更新方法
* @param event 事件信息
*/
void update(Event event);
}
依赖的事件对象
/**
* 事件(封装数据)
*/
public class Event {
private String officialAccountName;
private String articleName;
public Event(String officialAccountName, String articleName) {
this.officialAccountName = officialAccountName;
this.articleName = articleName;
}
//setter getter 忽略
}
具体观察者实现
/**
* 粉丝
*/
public class Follower implements Observer {
private String name;
public Follower(String name) {
this.name = name;
}
@Override
public void update(Event event) {
System.out.println(getName() + ",接受到公众号"+ event.getOfficialAccountName() +"推送的新文章:" + event.getArticleName());
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
抽象主题
/**
* 抽象主题
*/
public interface Subject {
/**
* 注册观察者
* @param observer
*/
void registerObserver(Observer observer);
/**
* 移除观察者
* @param observer
*/
void removeObserver(Observer observer);
/**
* 通知观察者
*/
void notifyObservers();
}
具体主题(公众号)
/**
* 公众号(具体主题)
*/
public class OfficialAccount implements Subject {
private String name;
private List<Observer> observers;
private String lastArticle;
public OfficialAccount(String name) {
this.name = name;
observers = new ArrayList<>();
}
@Override
public void registerObserver(Observer observer) {
observers.add(observer);
System.out.println(name + "公众号新增粉丝...");
}
@Override
public void removeObserver(Observer observer) {
observers.remove(observer);
System.out.println(name + "公众号掉粉...");
}
@Override
public void notifyObservers() {
Event event = new Event(name, lastArticle);
observers.forEach(observer -> observer.update(event));
}
public void pushArticle(String articleName){
this.lastArticle = articleName;
notifyObservers();
}
}
测试一波
/**
* 公众号测试
*/
public class OfficialAccountMain {
public static void main(String[] args) {
OfficialAccount account = new OfficialAccount("设计模式");
Observer zhangsan = new Follower("张三");
account.registerObserver(zhangsan);
account.registerObserver(new Follower("李四"));
account.registerObserver(new Follower("王五"));
System.out.println("推送新文章。。。");
account.pushArticle("观察者模式");
account.removeObserver(zhangsan);
System.out.println("推送新文章。。。");
account.pushArticle("策略模式");
}
}
测试结果:
设计模式公众号新增粉丝...
设计模式公众号新增粉丝...
设计模式公众号新增粉丝...
推送新文章。。。
张三,接受到公众号设计模式推送的新文章:观察者模式
李四,接受到公众号设计模式推送的新文章:观察者模式
王五,接受到公众号设计模式推送的新文章:观察者模式
设计模式公众号掉粉...
推送新文章。。。
李四,接受到公众号设计模式推送的新文章:策略模式
王五,接受到公众号设计模式推送的新文章:策略模式
https://gitee.com/cq-laozhou/design-pattern