《Head First 设计模式》笔记

2023/02/25Saturday 编程

策略模式

TIP

策略模式定义了算法族,分别封装起来,让他们之间可以互相替换,此模式让算法的变化独立于使用算法的客户

找出应用中可能需要变化之处,把他们独立出来,不要和那些不需要变化的代码混在一起

针对接口编程而不是针对现实编程

并不是所有鸭子都会飞,我们应该把所有鸭子共有的功能放到基类,一些特有的功能比如飞行,作为其需要实现的接口类型属性:

public abstract class Duck{
	FlyBehavior flyBehavior;

	public Duck() {}

	public abstract void display();

	public void setFlyBehavior(FlyBehavior fb) {
		flyBehavior = fb;
	}

	public void performFly() {
		flyBehavior.fly()
	}

	public void swim() {
		System.out.println("All ducks float, even decoys!");
	}
}

多用组合,少用继承

观察者模式

TIP

观察者模式定义了对象之间的一对多依赖,这样一来,当一个对象改变状态时,他的所有依赖者都会收到通知并自动更新

Observable 或者说是 Subject 需要维护一个列表来记录订阅了它的观察者 Observer。java.util.Observable是一个类而不是java.util.Observer那样的接口,在其内部帮你实现了维护列表的功能,但因为 java 不支持多重继承,用起来有一定局限性

在很多UI监听功能中会用到观察者模式

类应该对扩展开放,对修改关闭

装饰者模式

TIP

装饰者模式动态地将责任附加到对象上。若要扩展功能,装饰者提供了比继承更有弹性的替代方案。

// 抽象组件
public abstract class Beverage {
	String description = "Unknown Beverage";

	public String getDescription() {
		return description;
	}

	public abstract double cost();
}

// 抽象装饰着
public abstrcat class CondimentDecorator extends Beverage {
	public abstract String get Description();
}

// 具体组件
public class HouseBlend extends Beverage {
	public HouseBlend() {
		description = "House Blend Coffee";
	}
	public double cost() {
		return .89;
	}
}

// 具体装饰者
public class Mocha extends CondimentDecorator {
	Beverage beverage;

	public Mocha(Beverage beverage) {
		this.beverage = beverage;
	}

	public String getDescription() {
		return beverage.getDescrription() + ", Mocha";
	}

	public double cost()	{
		return .20 + beverage.cost();
	}
}

// 装饰
Beverage beverage = new HouseBlend();
beverage = new Mocha(beverage);
beverage = new Soy(beverage); // Soy 也是一个具体装饰者哦
beverage.cost...

