相关源码:
访问官网https://www.wickedlysmart.com/head-first-design-patterns/
Github Java8对其改写 https://github.com/bethrobson/Head-First-Design-Patterns
入门
从代码复用,到经验复用。
先从简单的模拟鸭子应用说起
设计标准OO技术,设计一个鸭子超类SuperClass
如果想做个能飞的鸭子,怎么办?直接在超类中添加fly()方法肯定不行,所有的鸭子都会飞了;利用接口flyable的话,其实有些不会叫,需要另外quarkable接口,又需要被迫检查并可能需要覆盖fly()和quark()等接口。需要一个更清晰的方法,让某些(而不是全部)鸭子类型可飞或可叫。
软件开发的不变真理
不管设计的多好,一段时间后,总是需要成长或改变,否则就会“死亡”。要么客户需要新功能,要么决定用其他数据库产品等等。
因此,第一个设计原则:找出应用中可能需要变化之处,把他们独立出来,不要和不需要变化的代码混在一起。
把会变化的部分取出并“封装”起来,好让其他部分不会受到影响。结果:代码变化引起的不经意后果变少,系统更加有弹性。
分开变化和不变部分
因为fly()和quark()会随着鸭子的不同而改变,需要把这两个行为从Duck类分开,建立一组新类代表每个行为。
设计鸭子的行为
我们希望有弹性,毕竟最开始就因为鸭子行为没有弹性,导致现在的思考。
第二个设计原则:针对接口编程,而不是针对实现编程
鸭子的行为将被放到分开的类中,此类专门提供某行为接口的实现。这样,鸭子类就不需要知道行为的实现细节。
利用接口代表每个行为,如FlyBehavior和QuackBehavior,而行为的每个实现都将实现其中的一个接口。
针对接口编程的真正意思是“针对超类型(supertype)编程”
实现鸭子的行为
有两个接口FlyBehavior和QuackBehavior,还有对应的类,负责实现具体的行为
如FlyWithWings类和FlyNoWay类,Quack类(呱呱叫)、Squeak类(吱吱叫)和MuteQuack类(不会叫)
这样的设计,可以让飞行和呱呱叫的动作被其他对象复用,行为和对象分离。
整合鸭子的行为
鸭子现在将飞行和呱呱叫的动作委托(delegate)别人处理,而不是使用定义在Duck类(或子类)内的呱呱叫和飞行方法。
做法:
首先,在Duck类中“加入两个实例变量”
分别是“flyBehavior”和“quackBehavior”,声明为接口类型(而非具体类实现类型),每个鸭子对象都会动态的设置这些变量以在运行时引用正确的行为类型,如FlyWithWings等
需要将Duck类和所有子类的fly()和quack()方法删除,替换为performFly()和performQuack()
实现performQuack()
public class Duck {
FlyBehavior flyBehavior;
QuackBehavior quackBehavior; // 每只鸭子都会引用实现QuackBehavior接口的对象
public void performQuack(){
quackBehavior.quack(); // 鸭子对象不亲自处理呱呱叫行为,而是委托给quackBehavior引用的对象
}
public void performFly(){
flyBehavior.fly(); // 同上,委托给flyBehavior引用的对象
}
}
更多的整合
public class MallardDuck extends Duck {
// 继承Duck类,具有flyBehavior和quackBehavior实例变量
public MallardDuck(){
quackBehavior = new Quack(); // 叫的职责委托给Quack对象,得到真正的呱呱叫
flyBehavior = new FlyWithWings(); // 使用FlyWithWings作为FlyBehavior类型
}
public void display(){
System.out.println("我是一头绿头鸭子");
}
}
小结
虽然我们把行为设定为具体的类(通过实例化类似Quack或FlyWithWings的行为类,并把它指定到行为引用变量中),但还可以在运行时“轻易地”改变它。
所以,目前的做法比较有弹性,只是初始化实例变量的做法不够弹性。因为quackBehavior的实例变量是个接口类型,能在运行时通过多态动态制定不同的实现类。
动态设定行为
假设想在鸭子子类中通过setter method设定鸭子行为,而不是在鸭子的构造器内实例化
在Duck类中,加入两个新方法
public abstract class Duck {
FlyBehavior flyBehavior;
QuackBehavior quackBehavior; // 每只鸭子都会引用实现QuackBehavior接口的对象
public void performQuack(){
quackBehavior.quack(); // 鸭子对象不亲自处理呱呱叫行为,而是委托给quackBehavior引用的对象
}
public void performFly(){
flyBehavior.fly(); // 同上,委托给flyBehavior引用的对象
}
public abstract void display();
public void setFlyBehavior(FlyBehavior fb){
flyBehavior = fb;
}
public void setQuackBehavior(QuackBehavior qb){
quackBehavior = qb;
}
}
制作一个鸭子新类型:鸭子模型
public class ModelDuck extends Duck {
public ModelDuck(){
flyBehavior = new FlyNoWay(); // 一开始不会飞
quackBehavior = new Quack();
}
@Override
public void display() {
System.out.println("我是一个鸭子模型");
}
}
建立一个新的FLyBehavior类型
public class FlyRocketPowered implements FlyBehavior {
@Override
public void fly() {
System.out.println("飞的像火箭一样快。。。");
}
}
改变测试类,加上模型鸭子,并使其具有火箭动力
public class MiniDuckSimulator {
public static void main(String[] args) {
Duck model = new ModelDuck();
model.performFly(); // 不会飞。。。
model.setFlyBehavior(new FlyRocketPowered());
model.performFly(); // 飞的像火箭一样快。。。
}
}
在运行时想要改变鸭子的行为,只需要调用setter方法即可。
封装行为的大局观
现在来看整体的格局
整个重构之后的类结构,鸭子继承Duck,飞行行为实现FlyBehavior接口,呱呱叫行为实现QuackBehavior接口
描述事情的方式也要改变,不再将鸭子的行为说成“一组行为”了,而是“一族算法”。算法代表鸭子能做的事情,这样也很容易用于一群类计算不同地区的交易税金。
注意类之间的“关系”。关系可以是IS-A(是一个)、HAS-A(有一个)和IMPLEMENT(实现)。
“有一个”可能比“是一个”更好
“有一个”的关系相当有趣:每个鸭子都有一个FlyBehavior和一个QuackBehavior,好将飞行和呱呱叫委托给他们代为处理。
当你将这两个类结合起来使用,如本例,就是组合(composition),和继承的区别是,鸭子的行为不是继承来的,而是和适当的行为对象“组合”来的。
涉及到第三个设计原则:多用组合,少用继承。
总结-策略模式
刚学到的就是第一个设计模式,也就是“策略模式(strategy pattern)”。
正式定义:策略模式定义了算法族,分别封装起来,让他们之间可以相互替换,此模式让算法的变化独立于使用算法的客户。
后记
共享模式词汇作用
- 威力强大,交流的不仅是模式名称,而是一整套模式背后所象征的质量、特性、约束
- 让你用更少的词汇做更充分的沟通,让其他开发人员更容易了解你对设计的想法
- 将说话的方式保持在模式层次,让你待在“设计圈子”久一些。不会被压低到对象与类这种琐碎的事情上
- 共享词汇帮你的团队快速充电 彼此对设计的看法不容易产生误解
- 共享词汇帮助初级开发迅速成长
“我们使用策略模式实现鸭子的各种行为”这句话就告诉我们,鸭子的行为被封装到一组类中,可以被轻易地扩展和改变,如果需要,甚至在运行时也可以改变行为。
设计模式比库的等级更高,告诉我们如何组织类和对象以解决某种问题,采纳这些设计并使他们适合
良好的OO设计必须具备可复用、可扩充、可维护三个特性。
观察者模式
观察者模式是JDK中使用最多的模式之一。让你的对象知悉现状。
需求
为Weather-O-Rama气象站建立下一代Internet气象观测站,建立在对方的WeatherData对象上,由WeatherData对象负责追踪目前的天气状况(温度、湿度、气压)应用有三个布告板,分别显示目前的状况、气象统计和简单的预报。当WeatherObject对象获取最新的测量数据时,三种公告板必须实时更新。
而且可扩展,Weather-O-Rama气象站希望公布一组API,好让其他开发人员可以写出自己的气象公告板,并插入此应用中。
盈利模式:客户使用每个公告板都要付钱。
气象监测应用的概况
该系统的三个部分是气象站(获取实际气象数据的物理装置)、WeatherData对象(追踪来自气象站的数据,并更新布告板)和布告板(显示目前天气状况给用户看)
Weather对象知道如何跟物理气象站联系,以取得更新的数据。WeatherData对象会随即更新三个布告板的显示:目前状况(温度、湿度、气压)、气象统计和天气预报。
我们的工作就是:建立一个应用,利用WeatherData对象取得数据,并更新三个布告板:目前状况、气象统计和天气预报。
看WeatherData类
目前所知
需要搞懂我们该做什么
- WeatherData类具有getter方法,可以取得三个测量值:温度、湿度和气压
- 当新的测量数据备妥时,measurementsChanged()方法被调用
- 需要实现三个使用天气数据的布告板:目前状况布告、气象统计布告和天气预报布告。一旦WeatherData有新的测量,这些布告必须马上更新
- 此系统可扩展,让其他开发人员建立定制的布告板,用户可随心所欲的增删布告板。
首先想到的实现
public class WeatherData {
// 实例变量的声明。。。
public void measurementsChanged(){
float temp = getTemperature(); // 获取最新值
float humidity = getHumidity();
float pressure = getPressure();
currentConditionsDisplay.update(temp,humidity,pressure);
statisticsDisplay.update(temp,humidity,pressure);
forecastDisplay.update(temp,humidity,pressure);
}
// 其他WeatherData方法
}
缺点:
- 针对具体实现编程,而非针对接口
- 对于每个新的布告板,都需要修改代码
- 无法在运行时动态增删布告板
- 未封装改变的部分
认识观察者模式
出版者+订阅者=观察者模式,出版者被称为主题Subject,订阅者被称为“观察者”Observer
现实世界的定义:观察者模式定义了对象之间的一对多依赖,这样一来,当一个对象改变状态时,它的所有依赖者都会收到通知并自动更新。
类图:
和一对多关系的关联
主题是具有状态的对象,并且可以控制这些状态。也就是说,有“一个”具有状态的主题。此外,观察者使用这些状态,虽然这些状态不属于他们。许多的观察者,依赖主题来告知状态何时改变。形成“一个”主题对“多个”观察者的关系。
依赖如何产生
因为主题是数据的拥有者,观察者是主题的依赖者,在数据变化时更新。这样比起让很多对象控制同一份数据,可以得到更干净的OO设计。
松耦合的威力
当两个对象之间松耦合,依然可以交互,但不太清楚彼此的细节。
观察者模式提供一种对象设计,让主题和观察者之间松耦合。
为什么?
关于观察者的一切,主题只知道观察者实现了某个接口(Observer接口)。不需要知道观察者的具体类是谁,做了什么或其他任何细节。
任何时候都可以增加新的观察者,因为主题唯一依赖的是一个实现Observer接口的对象列表。事实上,在运行时可以用新的观察者取代现有的观察者,主题不会受到任何影响。同样,也可以任何时候删除某些观察者。
有新类型的观察者出现,主题的代码不用修改,只需要新类实现观察者接口即可。可以独立复用主题或观察者。二者并非紧耦合。
设计原则:为了交互对象之间的松耦合设计而努力。
松耦合的设计之所以能让我们建立有弹性的OO系统,能够应对变化,是因为对象之间的互相依赖降到了最低。
实现气象站
java为观察者模式提供内置的支持,暂时不用,自己建立更具有弹性,也并不麻烦。
首先建立接口
public interface Subject {
public void registerObserver(Observer o);
public void removeObserver(Observer o);
public void notifyObservers();
}
public interface Observer {
public void update(float temp,float humidity,float pressure);
}
public interface DisplayElement {
public void display();
}
在WeatherData中实现主题接口
public class WeatherData implements Subject {
private ArrayList observers;
private float temperature;
private float humidity;
private float pressure;
public WeatherData(){
observers = new ArrayList();// ArrayList在构造器中建立
}
@Override
public void registerObserver(java.util.Observer o) {
observers.add(o);
}
@Override
public void removeObserver(java.util.Observer o) {
int i = observers.indexOf(o);
if(i>0){
observers.remove(i);
}
}
@Override
public void notifyObservers() { // 将状态告诉每一个观察者,因为都实现了update
for (int i = 0; i < observers.size(); i++) {
Observer observer = (Observer)observers.get(i);
observer.update(temperature,humidity,pressure);
}
}
// 从气象站得到更新观测值,通知观察者
public void measurementsChanged(){
notifyObservers();
}
// 可以写代码从天气网抓取观测值
public void setMeasurements(float temperature,float humidity,float pressure){
this.temperature = temperature;
this.humidity = humidity;
this.pressure = pressure;
measurementsChanged();
}
// WeatherData的其他方法
}
建立布告板
先写目前状况布告板
public class CurrentConditionsDisplay implements Observer, DisplayElement {
private float temperature;
private float humidity;
private Subject weatherData;
// 构造器需要weatherData对象(也就是主题)作为注册用
public CurrentConditionsDisplay(Subject weatherData){
this.weatherData = weatherData;
weatherData.registerObserver(this);
}
@Override
public void display() {
System.out.println("Current conditions: "+temperature
+ "℃ and "+humidity+"% humidity");// 展示最近的温湿度
}
@Override
public void update(float temp, float humidity, float pressure) {
this.temperature = temp;
this.humidity = humidity;
display();
}
}
update()是最适合调用display()的地方吗
在简单的案例中,当值变化的时候调用display(),很合理;的确有很多更好的方法来设计显示数据的方式,MVC模式时会说明
为什么保存Subject的引用?构造后没用到
以后想取消注册,如果有对Subject的引用会很方便。
启动气象站
1. 先建立测试程序
先尝试,之后再回来确定每个组件都能通过配置文件达到“易插拔”
public class WeatherStation {
public static void main(String[] args) {
WeatherData weatherData = new WeatherData();
CurrentConditionsDisplay currentDisplay =
new CurrentConditionsDisplay(weatherData);
// StatisticsDisplay statisticsDisplay = new StatisticsDisplay(weatherData);
// ForecastDisplay forecastDisplay = new ForecastDisplay(weatherData);
weatherData.setMeasurements(25,60,30.4f);
weatherData.setMeasurements(26,50,30.4f);
weatherData.setMeasurements(33,40,30.4f);
}
}
2. 运行程序,让观察者模式表演魔术
Current conditions: 25.0℃ and 60.0% humidity
Current conditions: 26.0℃ and 50.0% humidity
Current conditions: 33.0℃ and 40.0% humidity
使用java内置的观察者模式
java的API有内置的观察者模式,Observer接口和Observable类,基本功能都有,甚至提供包括pull和push的方式传输数据
如何运作
如何把对象变成观察者
实现观察者接口(java.util.Observer),然后调用任何Observable对象的addObserver()方法,不想当观察者,就调用deleteObserver()即可
可观察者如何发送通知
首先,利用扩展Observable接口产生“可观察者”类,然后
- 先调用setChanged()方法,标记状态已经改变
- 再调用两种notifyObservers()方法中的一个:
notifyObservers()
或notifyObservers(Object arg)
后者在通知时,可传递任意的数据对象给每一个观察者
观察者如何接收通知
观察者实现更新的方法,但方法的签名不太一样
update(Observable o, Object arg)
前者为主题本身,后者为传入notifyObservers的数据对象,没有说明为空
如果你想推送push数据给观察者,可以把数据当做数据对象传送给notifyObservers(arg)方法;否则,观察者必须从可观察者对象中拉取pull数据。
为何要setChanged
用来标记状态已改变的事实,让notifyObservers()知道当它被调用时应该更新观察者。如果调用notifyObservers()之前没调用setChanged(),观察者就不会被通知
Observable的简化版代码如下
setChanged(){
changed = true
}
notifyObservers(Object arg){
if(changed){ // notifyObservers只会在changed标记为true时通知观察者
for every observer on the list{
call update(this,arg)
}
changed = false // 通知观察者之后,changed标记改为false
}
}
notifyObservers(){
notifyObservers(null)
}
这样有更大的弹性,可以更适当的通知观察者。比如,气象站测量非常敏感,温度计每十分之一度都会改变,造成WeatherData对象持续不断通知观察者。不希望这种情况发生,设置温差半度时调用setChanged(),进行有效更新
利用内置的支持重做气象站
先把WeatherData改为使用java.util.Observable
public class WeatherData extends Observable {
private float temperature;
private float humidity;
private float pressure;
public WeatherData(){} // 构造器不需要再为观察者们建立数据结构了
public void measurementsChanged(){
setChanged(); // 先改变状态,再通知
notifyObservers(); // 我们的做法是pull拉,没有传送数据对象
}
public void setMeasurements(float temperature,float humidity,float pressure){
this.temperature = temperature;
this.humidity = humidity;
this.pressure = pressure;
measurementsChanged();
}
// 这些不是新方法,使用拉pull需要这些方法,观察者利用这些方法取得WeatherData对象的状态
public float getTemperature(){
return temperature;
}
public float getHumidity(){
return humidity;
}
public float getPressure(){
return pressure;
}
}
重做CurrentConditionsDisplay
public class CurrentConditionsDisplay implements Observer, DisplayElement {
Observable observable;
private float temperature;
private float humidity;
// 构造器需要一个Observable参数,将CurrentConditionsDisplay登记为观察者
public CurrentConditionsDisplay(Observable observable){
this.observable = observable;
observable.addObserver(this);
}
@Override
public void display() {
System.out.println("Current conditions: "+temperature
+ "℃ and "+humidity+"% humidity");// 展示最近的温湿度
}
// 改变update方法,增加Observable和数据对象作为参数
@Override
public void update(Observable obs, Object arg) {
// 先确定可观察者属于WeatherData类型,再利用getter方法获取温湿度,最后调用display()
if (obs instanceof WeatherData){
WeatherData weatherData = (WeatherData) obs;
this.temperature = weatherData.getTemperature();
this.humidity = weatherData.getHumidity();
display();
}
}
}
public class ForecastDisplay implements Observer, DisplayElement {
private float currentPressure = 29.92f;
private float lastPressure;
public ForecastDisplay(Observable observable){
observable.addObserver(this);
}
@Override
public void display() {
// 展示代码
}
@Override
public void update(Observable o, Object arg) {
if(o instanceof WeatherData){
WeatherData weatherData = (WeatherData)o;
lastPressure = currentPressure;
currentPressure = weatherData.getPressure();
}
}
}
java.util.Observable的缺点
- Observable是一个类 必须设计一个类继承它,限制了Observable的复用潜力;没有Observable接口,无法建立自己的实现,和Java内置的Observer API搭配使用,也无法将java.util的实现换成另一套做法的实现
- Observable将关键的方法保护起来 除非继承自Observable,否则无法创建Observable实例并组合到自己的对象中。违反多用组合、少用继承的原则
如果可以,就自己实现观察者模式。
JDK中,还有哪些地方可以找到观察者模式
在JavaBeans和Swing中,也实现了观察者模式,这部分属于GUI,一般不会用java做
public class SwingObserverExample {
JFrame jFrame;
public void go(){
jFrame = new JFrame();
JButton button = new JButton("我要按下这个按钮吗?");
button.addActionListener(new AngleListener());
button.addActionListener(new DevilListener());
jFrame.getContentPane().add(BorderLayout.CENTER,button);
// 设置jframe的属性
}
class AngleListener implements ActionListener{
@Override
public void actionPerformed(ActionEvent e) {
System.out.println("不要按,你可能会后悔!");
}
}
class DevilListener implements ActionListener{
@Override
public void actionPerformed(ActionEvent e) {
System.out.println("come on,只需要轻轻按一下");
}
}
public static void main(String[] args) {
SwingObserverExample example = new SwingObserverExample();
example.go();
}
}
总结要点
- 观察者模式定义了对象之间一对多的关系
- 主题(也就是可观察者)用一个共同的接口来更新观察者
- 观察者和主题之间用松耦合方式结合(loose coupling),主题不知道观察者的细节,只知道观察者实现了观察者接口
- 使用此模式,可从被观察者处推push或拉pull数据(推被认为更正确)
- 有多个观察者时,不可以依赖特定的通知次序
- java有多种观察者模式的实现,包括通用的java.util.Observable
- 要注意java.util.Observable的实现带来的一些问题
- 如有必要,可自己实现Observable,并不难
- Swing大量使用观察者模式,许多GUI框架也都是如此
- 该模式还被用到许多地方,如JavaBeans、RMI
观察者模式对设计原则的实现
- 找到程序中会变化的部分,然后将其和固定不变的方面相分离 在观察者模式中,会变的是主题的状态,以及观察者的数目和类型。用这个模式,你可以改变依赖于主题状态的对象,不必改变主题,这就叫提前规划!
- 针对接口编程,不针对实现编程 主题和观察者都使用接口:观察者利用主题的接口向主题注册,而主题利用观察者的接口通知观察者,这样可以让两者之间运作正常,又同时具有松耦合的优点
- 多用组合,少用继承 观察者模式利用“组合”将许多观察者组合到主题中,对象之间的这种关系不是通过继承产生,而是在运行时利用组合的方式产生。