Java设计模式学习
Auther: Ice Man
StartTime:2022-2-15
Book:Head First 设计模式
前言:编程中会出现的问题
继承
问题: 如果项目中众多子类来继承父类的特性,考虑到有些子类并不需要去复用父类的一些特性,或者父类增加某些特性时,可能会造成某些子类功能的混乱(例如🦆和橡皮鸭的区别)🤔🤔🌚🌚
尝试解决?
采用接口?
接口可以使每个子类去掉用自己需要的特性,避免了子类全部继承父类的特性,但当我们去增加新的子类时,需要去考虑每一个接口是否需要调用,维护项目的时候也大幅增加了工作量,并不是一个很好的方法👎
思考
🌟设计原则🌟:找出应用中可能需要变化之处,把他们独立出来,不要和那些不需要的变化的代码混在一起。
把会变化的部分取出并 “封装” 起来 => 代码变化减少,系统变得有弹性
解决方法
依然用鸭子🦆这个类来举例,我们在设计类的时候,将会飞行和回呱呱叫的鸭子用两个不同的类来描述
🌟设计原则🌟:针对接口编程,而不是针对实现编程
策略模式(Strategy Pattern)
思考:”有一个“ 和 ”是一个“ 之间的区别?😎😎😎
问题:”有一个“ 和 ”是一个“ 之间的区别?😎😎😎
🌟设计模式🌟:多用组合,少用接口
策略模式 定义了算法组,分别封装起来,让他们之间可以互相转换,此模式让算法的变化独立于使用算法的客户
场景:在设计一款复杂的生存游戏的时候,我们有很多函数需要封装,例如仅仅是开枪这一个动作,我们就可以拆分成选择武器、装填武器、瞄准目标和开枪等等多个动作,这样去将一个复杂的实现过程进行拆分,不仅能提高函数的复用性,还能节省后期维护成本
总结
- 仅仅知道OO(面向对象)基础,并不能设计出良好的OO系统
- 良好的OO设计必须具备可复用、可扩充、可维护三个特性
- 模式可以让我们建造出具有良好OO设计质量的系统
- 模式让开发人员之间有共享的语言,能够最大化沟通的价值
- 大多数的模式和原则,都着眼于软件变化的主题
二、观察者模式(Observer)
场景
帮助一个气象公司设计一个系统用来显示当前追踪到的天气状况(温度、湿度、气压),要求所有的数据能够进行实时更新
项目梗概图
需求分析
我们的工作就是建立一个应用,利用设计的系统(WeatherData对象)取得数据,并更新三个公告板:目前状况、气象统计和天气预报
分析已有资源
气象站提供的代码如下所示
前三个数据很明显用于获取实时的气象测量数据,Changed方法是用来检测气象数据,一旦气象测量数据更新,就会调用该方法
/*
* 一旦气象测量更新,此方法就会被调用
*/
public void measurementsChanged() {
// 实现代码
}
需求总结
- WeatherData类具有getter方法可以取得三个测量值:温度、湿度和气压
- 当新的测量数据备妥时,measurementsChanged( )方法就会被调用(不用纠结是如何被调用的)
- 我们需要三个实用天气数据的布告板:“目前状况“布告、“气象统计”布告 和 “天气预告”布告。一旦WeatherData有新的测量,这些布告必须马上更新
- 此系统必须可扩展,让其他开发人员建立定制的布告板,用户可以随性所欲地添加或删除任何布告板。目前初始的布告板有以上三类
错误示范
以下代码有何问题
public class WeatherData {
public void measurementsChanged() {
// 实例变量申明
float temp = getTemperature();
float humidity = getHumidity();
float pressure = getPressure();
currentConditionDisplay.update(temp, humidity, pressure);
statisticsDisplay.update(temp, humidity, pressure);
forecastDisplay.update(temp, humidity, pressure);
}
// 这里是其他WeatherData方法
}
出现问题:
- 我们是针对具体编程,而非针对接口
- 对于每一个新的布告板,我们都得修改代码
- 我们无法在运行时动态地增加或删除布告板
- 我们尚未封装改变的部分
观察者的一天
以上面模型为为例子,主题对象主要管理某些数据,当主题内的数据改变,就会通知观察者
一旦数据改变,新的数据会以某种形式送到观察者手上
狗,猫,老鼠对象都为观察者,可以在主题数据改变时能够收到更新,而鸭子对象不属于观察者对象,所以主题数据改变时不会被通知
观察者模式👀 = 出版者 + 订阅者
1. 订阅主题对象
- 鸭子对象过来告诉主题对象,他想当一个观察者(向主题对象注册【订阅】)
- 鸭子对象现在已经是正式的观察者了
- 当主题对象的数据进行更新,鸭子和其他对象就都会接收到通知:主题已经改变了
2. 退订主题对象
- 老鼠对象要求从观察者中把自己除名
- 主题知道老鼠的请求后,把它从观察者中除名了(老鼠离开了!)
- 主题现在有一个新的整数,出了老鼠之外,每个观察者都会收到新的通知[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
观察者模式的定义
观察者模式:定义了对象之间的一对多依赖,这样一来,当一个对象改变状态时,它的所有依赖者都会收到通知并自动更新
实现观察者模式的方法不只一种,但是以包含Subject与Observer接口的类设计的做法最常见
类图
-
主题接口: 对象使用此接口注册为观察者,或者把自己从观察者中删除
-
观察者接口:所有潜在的观察者必须实现观察者必须实现观察者接口,这个接口只有update( )一个方法,当这个主题状态改变时它该调用
-
具体主题的实现方法:具体主题里除了注册和撤销方法之外,具体主题还实现了notifyObserver( )方法,此方法用于在状态改变时更新所有当前观察者
具体主题也可能有设置和获取状态的方法
-
观察者的实现方法:可以是实现该接口的任意类,观察者必须注册具体主题,以便接收更新
松耦合
当两个对象之间松耦合,他们依然可以交互,但是不太清楚彼此的细节
🌟设计原则🌟: 为了交互对象之间的松耦合设计而努力
任何时候我们都可以增加新的观察者。因为主题唯一依赖的东西是一个实现Observer接口的对象列表,所以我们可以随意增加观察者。事实上,在运行时我们可以用新的观察者来取代现有的观察者,主题不会收到任何影响。同样的,也可以在任何时候删除某些观察者
有新类型的观察者出现时,主题的代码不需要修改。如果我们有个新的具体类需要当观察者,我们不需要为兼容新类型而修改主题代码的代码,所有要做的就是在新的类里实现此观察者接口,然后注册成为观察者即可。主题不在乎别的,他只会发送通知给所有实现了观察者接口的对象
我们可以独立复用主题或者观察者。如果我们在其他地方需要使用主题或者观察者,可以轻易复用,应为二者并为紧耦合
改变主题或观察者的任何一方,并不会影响另一方。因为二者是松耦合,所以只要他们之间的接口仍被遵循,我们就可以自由地改变他们
松耦合的设计之所以能让我们建立有弹性的OO系统,能够应对变化,是因为对象之间的互相依赖降到了最低
实现项目需求
思考一:现在让我们回到气象站项目,那我们该如何去用观察者模式来实现功能?
分析模式:观察者模式定义了对象之间的**“一对多”**的依赖,而在这个项目中WeatherData类正是“一”,“多”就是使用天气观测的各种布告板
思考二:解决了模型应用场景后,那怎么将气象预测值放到布告板上?
分析模式:观察者模式中有两个对象,主题对象和观察者对象,如果将WeatherData对象作为主题,把布布告板作为观察者,布告板为了取得信息,就必须先向WeatherData对象注册
思考三:每一个布告板都会去展示不一样的数据,怎么做到代码的通用性?
分析模式:这正是我们需要一个共同的接口的原因,尽管布告板的类都尽不相同,但是它们都应该实现相同的接口,好让WeatherData对象能够知道如何把观测值送给他们,所以有一个update( )方法在所有的布告板都实现的共同接口里定义
设计气象站
设计图如下
实现气象站
Java为观察者模式提供了许多内置的支持,这里先不使用,而是先自己从头开始写首先从创建接口开始
实例代码: http://gitlab.code-nav.cn/13706531210/observerpattern.git
1. 创建接口
// 主题接口
public interface Subject {
public void registerObserver(Observer o); // 注册观察者
public void removeObserver(Observer o); // 移除观察者
public void notifyObserver(); // 当主题状态发生改变时,这个方法会被调用,来通知所有观察者
}
// 观察者接口
public interface Observer {
public void update(float temp, float humidity, float pressure); // 传递气象状态值给观察者
}
// 布告板展示接口
public interface DisplayElement {
public void display(); // 当布告板需要展示时,调用此方法
}
思考点:直接把观测值传入观察者这个方法是否明智?(暗示:这些观测值的种类和个数在未来有可能改变,改变后是否能够很好地封装,或者是否需要修改许多代码才能办到?)
在实现基础方法后,会根据这个问题进行优化
2. 在WeatherData中实现主题接口
import interfaces.Observer;
import interfaces.Subject;
import java.util.ArrayList;
public class WeatherData implements Subject {
private ArrayList observers; // 注册的观察者
private float temperature; // 温度
private float humidity; // 湿度
private float pressure; // 气压
public WeatherData() {
observers = new ArrayList<>();
}
/**
* 注册观察者
*/
@Override
public void registerObserver(Observer o) {
observers.add(o);
}
/**
* 移除观察者
*/
@Override
public void removeObserver(Observer o) {
int i = observers.indexOf(0);
if (i >= 0) {
observers.remove(i);
}
}
/**
* 将状态告诉每一个观察者
*/
@Override
public void notifyObserver() {
for (int i = 0; i < observers.size(); i++) {
Observer observer = (Observer) observers.get(i);
observer.update(temperature, humidity, pressure);
}
}
/**
* 当从气象站得到更新观测值时,我们通知观察者
*/
public void measurementsChanged() {
notifyObserver();
}
public void setMeasurements(float temperature, float humidity, float pressure) {
this.temperature = temperature;
this.humidity = humidity;
this.pressure = pressure;
}
// WeatherData的其他方法
}
3. 创建布告板
布告板的逻辑都十分类似,这里就先实现其中一种,其他的布告板可以自己去定义编写
public class CurrentConditionsDisplay implements Observer, DisplayElement {
private float temperature;
private float humidity;
private Subject weatherData;
/**
* 注册观察者
*/
public CurrentConditionsDisplay(Subject weatherData) {
this.weatherData = weatherData;
weatherData.registerObserver(this);
}
@Override
public void update(float temperature, float humidity, float pressure) {
// 把最近的温度和湿度保存并调用display
this.temperature = temperature;
this.humidity = humidity;
display();
}
/**
* 把最近的温度和湿度显示出来
*/
@Override
public void display() {
System.out.println("Current conditions: " + temperature
+ "F degrees and " + humidity + "% humidity");
}
}
4. 启动气象站
现在先建立一个测试程序
public class WeatherStation {
public static void main(String[] args) {
WeatherData weatherData = new WeatherData();
CurrentConditionsDisplay currentDisplay = new CurrentConditionsDisplay(weatherData);
// 模拟新的气象数据
weatherData.setMeasurements(80, 65, 30.4f);
currentDisplay.display();
weatherData.setMeasurements(82, 70, 29.2f);
currentDisplay.display();
weatherData.setMeasurements(78, 90, 29.2f);
currentDisplay.display();
}
}
展示程序运行结果
模式思考
思考一:为什么需要主题去发送更新信息,而不是让每个观察者去索取数据?🤔️
😎如果采用观察者去主动索取数据,就意味着主题类需要门户大开,这样主题就暴露在所有观察者面前了,这样十分危险
思考二:主题为什么不开放一些getter方法让观察者去获取它们所需要的数据
😎通过主题直接推送信息可以减少观察者的调用次数,但每个观察者也并不是需要主题里所有的数据,get方法可以避免观察者获取不需要的信息
😎直接推送所有消息和观察者get部分数据两种方法各有优点,Java内置的Observer模式两种方法都支持
使用Java内置的观察者模式
上面通过自己写的观察者模式的代码还是比较繁琐,Java Util包中提供了最基本的Observer接口与Observable类,里面许多功能都已经实现准备好了,可以使用推(push)和拉(pull)的方式传输数据。通过Java内置的观察者,我们可以将结构图进行简化
1. 如何把对象变成观察者
如同以前一样,实现观察者接口(java.util.Observer),然后调用任何Observable对象的addObserver( )方法。不想再当观察者时,调用deleteObserver( )方法就可以了
2. 观察者如何送出通知
- 先利用java.util.Observable接口产生 “可观察者“ 类
- 调用setChanged( )方法,标记状态已经改变的事实
- 调用两种notifyObservers( )方法中的一个:notifyObservers 或 notifyObservers(Object arg)
3. 观察者如何接受通知
同以前一样,观察者实现了更新的方法,但是方法的签名不太一样
update(Observable o, Object org)
如果你想 ”推“(pull)数据给观察者,你可以把数据当作数据对象传送给notifyObservers(arg)方法。否则观察者就必须从可观察者对象中“拉”(pull)数据。
4. setChanged()方法
Java内置的观察者比我们之前编写的代码多了setChanged( )方法,现在单独把setChanged方法拉出,以来了解这一切
setChanged() {
changed = true;
}
notifyObservers(Object arg) {
if (changed) {
for every observer on the list {
call update(this, arg)
}
changed = false;
}
}
public void notifyObservers() {
notifyObservers(null);
}
利于Java内置的支持重做气象站
1. 把WeatherData改成使用java.util.Observable
public class WeatherDataByJavaUtil extends Observable {
private float temperature; // 温度
private float humidity; // 湿度
private float pressure; // 气压
public WeatherDataByJavaUtil() { } // 构造器不再需要为了记住观察者们而建立数据结构了
public void measurementsChanged() {
// 我们没有调用notifyObservers()传送数据对象,所以这里需要设置一下changed值
setChanged();
notifyObservers();
}
public void setMeasurements(float temperature, float humidity, float pressure) {
this.temperature = temperature;
this.humidity = humidity;
this.pressure = pressure;
measurementsChanged();
}
public float getTemperature() {
return temperature;
}
public float getHumidity() {
return humidity;
}
public float getPressure() {
return pressure;
}
}
以上中的三个get方法并不是新方法,只是因为要使用 ”拉“,而特意再写一遍来强调。观察者会利用这些方法取得WeatherData对象的状态
⚠️注意:这里没有调用notifyObservers( )来传送数据,说明是由观察者对象向主题“拉”数据
2. 重写CurrentConditionsDisplay
public class CurrentConditionsDisplay implements Observer, DisplayElement {
Observable observable;
private float temperature;
private float humidity;
public CurrentConditionsDisplay(Observable observable) {
this.observable = observable;
observable.addObserver(this);
}
@Override
public void display() {
System.out.println("Current conditions: " + temperature
+ "F degrees and " + humidity + "% humidity");
}
@Override
public void update(Observable obs, Object arg) {
if (obs instanceof WeatherData) {
WeatherData weatherData = (WeatherData) obs;
this.temperature = weatherData.getTemperature();
this.humidity = weatherData.getHumidity();
display();
}
}
}
构造器中需要一个Observable当参数,并将当前CurrentConditionsDisplay对象登记成为观察者
运行后发现运行结果与之前自己写的如出一辙
Observable的缺陷
- Java自带的Observable并不是一个接口,而是一个类,这似乎违反了我们OO设计原则中的针对接口编程,更糟糕的是Observable类中甚至没有一个实现接口。因此java.util.Observable的实现有许多问题,限制了它的使用和复用,但这并没有影响它应有的功能
- 从Observable是一个类出发,我们又能引出两个问题
- Observable是一个类,你必须设计一个类去继承它。但如果某个类相同时具有Observable类和其他另一个超类的行为,就会陷入两难,限制了Observable的复用潜力
- 因为没有Observable接口,所以无法建立自己的实现,和Java内置的Observer API搭配使用,也无法将java.util的实现换成另一套方法实现
- Observable中将setChanged方法保护起来了,这意味着除非去继承Observable,否则无法创建Observable实例并组合到自己的对象中来。这违反了设计原则第二点:多用组合,少用继承
总结
- 观察者模式定义了对象之间一对多的关系
- 主题(也就是可观察者)用一个共同的接口来更新观察者
- 观察者和可观察者之间用松耦合方式结合
- 使用观察者模式,你可以从被观察者处推(push)或者拉(pull)的方式获取数据(建议使用推的形式)
- 有多个观察者时,不可以依赖特定的通知次序
- Java有多种观察者模式的实现,包括通用的java.util.Observable
- 注意Observable类上的缺陷
- 如果有必要的话,建议实现自己的Observable
- 此模式应用于许多地方,例如:JavaBeans、RMI
三、装饰者模式
场景
星巴兹(Starbuzz)是一家以扩张速度最快而闻名的咖啡连锁店,因为吸纳在