在java I/O包中有许多类都是装饰者,比如 LineNumberInputStream(BufferedInputStream(FileInputStream))),(PHP也差不多

工厂(Factory)

使用new时,是在实例化一个具体类,这和之前说的“针对接口编程”相违背,在涉及一群相关的具体类时,通常会写出这样的代码:

Duck duck;
if (picnic) {
	duck = new MallardDuck();
} else if (hunting) {
	duck = new DecoyDuck();
} else if (inBathTub) {
	duck = new RubberDuck();
}

一旦有变化或者扩展,就必须重新打开这段代码进行检查和修改。

简单工厂/静态工厂,无需多介绍了

工厂方法模式

TIP

工厂方法模式通过让子类决定该创建的对象是什么,来达到将对象创建的过程封装的目的

public abstract class PizzaStore {

	public Pizza orderPizza(String type) {
		Pizza pizza;
		pizza = createPizza(type);
		pizza.prepare();
		...
		return pizza;
	}

	protected abstract Pizza createPizza(String type);
}

工厂方法模式定义了一个创建对象的接口,但由子类决定要实例化的类是哪一个。工厂方法让类把实例化推迟到子类。

一段很有意思的对话:

门徒:大师,我知道封装起创建对象的代码,就可以对抽象编码,将客户代码和真实的现实解耦。然而在我的工厂代码中,不可避免的,仍然必须使用具体类来实例化真正的对象。我这不是“蒙着眼睛骗自己”吗?(pulling wool over my own eyes)

大师:蚱蜢呀!对象的创建是现实的,如果不创建任何对象,就无法创建任何Java程序,然而,利用这个现实的知识,可将这些创建对象的代码用栅栏围起来,就像你把所有的羊毛堆到眼前一样,就可以保护这些创建对象的代码。如果让创建对象的代码到处乱跑,那就无法收集到“羊毛”,你说是吧?

依赖倒置原则(Dependency Inversion Principle):要依赖抽象,不要依赖具体类。

这个原则听起来很像是“针对接口编程而不是针对现实编程”,然而这里更强调“抽象”。不能让高层组件依赖低层组件,而且,不管高层低层,两者都应该依赖于抽象。

回到上面的例子,Pizza也是一个抽象类,高层组件(PizzaStore)和低层组件(也就是具体的Pizza实现)都依赖了Pizza抽象。想要遵循依赖倒置原则,工厂方法并非唯一的技巧,但却是最有威力的技巧之一。

抽象工厂模式

TIP

抽象工厂模式提供一个接口,用于创建相关或依赖对象的家族,而不需要明确指定具体类

public abstract class AbstractDuckFactory {
	public abstract Quackable createMallardDuck();
	public abstract Quackable createReadheadDuck();
	...
}

public class CountingDuckFactory extends AbstractDuckFactory {
	public Quackable createMallardDuck() {
		return new QuackCounter(new MallardDuck());
	}
	public Quackable createRedheadDuck() {
		return new QuackCounter(new RedheadDuck());
	}
	...
}

public class DuckSimulator {
	public static void main(String[] args) {
		DuckSimulator simulator = new DuckSimulator();
		// 注意这里的多态
		AbstractDuckFactory duckFactory = new CountingDuckFactory();
		simulator.simulate(duckFactory);
	}
	
	void simulate(AbstractDuckFactory duckFactory) {
		Quackable mallardDuck = duckFactory.createMallardDuck();
		Quackable readheadDuck = duckFactory.createRedheadDuck();
		...
	}
}

抽象工厂的方法经常以工厂方法的方式实现。(书中此处的例子就是以工厂方法,但上面是我在书中另一处找的简单实现

工厂方法创建对象需要用到继承,而抽象工厂用到对象的组合

单件模式

TIP

单件模式,确保一个类只有一个实例,并提供一个全局的访问点。

java双重检查加锁(double-checked locking)实现线程安全的单例:

public class Singleton {
	// volatile关键字确保多线程正确处理初始化
	private volatile static Singleton uniqueInstance;

	private Singleton() {}

	public static Singleton getInstance() {
		if (uniqueInstance == null) {
			synchronized (Singleton.class) {
				if (uniqueInstance == null) {
					uniqueInstance =  new Singleton();
				}
			}
		}
		return uniqueInstance;
	}
}

命令模式

TIP

命令模式将"请求"封装成对象,以便使用不同的请求、队列或日志来参数化其他对象。命令模式也支持可撤销的操作。

public class LightCommand implements Command {
	Light light;

	public LightOnCommand(Light light) {
		this.light = light;
	}

	public void execute() {
		light.on();
	}
}

public class SimpleRemoteControl {
	Command slot;

	public SimpleRemoteControl() {}

	public void setCommand(Command command) {
		slot = command;
	}

	public void buttonWasPressed() {
		slot.execute();
	}
}

适配器模式

TIP

适配器模式将一个类的接口,转换成客户期望的另一个接口,适配器让原本接口不兼容的类可以合作使用。

// 首先,你需要实现你想转换成的类型接口
public class TurkeyAdapter implements Duck {
	Turkey turkey;

	// 接着,需要取得适配器对象的引用
	public TurkeyAdapterr(Turkey turkey) {
		this.turkey = turkey;
	}

	// 适配...
	public void quack() {
		turkey.gobble();
	}
}
  • 装饰者:不改变接口,但加入责任
  • 适配器:将一个接口转换成另一个接口
  • 外观:让接口更简单

外观模式

TIP

外观模式提供了一个统一的接口,用来访问子系统中的一群接口,外观定义了一个高层接口,让系统更容易使用。

public class HomeTheaterFacade {
	Amlifier amp;
	...
	// 外观将子系统中每一个组件的引用都传入它的构造器
	public HomeTheaterFacade(Amplifier amp,...) {
		this.amp =amp;
	}
	// 将子系统的组件整合成一个统一的接口
	public void watchMovie(string movie) {
		amp.on();
		...
	}
}

最少知识原则

IMPORTANT

最少知识(Least Knowledge)原则告诉我们要减少对象之间的交互,只留下几个“密友”

就任何对象而言,在该对象的方法内,我们只应该调用属于以下范围的方法:

  • 该对象本身
  • 被当作方法的参数而传进来的对象
  • 此方法所创建或实例化的任何对象
  • 对象的任何组件

问:还有另一个原则,叫做得墨忒耳法则(Law of Demeter),它和最少知识原则有什么关系?

答:其实两个名词指的是同一个原则,我们倾向于使用最少知识原则来称呼它是因为(1)这个名字更直接。(2)法则(Law)给人的感觉是强制的。事实上,没人任何原则是法律(Law),所有的原则都应该在有帮助的时候才遵守,所有的设计都不免需要折中(在抽象和速度之间取舍,在空间和时间之间平衡)。虽然原则提供了方针,但在采用原则之前,必须全盘考虑所有的因素。

public House {
	WeatherStation station;

	public float getTemp() {
		// 违反了最少知识原则
		return station.getThermometer().getTemperature();
	}
}
public Hose {
	weatherStation station;
	
	public float getTemp() {
		Thermometer thermometer = station.getThermometer();
		return getTempHelper(thermometer);
	}

	public float getTempHelper(Thermometer thermometer) {
		return thermometer.getTemperature();
	}
}

没有违反最少知识原则,但是,把程序改成这样真的有意义吗?

模板方法模式

TIP

模板方法模式在一个方法中定义一个算法的骨架,而将一些步骤延迟到子类中,模板方法使得子类可以在不改变算法结构的情况下,重新定义算法中的某些步骤。

算是最常用的设计模式吧?和策略模式

java 数组类就用到了模板方法来排序

public static void sort(Object[] a) {
	Object aux[] = (Object[])a.clone();
	mergeSort(aux, a, 0, a.length, 0);
}

private static void mergeSort(Object src[], Object dest[], int low, int hight, int off) {
	for (int i=low; i<high; i++) {
		for (int j=i; j<low && ((Comparable)dest[j-1]).compareTo((Comparable)dest[j])>0; j--) {
			swap(dest, j, j-1);
		}
	}
	return;
}

Comparable是一个接口,实现这个接口需要提供compareTo方法

java applet 类也是使用了模板方法与其中的钩子

public class MyApplet extends Applet {
	String message;

	public void init() {
		message = "Hello World";
		repaint();
	}
	public void start() {}
	// ...

	public void paint(Graphics g) {
		g.drawSring(message, 5, 15);
	}
}

工厂方法是模板方法是一种特殊版本

迭代器模式

TIP

迭代器模式提供一种方法顺序访问一个聚合对象中的各个元素,而又不暴露其内部的表示。

自带的java.util.Iterator接口需要实现三个方法 hasNext() next() remove()

一个类应该只有一个引起变化的原因。

组合模式

TIP

组合模式允许你将对象组合成树形结构来表现“整体/部分”层次结构。组合能让客户以一致的方式处理个别对象以及对象的组合。

书中例子代码太多,概况来说就是父类为叶节点和组合节点提供一个共同的接口,在组合节点中实现迭代器模式。对于不适用某种节点的方法,抛出运行时异常。虽然这样违反了一个类一个责任的规则但却是一个非常好的折中方案。

组合迭代器实现:

public class Menu extends MenuComponent {
	// 组合模式中组合对象实现...

	public Iterator createIterator() {
		return new CompositeIterator(menuComponent.iterator());
	}
}

public class MenuItem extends MenuComponent {
	// 组合模式中叶节点对象实现...

	public Iterator createIterator() {
		return new NullIterator();
	}
}

import java.util.*;

public class CompositeIterator implements Iterator {
	Stack stack = new Stack(); // 用栈来记录遍历位置

	public CompositeIterator(Iterator iterator) {
		stack.push(iterator);
	}

	public Object next() {
		if (hasNext()) {
			Iterator iterator = (Iterator) stack.peek();
			MenuComponent component = (MenuComponent) iterator.next();
			if (component instanceof Menu) {
				stack.push(component.createIterator());
			}
			return component;
		} else {
			return null;
		}
	}

	public boolean hasNext() {
		if (stack.empty()) {
			return false;
		} else {
			Iterator iterator = (Iterator) stack.peek();
			if (!iterator.hasNext()) {
				stack.pop();
				return hashNext();
			} else {
				return true;
			}
		}
	}

	public void remove() {
		throw new UnsupportedOperationException();
	}
}

状态模式

TIP

状态模式允许对象在内部状态改变时改变它的行为,对象看起来好像修改了它的类。

不使用状态模式之前代码:

public class GumballMachine {
	// 状态
	final static int SOLD_OUT = 0;
	final static int NO_QUARTER = 1;
	final static int HAS_QUARTER = 2;
	final static int SOLD = 3;

	int state = SOLD_OUT;

	// 动作
	public void insertQuarter() {
		if (state == HAS_QUARTER) {
			System.out.println("You can't insert another quarter");
		} else if (state == NO_QUARTER) {
			state = HAS_QUARTER
			...
		} else if ... 
	}
	...

可以预见,每新增一个转换状态的动作,都需要写一大堆if else语句。

我们将状态抽象出一个接口State,在这个接口内,每个动作都有一个对应的方法

public class NoQuarterState implements State {
	GumballMachine gumballMachine;

	public NoQuarterState(GumballMachine gm) {
		gumballMachine = gm;
	}

	// 表示如果在 NO_QUARTER 状态下进行 insertQuarter 动作
	public void insertQuarter() {
		gumballMachine.setState(gumballMachine.getHasQuarterState());
	}
	...
}

public class GumballMachine {
	// 四个实现了 State 接口的状态类
	State soldOutState;
	State noQuarterState;
	State hasQuarterState;
	State soldState;

	State state = soldOutState;

	public void insertQuarter() {
		state.insertQuarter();
	}
}

剩下的代码应该不用写了,可以看到通过状态模式我们不需要if else就可以完成复杂的状态转换操作

像是一种复杂的策略模式,Context 会将行为委托给当前状态对象

代理模式

TIP

代理模式为另一个对象提供一个替身或占位符以控制这个对象的访问。

使用代理模式创建代表(representative)对象,让代表对象控制某对象的访问,被代理的对象可以是远程的对象、创建开销大的对象或需要安全控制的对象。

图像未加载完成前显示提示文字:

class ImageProxy implements Icon {
	ImageIcon imageIcon;

	URL imageURL;
	Thread retrievalThread;
	...

	public ImageProxy(URL url) { imageUrl = url; }

	// 实现 Icon 接口...

	public void paintIcon(final Component c, Graphics g, int x, int y) {
		if (imageIcon != null) {
			imageIcon.paintIcon(c, g, x, y);
		} else {
			g.drawString("Loading CD cover, please wait...", x+300, y+190);
			if (!retrieving) {
				retrieving = true;
				retrievalThread = new Thread new Runnable() {
					public void run() {
						try {
							// 请求图片...
						} catch (Exception e) {
							e.printStackTrace();
						}
					}
				});
				retrievalThread.start();
			}
		}
	}
}

因为和源对象实现同样的接口,所以看起来像是装饰者模式。

java 在 java.lang.reflect 中有自己的代理支持,利用这个包你可以在运行时动态地创建一个代理类,实现一个或多个接口,并将方法的调用转发到你所指定的类。因为实际的代理类是在运行时创建的,我们称这个 Java 技术为:动态代理

除了远程代理、虚拟代理、保护代理,还有以下常见代理类型:

  • 防火墙代理(Firewall Proxy),控制网络资源的访问
  • 智能引用代理(Smart Reference Proxy),当主题被引用时,进行额外的动作,比如计算一个对象被引用次数
  • 缓存代理(Caching Proxy),为开销大的运算结果提供暂时存储
  • 同步代理(Synchronization Proxy),在多线程情况下为主题提供安全访问。
  • 复杂隐藏代理(Complexity Hiding Proxy),用来隐藏一个类的复杂集合的复杂度,并进行访问控制。有时候也被称为外观代理(Facade Proxy)

总结

模式 描述
装饰者 包装一个对象,以提供新的行为
状态 封装了基于状态的行为,并使用委托在行为之间切换
迭代器 在对象的集合之中游走,而不暴露集合的实现
外观 简化一群类的接口
策略 封装可以互换的行为,并使用委托来决定要使用哪一个
代理 包装对象,以控制对此对象的访问
工厂方法 由子类决定要创建的具体类是哪一个
适配器 封装对象,并提供不同的接口
观察者 让对象能够在状态改变时被通知
模板方法 由子类决定如何实现一个算法中的步骤
组合 客户用一致的方式处理对象集合和单个对象
单件 确保只有一个对象被创建
抽象工厂 允许客户创建对象的家族,而无需指定他们的具体类
命令 封装对象成为请求

复合模式

模式通常被一起使用,并被组合在同一个设计解决方案中

复合模式在一个解决方案中结合两个或多个模式,以解决一般或重复发生的问题

书中一步步实现了一个结合多种设计模式的程序(P501)

问:这就是复合模式?

答:不,这只是一群模式携手合作。所谓的复合模式,是指一群模式被结合起来使用,以解决一般性问题。我们很快就会看到 MVC (模型-视图-控制器)复合模式,它是由数个模式结合起来而形成的新模式,一再地被用于解决许多设计问题。

问:所以,设计模式真正漂亮的地方在于,遇到问题时,我可以拿模式逐一地解决问题,直到所有问题都被解决。我这样说对吗?

答:错!我们在鸭子的例子中之所以这么做,主要目的是展示许多模式可以合作。在真实的设计过程中,你不会想要这么做的。事实上,鸭子模拟器的许多部分都可以用模式来解决,只是有一点‘杀鸡焉用牛刀’的感觉。有时候,用好的 OO设计原则就可以解决问题,这样其实就够了。...采用模式时必须要考虑到这么做是否有意义,绝对不能为了使用模式而使用模式。

生活中的模式

模式是在某种情景(context)下,针对某问题的某种解决方案。

模式并非从软件开始,而是始于建筑和城镇的架构。事实上,模式的概念可以被应用在许多不同的领域:

  • 架构模式,用来建立生机勃勃的建筑、城市的架构
  • 应用模式,是建立系统架构的模式
  • 领域特定模式,关注特点领域的问题,例如并发系统或实时系统
  • 业务流程模式,描述业务、顾客和数据之间的交互
  • 组织模式,描述人类组织的结构以及实践
  • 用户界面设计模式,致力于解决设计交互软件时的问题

让设计模式自然而然地出现在你的设计中,而不是为了使用而使用。

剩下的模式

桥接

使用桥接模式不只改变你的实现,也改变你的抽象。

桥接模式通过将实现和抽象放在两个不同的类层次中而使他们可以独立改变

生成器

使用生成器模式封装一个产品的构造过程,并允许按步骤构造。

责任链

当你想要让一个以上的对象有机会能处理某个请求的时候,就使用责任链模式。

蝇量

如想让某个类的一个实例能用来提供许多的“虚拟实例”,就使用蝇量模式。

解释器

使用解释器模式为语言创建解释器。

中介者

使用中介者模式来集中相关对象之间复杂的沟通和控制方式。

备忘录

当你需要让对象返回之前的状态时(例如,你的用户请求“撤销”),就使用备忘录模式。

原型

当创建给定类的实例的过程很昂贵或很复杂时,就使用原型模式。

访问者

当你想要为一个对象的组合增加新的能力,且封装不重要时,就使用访问者模式。