设计模式知识整理

50 minute

观察者模式

理解

对于观察者模式,既然有观察者,那么就首先有被观察者。

观察者可以通过订阅,监听等方式实现“观察”,被观察者需要通过通知,发消息之类的方式通知观察者接收信息。

假设有一个气象观测站,天气数据对象作为被观察者,各个气象观测站作为观察者订阅天气数据,而天气数据记录各个订阅了自己的观测站,以便于通知。

再比如 rss 订阅工具,它作为观察者,而各个用户订阅的 rss 地址作为被观察者,rss 订阅工具定时抓取 rss 内容,并将有更新的内容发送给用户。

实现

下面以气象站为例进行模拟实现。

首先定义两个接口:

被观察者:

1public interface Subject {
2	public void registerObserver(Observer o);
3	public void removeObserver(Observer o);
4	public void notifyObservers();
5}

观察者:

1public interface Observer {
2	public void update(float tempfloat humidityfloat pressure);
3}

接着,定义对象,实现接口:

天气数据对象:

 1public class WeatherData implements Subject {
 2	private List<Observer> observers;
 3	private float temperature;
 4	private float humidity;
 5	private float pressure;
 6	
 7	public WeatherData() {
 8		observers = new ArrayList<Observer>();
 9	}
10	
11	public void registerObserver(Observer o) {
12		observers.add(o);
13	}
14	
15	public void removeObserver(Observer o) {
16		observers.remove(o);
17	}
18	
19	public void notifyObservers() {
20		for (Observer observer : observers) {
21			observer.update(temperaturehumiditypressure);
22		}
23	}
24	
25	public void measurementsChanged() {
26		notifyObservers();
27	}
28	
29	public void setMeasurements(float temperaturefloat humidityfloat pressure) {
30		this.temperature = temperature;
31		this.humidity = humidity;
32		this.pressure = pressure;
33		measurementsChanged();
34	}
35
36	public float getTemperature() {
37		return temperature;
38	}
39	
40	public float getHumidity() {
41		return humidity;
42	}
43	
44	public float getPressure() {
45		return pressure;
46	}
47
48}

气象观测站对象:

省略了 StatisticsDisplay 和 ForecastDisplay。

 1public class CurrentConditionsDisplay implements Observer {
 2	private float temperature;
 3	private float humidity;
 4	private WeatherData weatherData;
 5	
 6	public CurrentConditionsDisplay(WeatherData weatherData) {
 7		this.weatherData = weatherData;
 8		weatherData.registerObserver(this);
 9	}
10	
11	public void update(float temperaturefloat humidityfloat pressure) {
12		this.temperature = temperature;
13		this.humidity = humidity;
14		display();
15	}
16	
17	public void display() {
18        ...
19	}
20}

最后,编写测试代码:

 1public class WeatherStation {
 2
 3	public static void main(String[] args) {
 4		WeatherData weatherData = new WeatherData();
 5	
 6		CurrentConditionsDisplay currentDisplay = 
 7			new CurrentConditionsDisplay(weatherData);
 8		StatisticsDisplay statisticsDisplay = new StatisticsDisplay(weatherData);
 9		ForecastDisplay forecastDisplay = new ForecastDisplay(weatherData);
10
11		weatherData.setMeasurements(806530.4f);
12		weatherData.setMeasurements(827029.2f);
13		weatherData.setMeasurements(789029.2f);
14		
15		weatherData.removeObserver(forecastDisplay);
16		weatherData.setMeasurements(629028.1f);
17	}
18}

装饰者模式

理解

装饰,如其名,就是对一个对象进行加工,包装,修饰。

假设一杯“普通的咖啡,价格 10”,我们可以选择添加自己喜欢的配料。

首先我们添加牛奶,那么就变成了一杯“含牛奶配料的咖啡,价格 2 +(价格 12)”;

我想再加点巧克力,那么再往上包装变成“含巧克力的含牛奶配料的咖啡,价格 3 +【价格 2+(价格 12)】”。

这么一层层封装上去就是装饰模式。

我们要在每一个配料中定义一个可包装的对象,在包装后返回这个对象。具体代码如下文。

实现

以饮料店作为例子编写代码。

首先定义一些抽象类:

饮料抽象类:

1public abstract class Beverage {
2	String description = "Unknown Beverage";
3  
4	public String getDescription() {
5		return description;
6	}
7 
8	public abstract double cost();
9}

配料抽象类:

注意这里配料也继承了饮料类,这样Beverage在通过包装之后还是返回一个Beverage,具体见测试代码。

1public abstract class CondimentDecorator extends Beverage {
2	Beverage beverage;
3	public abstract String getDescription();
4}

接着,定义一些实际的饮料和配料:

牛奶配料:(其他配料省略)

 1public class Milk extends CondimentDecorator {
 2	public Milk(Beverage beverage) {
 3		this.beverage = beverage;
 4	}
 5
 6	public String getDescription() {
 7		return beverage.getDescription() + ",Milk";
 8	}
 9
10	public double cost() {
11		return .10 + beverage.cost();
12	}
13}

浓缩咖啡类:(其他饮料略)

1public class Espresso extends Beverage {
2	public Espresso() {
3		description = "Espresso";
4	}
5  
6	public double cost() {
7		return 1.99;
8	}
9}

最后,编写测试代码:

 1public class StarbuzzCoffee {
 2 
 3	public static void main(String args[]) {
 4		Beverage beverage = new Espresso();
 5		System.out.println(beverage.getDescription() 
 6				+ " $" + beverage.cost());
 7 
 8		Beverage beverage2 = new DarkRoast();
 9		beverage2 = new Mocha(beverage2);
10		beverage2 = new Mocha(beverage2);
11		beverage2 = new Whip(beverage2);
12		System.out.println(beverage2.getDescription() 
13				+ " $" + beverage2.cost());
14 
15		Beverage beverage3 = new HouseBlend();
16		beverage3 = new Soy(beverage3);
17		beverage3 = new Mocha(beverage3);
18		beverage3 = new Whip(beverage3);
19		System.out.println(beverage3.getDescription() 
20				+ " $" + beverage3.cost());
21	}
22}

工厂模式

理解

工厂模式,包括工厂方法模式和抽象工厂模式。

对于工厂方法模式,工厂是一个抽象类,提供了一些默认实现方法和一些抽象方法,具体工厂继承于它,实现对应抽象方法。

假设有多家比萨店,他们提供不同口味的比萨,而都有相同的订购比萨的方法,那么可以定义一个抽象类,提供订购比萨的具体方法和创建比萨的抽象方法。

对于抽象工厂模式,工厂是一个接口,提供了一些具体工厂会用到的方法,同时还需要定义这些方法可能用到的接口。具体工厂需要首先实现抽象工厂定义的方法可能用到的接口,然后实现抽象工厂的所有方法。

假设有多家生产比萨配料的工厂,他们都有自己的独特的配料(实现所有配料接口),那么可以定义一个抽象工厂(一个接口),提供所有配料创建方法,具体工厂各自实现所有创建方法即可。

总之,他们的具体区别如下:

  • 工厂方法模式使用继承,把对象的创建委托给子类,子类实现工厂方法来创建对象。
  • 抽象工厂模式使用对象组合,对象的创建被实现在工厂接口所暴露出来的方法中。

工厂方法模式实现

下面通过披萨店的例子来进行模拟。

从抽象开始,定义一个比萨店抽象类:

 1public abstract class PizzaStore {
 2 
 3	abstract Pizza createPizza(String item);
 4 
 5	public Pizza orderPizza(String type) {
 6		Pizza pizza = createPizza(type);
 7		System.out.println("--- Making a " + pizza.getName() + " ---");
 8		pizza.prepare();
 9		pizza.bake();
10		pizza.cut();
11		pizza.box();
12		return pizza;
13	}
14}

定义一些具体的比萨店,比如一个芝加哥的比萨店:

 1public class ChicagoPizzaStore extends PizzaStore {
 2
 3	Pizza createPizza(String item) {
 4        	if (item.equals("cheese")) {
 5            		return new ChicagoStyleCheesePizza();
 6        	} else if (item.equals("veggie")) {
 7        	    	return new ChicagoStyleVeggiePizza();
 8        	} else if (item.equals("clam")) {
 9        	    	return new ChicagoStyleClamPizza();
10        	} else if (item.equals("pepperoni")) {
11            		return new ChicagoStylePepperoniPizza();
12        	} else return null;
13	}
14}

关于其它比萨店和比萨的实现省略了。

最后进行代码的测试:

根据自然的思路:首先应有比萨店,顾客选择比萨店,选择具体口味的比萨,完成订单。

 1PizzaStore nyStore = new NYPizzaStore();
 2PizzaStore chicagoStore = new ChicagoPizzaStore();
 3
 4Pizza pizza = nyStore.orderPizza("cheese");
 5System.out.println("Ethan ordered a " + pizza.getName() + "\n");
 6
 7pizza = chicagoStore.orderPizza("cheese");
 8System.out.println("Joel ordered a " + pizza.getName() + "\n");
 9
10pizza = nyStore.orderPizza("clam");
11System.out.println("Ethan ordered a " + pizza.getName() + "\n");

抽象工厂模式实现

还是从抽象开始,定义一个配料工厂接口:

 1public interface PizzaIngredientFactory {
 2 
 3	public Dough createDough();
 4	public Sauce createSauce();
 5	public Cheese createCheese();
 6	public Veggies[] createVeggies();
 7	public Pepperoni createPepperoni();
 8	public Clams createClam();
 9 
10}

接着提供各种配料接口:

1public interface Cheese {
2	public String toString();
3}

其它的省略了。

然后实现配料接口:

1public class MozzarellaCheese implements Cheese {
2	public String toString() {
3		return "Shredded Mozzarella";
4	}
5}

其它的省略了。

接着再实现配料工厂接口,也就是定义具体的配料工厂,提供自己实现的配料。

 1public class ChicagoPizzaIngredientFactory
 2        implements PizzaIngredientFactory {
 3
 4    public Dough createDough() {
 5        return new ThickCrustDough();
 6    }
 7
 8    public Sauce createSauce() {
 9        return new PlumTomatoSauce();
10    }
11	// ...
12}

其它配料工厂省略了。

接着,再从抽象开始。抽象的比萨应该含有所有工厂可能用到的配料,同时应该提供一个准备配料的抽象方法,在具体的比萨中实现这个方法,指定需要的配料,不要的则为null。抽象的比萨店如工厂方法模式一样。这里代码省略了。测试代码亦如工厂方法模式一样,这里省略了。

如何决定使用哪个工厂模式

工厂方法模式和抽象工厂模式都是创建对象的设计模式,但它们的使用场景略有不同。

当需求只需要单个对象的实例时,可以使用工厂方法模式。在这种模式下,一个工厂类负责实例化唯一的具体产品类。

而当需要创建多个相关的对象(产品族)时,可以使用抽象工厂模式。在这种模式下,一个抽象工厂类定义了一组方法,每个方法都用于实例化一个相关的具体产品类。使用抽象工厂模式的好处是,它能够确保创建的产品族是相互兼容的,因为所有产品都是由同一个工厂创建的。

在实践中,需要仔细考虑需求,以确定是否需要使用工厂方法模式或抽象工厂模式。

当需要创建一个简单图形的绘画程序时,可以使用工厂方法模式。

例如,一个工厂类可以根据用户选择创建圆形、矩形或三角形的实例。这些形状对象可以继承自一个通用的 Shape 类,该类定义了绘制图形所需的公共接口。

当需要创建一个跨不同操作系统的 UI 组件时,可以使用抽象工厂模式。

例如,一个抽象工厂类(一个应用程序)可以定义一组方法,每个方法都用于实例化一个操作系统特定的 UI 组件,如窗口、按钮和文本框。不同操作系统的具体工厂类(应用程序在某个操作系统上的表现)可以实现这些方法以创建适用于特定操作系统的 UI 组件(实现各个组件对应的接口并在实现工厂方法时使用对应的组件)。在这种情况下,每个具体工厂类实现的产品族都包含不同操作系统的 UI 组件,但它们共同满足了通用的接口。

策略模式

解释

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

比如对于鸭子而言,不同鸭子的叫声方式可能不一样也可能一样,我们可以定义一个叫声行为接口,然后通过创建不同的叫声行为实现这个接口,在实例化鸭子时设定叫声行为即可。

这样相比于直接继承抽象鸭子后重写叫声行为方法,好处是减少了重复代码,而坏处是增加了类的数量。

实现

首先定义一个叫声接口:

1public interface QuackBehavior {
2	public void quack();
3}

其它接口省略。

实现叫声接口:

1public class MuteQuack implements QuackBehavior {
2	public void quack() {
3		System.out.println("<< Silence >>");
4	}
5}

其它实现省略。

创建抽象鸭子:

 1public abstract class Duck {
 2	FlyBehavior flyBehavior;
 3	QuackBehavior quackBehavior;
 4
 5	public Duck() {
 6	}
 7
 8	public void setFlyBehavior(FlyBehavior fb) {
 9		flyBehavior = fb;
10	}
11
12	public void setQuackBehavior(QuackBehavior qb) {
13		quackBehavior = qb;
14	}
15
16	abstract void display();
17
18	public void performFly() {
19		flyBehavior.fly();
20	}
21
22	public void performQuack() {
23		quackBehavior.quack();
24	}
25
26	public void swim() {
27		System.out.println("All ducks float, even decoys!");
28	}
29}

最后,即可创建具体鸭子,继承于抽象鸭子,实例化时指定具体行为:

 1public class MallardDuck extends Duck {
 2
 3	public MallardDuck() {
 4		quackBehavior = new Quack();
 5		flyBehavior = new FlyWithWings();
 6	}
 7
 8	public void display() {
 9		System.out.println("I'm a real Mallard duck");
10	}
11}

同样可以轻松地创建其它鸭子。

和工厂模式的区别

工厂模式是创建型模式,适应对象的变化;策略模式是行为性模式,适应行为的变化。

形象点的例子:

工厂模式:

有一天你决定去吃培根披萨,首先得选择店铺,A 店和 B 店都有培根披萨;你点了 A 店的培根披萨,过了二十分钟,你的披萨就来了就可以吃到了。但这个披萨是怎么做的,到底面粉放了多少,培根放了多少,佐料放了多少,有多少道工序,你是不需要管的,你需要的是一个美味培根披萨。

策略模式:

在披萨店,你要一个培根披萨,老板说有标准的 pizza,也可以自己去做。原料有培根、面粉、佐料。工序有 1、2、3 工序,你自己去做吧。然后你就需要自己去做,到底放多少培根,放多少面粉,放多少佐料,这都你自己来决定,工序 1、2、3,你是怎么实现的,都你自己决定。最后你得到了披萨。

单例模式

理解

单例模式,容易知道需要某个对象是独一无二的,那么它首先应该是静态的,不能在程序动态运行期间被再次创建。

当在多个线程中用到它时,在创建时需要考虑线程安全问题,可通过加锁等方式解决。


单例模式的实现方式有多种,下面从最简单的方式开始实现,接着使用线程安全的方式实现,最后对多线程性能进行改善。

懒汉式

如其名,不是“急切”地进行对象的创建,而是在第一次主动获取对象时才加锁进行对象的初始化,实现对象的懒加载。

定义一个私有实例化的对象,把单例对象作为一个私有的静态成员变量,通过静态方法 Singleton.getInstance() 获取,若获取为 null 则创建对象。

 1public class Singleton {
 2	private static Singleton uniqueInstance;
 3 
 4	private Singleton() {}
 5 
 6	public static Singleton getInstance() {
 7		if (uniqueInstance == null) {
 8			uniqueInstance = new Singleton();
 9		}
10		return uniqueInstance;
11	}
12}

懒汉式(线程安全)

上面的懒汉式是线程不安全的,只适用于单线程环境。在多线程环境下,同时初始化该对象时将导致重复初始化对象的问题。解决方法如下:

 1public class Singleton {
 2	private static Singleton uniqueInstance;
 3 
 4	private Singleton() {}
 5 
 6	public static synchronized Singleton getInstance() {
 7		if (uniqueInstance == null) {
 8			uniqueInstance = new Singleton();
 9		}
10		return uniqueInstance;
11	}
12}

如上线程安全的代码,getInstance() 在多次调用的情况下性能太低,需要进行改善。

懒汉式(双重检查锁)

这里需要通过双重检查并加锁的方式实现,在进入 getInstance() 方法后检查对象是否为空,以及在获得锁之后,再次检查对象是否为空。

 1public class Singleton {
 2	private volatile static Singleton uniqueInstance;
 3 
 4	private Singleton() {}
 5 
 6	public static Singleton getInstance() {
 7		if (uniqueInstance == null) {
 8			synchronized (Singleton.class) {
 9				if (uniqueInstance == null) {
10					uniqueInstance = new Singleton();
11				}
12			}
13		}
14		return uniqueInstance;
15	}
16}

volatile 用于告诉编译器该变量可能会被其它线程或外部程序修改,一次每次访问该变量时都需要从内存重新读取其值,而不是使用缓存。这样可以确保当 uniqueInstance 被初始化时,多个线程正确处理 uniqueInstance 变量,保证并发的正确性。

另外 volatile 还可以防止编译器对代码进行重排序优化。比如在多线程环境下,几个线程对某个对象进行读操作,其它几个进行写操作,在写操作中,如果在进行对象的初始化,并且构造函数中操作比较多时,为了提升效率,JVM 会在构造函数里面的属性未全部完成实例化时,就返回对象。这样可能导致其它进行读操作的线程读取到未初始化完全的对象,而加上 volatile 关键字就没有这个问题了。

为什么之前的线程安全方式不需要添加 volatile 呢?因为之前的 synchronized 是作用在方法上的,也就是说在进入该方法之前不会访问到 uniqueInstance 变量,也就不会有缓存,不需要重新读取值。同时,也不会被重排序优化影响,因为读操作或写操作都是在进入方法内部之后进行的。

而这里是进入 getInstance 方法后首先访问 uniqueInstance 变量,之后再尝试获取锁,所以编译器可能会对 uniqueInstance 进行优化,将其缓存到寄存器中,同时,写操作和读操作都是进入方法之后进行的,可能被重排序优化影响。

双重检查锁模式是一种比较好的单例实现模式,能够保证在多线程的情况下线程安全也不会有性能问题。

饿汉式

如其名,就是“急切”地创建实例,也就是在 JVM 加载该类时马上创建实例,也可以保证线程安全。

1public class Singleton {
2	private static final Singleton uniqueInstance = new Singleton();
3 
4	private Singleton() {}
5 
6	public static Singleton getInstance() {
7		return uniqueInstance;
8	}
9}

该方式不能实现懒加载,造成空间浪费,如果一个类比较大,我们在初始化的时就加载了这个类,但是我们长时间没有使用这个类,这就导致了内存空间的浪费。当然如果类比较小可以考虑使用。

饿汉式(静态内部类)

静态内部类单例模式也称单例持有者模式,实例由内部类创建,由于 JVM 在加载外部类的过程中,是不会加载静态内部类的,只有内部类的属性/方法被调用时才会被加载,并初始化其静态属性。静态属性由static修饰,保证只被实例化一次,并且严格保证实例化顺序。

 1public class Singleton {
 2    private Singleton() {}
 3
 4    private static class InstanceHolder {
 5        private final static Singleton instance = new Singleton();
 6    }
 7    
 8    public static Singleton getInstance() {
 9        return InstanceHolder.instance;
10    }
11}

静态内部类单例模式是一种优秀的单例模式,是开源项目中比较常用的一种单例模式。在没有加任何锁的情况下,保证了多线程下的安全,并且没有任何性能影响和空间的浪费。

饿汉式(枚举)

为什么可以通过枚举实现单例?这是因为我们定义的一个枚举,只有在第一次被真正用到的时候,才会被虚拟机加载并初始化。基于这个特性,可以知道通过枚举实现的单例是天生线程安全的。

枚举类实现单例模式是 effective java 作者极力推荐的单例实现模式,因为枚举类型是线程安全的,并且只会装载一次,设计者充分的利用了枚举的这个特性来实现单例模式,枚举的写法非常简单,而且枚举类型是所用单例实现中唯一一种不会被破坏的单例实现模式。

 1public class Singleton {
 2    private Singleton() {}
 3
 4    private enum SingletonEnum {
 5        INSTANCE;
 6
 7        private final Singleton instance;
 8
 9        SingletonEnum() {
10            instance = new Singleton();
11        }
12
13        private Singleton getInstance() {
14            return instance;
15        }
16    }
17
18    public static Singleton getInstance() {
19        return SingletonEnum.INSTANCE.getInstance();
20    }
21}

破坏单例模式的方法及解决办法

1、除枚举方式外,其他方法都会通过反射的方式破坏单例,反射是通过调用构造方法生成新的对象,所以如果我们想要阻止单例破坏,可以在构造方法中进行判断,若已有实例,则阻止生成新的实例,解决办法如下:

1private Singleton() {
2    if (instance != null){
3        throw new RuntimeException("实例已经存在,请通过 getInstance()方法获取");
4    }
5}

2、如果单例类实现了序列化接口 Serializable,就可以通过反序列化破坏单例,所以我们可以不实现序列化接口,如果非得实现序列化接口,可以重写反序列化方法 readResolve(),反序列化时直接返回相关单例对象。

1public Object readResolve() throws ObjectStreamException {
2	return instance;
3}

命令模式

理解

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

比如对于遥控器而已,我们会将操作封装为一个按钮(命令)对象,通过按下按钮执行操作。

实现

下面以遥控器作为例子进行代码模拟。

首先从接口开始,定义一个命令接口:

1public interface Command {
2	public void execute();
3}

接着,定义操作对象风扇:(其它对象省略了)

 1public class Light {
 2	String location = "";
 3
 4	public Light(String location) {
 5		this.location = location;
 6	}
 7
 8	public void on() {
 9		System.out.println(location + " light is on");
10	}
11
12	public void off() {
13		System.out.println(location + " light is off");
14	}
15}

然后编写操作风扇的命令:

 1// 开风扇,其它操作省略了
 2public class LightOnCommand implements Command {
 3	Light light;
 4
 5	public LightOnCommand(Light light) {
 6		this.light = light;
 7	}
 8
 9	public void execute() {
10		light.on();
11	}
12}

还需要定义空命令,用于初始化遥控器命令:

1public class NoCommand implements Command {
2	public void execute() {}
3}

最后,定义遥控器,包含两个数组,分别用于开命令和关命令:

 1public class RemoteControl {
 2	Command[] onCommands;
 3	Command[] offCommands;
 4 
 5	public RemoteControl() {
 6		onCommands = new Command[7];
 7		offCommands = new Command[7];
 8 
 9		Command noCommand = new NoCommand();
10		for (int i = 0; i < 7; i++) {
11			onCommands[i] = noCommand;
12			offCommands[i] = noCommand;
13		}
14	}
15  
16	public void setCommand(int slot, Command onCommand, Command offCommand) {
17		onCommands[slot] = onCommand;
18		offCommands[slot] = offCommand;
19	}
20 
21	public void onButtonWasPushed(int slot) {
22		onCommands[slot].execute();
23	}
24 
25	public void offButtonWasPushed(int slot) {
26		offCommands[slot].execute();
27	}
28  
29	public String toString() {
30        // ...
31	}
32}

对代码进行简单测试:

 1// 定义遥控
 2RemoteControl remoteControl = new RemoteControl();
 3// 定义一些遥控对象
 4Light livingRoomLight = new Light("Living Room");
 5// ...
 6// 定义作用在某个遥控对象上的一些命令
 7LightOnCommand livingRoomLightOn = 
 8		new LightOnCommand(livingRoomLight);
 9LightOffCommand livingRoomLightOff = 
10		new LightOffCommand(livingRoomLight);
11LightOnCommand kitchenLightOn = 
12		new LightOnCommand(kitchenLight);
13LightOffCommand kitchenLightOff = 
14		new LightOffCommand(kitchenLight);
15// ...
16// 配置遥控器命令
17remoteControl.setCommand(0, livingRoomLightOn, livingRoomLightOff);
18// ...
19// 操作遥控
20remoteControl.onButtonWasPushed(0);
21remoteControl.offButtonWasPushed(0);
22// ...

适配器模式

解释

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

实现

鸭子和火鸡叫声不一样,通过编写适配器让火鸡适配鸭的方法。

首先,从接口开始,定义鸭子和火鸡接口:

1public interface Duck {
2	public void quack();
3	public void fly();
4}
5
6public interface Turkey {
7	public void gobble();
8	public void fly();
9}

接着编写适配器,让火鸡适配鸭子,也就是让火鸡适配器实现鸭子接口:

 1public class TurkeyAdapter implements Duck {
 2	Turkey turkey;
 3 
 4	public TurkeyAdapter(Turkey turkey) {
 5		this.turkey = turkey;
 6	}
 7    
 8	public void quack() {
 9		turkey.gobble();
10	}
11  
12	public void fly() {
13		for(int i=0; i < 5; i++) {
14			turkey.fly();
15		}
16	}
17}

接着就可以编写一些具体类了:

1// 绿头鸭
2public class MallardDuck implements Duck {
3	// ...
4}
5// 野火鸡:
6public class WildTurkey implements Turkey {
7	// ...
8}

最后对代码进行测试:

 1public class DuckTestDrive {
 2    static void testDuck(Duck duck) {
 3		duck.quack();
 4		duck.fly();
 5	}
 6	public static void main(String[] args) {
 7		Duck duck = new MallardDuck();
 8
 9		Turkey turkey = new WildTurkey();
10		Duck turkeyAdapter = new TurkeyAdapter(turkey);
11
12		turkey.gobble();
13		turkey.fly();
14		testDuck(duck);
15		testDuck(turkeyAdapter);
16	}
17}

外观模式

解释

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

最少知识原则:减少对象之间的交互,只和“密友”谈话,也就是减少一个类所交互的类的数量。

实现

执行一些复杂操作需要一步步执行许多小操作,那么可以将复杂操作封装为一个高层类中的方法,将所有复杂操作需要用到的类作为高层类的成员,在复杂操作的方法中可以方便的调用各个类执行各自的功能。

下面是一个家庭影院的例子:

 1public class HomeTheaterFacade {
 2	Amplifier amp;
 3	Tuner tuner;
 4	StreamingPlayer player;
 5	CdPlayer cd;
 6	Projector projector;
 7	TheaterLights lights;
 8	Screen screen;
 9	PopcornPopper popper;
10 
11	public HomeTheaterFacade(Amplifier amp, 
12				 Tuner tuner, 
13				 StreamingPlayer player, 
14				 Projector projector, 
15				 Screen screen,
16				 TheaterLights lights,
17				 PopcornPopper popper) {
18					// ...
19	}
20 
21	public void watchMovie(String movie) {
22		System.out.println("Get ready to watch a movie...");
23		popper.on();
24		popper.pop();
25		lights.dim(10);
26		screen.down();
27		projector.on();
28		projector.wideScreenMode();
29		amp.on();
30		amp.setStreamingPlayer(player);
31		amp.setSurroundSound();
32		amp.setVolume(5);
33		player.on();
34		player.play(movie);
35	}
36 
37 
38	public void endMovie() {
39		System.out.println("Shutting movie theater down...");
40		// ...
41	}
42
43	public void listenToRadio(double frequency) {
44		System.out.println("Tuning in the airwaves...");
45		// ...
46	}
47
48	public void endRadio() {
49		System.out.println("Shutting down the tuner...");
50		// ...
51	}
52}

各个功能器件的代码和测试代码省略了。

模板方法模式

解释

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

关于好莱坞原则:别调用我们,我们会调用你。

其实就是防止“依赖腐败”,也就是要避免高层组件和低层组件相互依赖。一般由高层组件依赖低层组件。

实现

泡茶和泡咖啡有着几乎相同的步骤,但在某些子步骤中有差别。通过模板方法可以很好地解决问题。

可以定义一个泡饮料的模板方法,其中子步骤 brew()addCondiments() 为抽象方法,由子类进行具体实现。

 1public abstract class CaffeineBeverage {
 2  
 3	final void prepareRecipe() {
 4		boilWater();
 5		brew();
 6		pourInCup();
 7		addCondiments();
 8	}
 9 
10	abstract void brew();
11  
12	abstract void addCondiments();
13 
14	void boilWater() {
15		System.out.println("Boiling water");
16	}
17  
18	void pourInCup() {
19		System.out.println("Pouring into cup");
20	}
21}

其它代码就省略了。

迭代器模式

解释

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

通过一个 Iterator,可以方便地遍历各种类型,如 HashMap,LinkedList 等。特别对于 LinkedList,使用迭代器进行迭代比使用下标进行迭代会快很多,因为是它基于链表的结构。

实现

下面通过菜单的例子进行说明。

分别用简单数组 String[]ArrayList<String> 类创建菜单内容。

首先创建菜单接口:

1public interface Menu {
2	public Iterator<String> createIterator();
3}

每个菜单都可以返回一个迭代器。

接着创建 Diner 的菜单,用String[]创建内容,需要编写自己的迭代器。

 1public class DinerMenu implements Menu {
 2	static final int MAX_ITEMS = 6;
 3	int numberOfItems = 0;
 4	String[] menuItems;
 5  
 6	public DinerMenu() {
 7		menuItems = new String[MAX_ITEMS];
 8 
 9		addItem("Vegetarian BLT");
10		// ...
11	}
12  
13	public void addItem(String name) 
14	{
15		if (numberOfItems >= MAX_ITEMS) {
16			System.err.println("Sorry, menu is full!  Can't add item to menu");
17		} else {
18			menuItems[numberOfItems] = name;
19			numberOfItems = numberOfItems + 1;
20		}
21	}
22 
23	public String[] getMenuItems() {
24		return menuItems;
25	}
26  
27	public Iterator<String> createIterator() {
28		return new DinerMenuIterator(menuItems); // 后面需要进行实现
29	}
30 
31	public String toString() {
32		return "Diner Menu";
33	}
34	// other menu methods here
35}

接着,创建菜单迭代器,需要实现 Iterater<String> 接口,实现 next()hasNext()remove() 方法。

 1public class DinerMenuIterator implements Iterator<String> {
 2	String[] list;
 3	int position = 0;
 4 
 5	public DinerMenuIterator(String[] list) {
 6		this.list = list;
 7	}
 8 
 9	public String next() {
10		String menuItem = list[position];
11		position = position + 1;
12		return menuItem;
13	}
14 
15	public boolean hasNext() {
16		if (position >= list.length || list[position] == null) {
17			return false;
18		} else {
19			return true;
20		}
21	}
22  
23	public void remove() {
24		if (position <= 0) {
25			throw new IllegalStateException
26				("You can't remove an item until you've done at least one next()");
27		}
28		if (list[position-1] != null) {
29			for (int i = position-1; i < (list.length-1); i++) {
30				list[i] = list[i+1];
31			}
32			list[list.length-1] = null;
33		}
34	}
35}

以后,访问菜单就很方便了,只需要获得菜单的迭代器即可。

其它代码省略了。

组合模式

解释

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

对于菜单内容而言,假如只有简单的菜品项,那么通过迭代器可以轻松地遍历,但是如果菜单内容中含有子菜单,那么就需要组合模式了。

实现

下面就以菜单为例子展示组合模式,每个菜单既可能由菜单项,也可能有子菜单,所以可以把菜单项和子菜单这两者都理解为菜单组件,作为一个抽象的对象,具体操作时根据需要进行实现即可。

首先创建菜单组件:

 1public abstract class MenuComponent {
 2   
 3	public void add(MenuComponent menuComponent) {
 4		throw new UnsupportedOperationException();
 5	}
 6	public void remove(MenuComponent menuComponent) {
 7		throw new UnsupportedOperationException();
 8	}
 9	public MenuComponent getChild(int i) {
10		throw new UnsupportedOperationException();
11	}
12	public String getName() {
13		throw new UnsupportedOperationException();
14	}
15	public String getDescription() {
16		throw new UnsupportedOperationException();
17	}
18	public double getPrice() {
19		throw new UnsupportedOperationException();
20	}
21	public boolean isVegetarian() {
22		throw new UnsupportedOperationException();
23	}
24	public void print() {
25		throw new UnsupportedOperationException();
26	}
27}

接着创建具体的菜单,继承菜单组件,实现父类方法中关于操作子菜单:

 1public class Menu extends MenuComponent {
 2	ArrayList<MenuComponent> menuComponents = new ArrayList<MenuComponent>();
 3	String name;
 4	String description;
 5  
 6	public Menu(String name, String description) {
 7		this.name = name;
 8		this.description = description;
 9	}
10	public void add(MenuComponent menuComponent) {
11		menuComponents.add(menuComponent);
12	}
13	public void remove(MenuComponent menuComponent) {
14		menuComponents.remove(menuComponent);
15	}
16	public MenuComponent getChild(int i) {
17		return (MenuComponent)menuComponents.get(i);
18	}
19	public String getName() {
20		return name;
21	}
22	public String getDescription() {
23		return description;
24	}
25	// getPrice 和 isVegetarian 就不需要了,因为是菜单
26	public void print() {
27		System.out.print("\n" + getName());
28		System.out.println(", " + getDescription());
29		System.out.println("---------------------");
30		for (MenuComponent menuComponent : menuComponents) {
31			menuComponent.print();
32		}
33	}
34}

接着创建菜单项,它也是一个菜单组件,但是它不需要重写部分关于子菜单的方法:

 1public class MenuItem extends MenuComponent {
 2	String name;
 3	String description;
 4	boolean vegetarian;
 5	double price;
 6    
 7	public MenuItem(String name, 
 8	                String description, 
 9	                boolean vegetarian, 
10	                double price) 
11	{ 
12		//...
13	}
14  
15	public String getName() {
16		return name;
17	}
18  
19	public String getDescription() {
20		return description;
21	}
22  
23	public double getPrice() {
24		return price;
25	}
26  
27	public boolean isVegetarian() {
28		return vegetarian;
29	}
30  
31	public void print() {
32		// ...
33	}
34}

最后进行测试:

 1public class MenuTestDrive {
 2	public static void main(String args[]) {
 3		// 主菜单
 4		MenuComponent allMenus = new Menu("ALL MENUS", "All menus combined");
 5		// 一些子菜单
 6		MenuComponent pancakeHouseMenu = 
 7			new Menu("PANCAKE HOUSE MENU", "Breakfast");
 8		// 其它子菜单省略了
 9		// ...
10		// 添加子菜单
11		allMenus.add(pancakeHouseMenu);
12		allMenus.add(dinerMenu);
13		allMenus.add(cafeMenu);
14		// 在子菜单中添加菜单项
15		pancakeHouseMenu.add(new MenuItem(
16			"K&B's Pancake Breakfast", 
17			"Pancakes with scrambled eggs and toast", 
18			true,
19			2.99));
20        // 省略其它菜品项
21		// ...
22		// 子菜单添加子菜单
23		dinerMenu.add(dessertMenu);
24	}
25}

状态模式

解释

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

比如某个机器有各种复杂的状态,每个状态都着共同的参数,而这些参数值有区别。用户通过某些操作会改变机器的状态,机器转变状态后,以当前状态的方法给用户反馈。

实现

以机器的例子说明。每次对机器进行操作,机器的状态都可能发生改变,但是下一个状态是什么需要由当前状态决定。所以每个具体的状态定义好了从当前状态经过当前操作会跳转到哪个状态。

首先编写一个状态接口,其中包括该机器可能的所有状态:

1public interface State {
2	public void insertQuarter();
3	public void ejectQuarter();
4	public void turnCrank();
5	public void dispense();
6	public void refill();
7}

接着创建机器类:

 1public class GumballMachine {
 2    // 各种机器状态,后续需要进行具体实现
 3	State soldOutState;
 4	State noQuarterState;
 5	State hasQuarterState;
 6	State soldState;
 7    // 当前状态
 8	State state;
 9	int count = 0;
10	
11	// 初始化该机器的各个状态
12	public GumballMachine(int numberGumballs) {
13		soldOutState = new SoldOutState(this); // 这些状态依赖于该机器
14		noQuarterState = new NoQuarterState(this);
15		hasQuarterState = new HasQuarterState(this);
16		soldState = new SoldState(this);
17		this.count = numberGumballs;
18 		if (numberGumballs > 0) {
19			state = noQuarterState;
20		} else {
21			state = soldOutState;
22		}
23	}
24	// 定义一些操作,经过某个操作后,机器状态发生改变
25	public void turnCrank() {
26		state.turnCrank();
27		state.dispense();
28	}
29	// 其它操作省略了
30	// ...
31	// constructor getter setter
32}

接着,定义各种机器状态,以其中一个状态为例子:

 1public class HasQuarterState implements State {
 2	GumballMachine gumballMachine;
 3	// 依赖某个具体的机器
 4	public HasQuarterState(GumballMachine gumballMachine) {
 5		this.gumballMachine = gumballMachine;
 6	}
 7	// 经过该操作,该机器从 HasQuarterState 转移到下一个状态
 8	public void ejectQuarter() {
 9		System.out.println("Quarter returned");
10		gumballMachine.setState(gumballMachine.getNoQuarterState()); // 转移到该机器的其它状态
11	}
12	// 重写的其它方法省略了
13	// ...
14}

测试部分代码省略了。

代理模式

解释

代理模式提供了对目标对象另外的访问方式,即通过代理对象访问目标对象。这样做的好处是可以在目标对象实现的基础上,增强额外的功能操作,即扩展目标对象的功能。

这里使用到编程中的一个思想:不要随意去修改别人已经写好的代码或者方法,如果需改修改,可以通过代理的方式来扩展该方法。

实现静态代理

静态代理在使用时,需要定义接口或者父类,被代理对象与代理对象一起实现相同的接口或者是继承相同父类。

下面举个案例来解释。

模拟保存动作,定义一个保存操作的接口 IUserDao,然后目标对象实现这个接口的方法 UserDao,此时如果使用静态代理方式,就需要在代理对象 UserDaoProxy 中也实现 IUserDao 接口,调用的时候通过调用代理对象的方法来调用目标对象。

需要注意的是,代理对象与目标对象要实现相同的接口,然后通过调用相同的方法来调用目标对象的方法。

首先定义接口 IUserDao:

1public interface IUserDao {
2    void save();
3}

接着定义目标对象 UserDao:

1public class UserDao implements IUserDao {
2    public void save() {
3        System.out.println("saved.");
4    }
5}

然后代理对象 UserDaoProxy:

 1public class UserDaoProxy implements IUserDao{
 2    // 接收保存目标对象
 3    private IUserDao target;
 4
 5    public UserDaoProxy(IUserDao target){
 6        this.target=target;
 7    }
 8
 9    public void save() {
10        System.out.println("before..."); // 增强的方法
11        target.save(); // 执行目标对象的方法
12        System.out.println("after..."); // 增强的方法
13    }
14}

静态代理在编译器就完成了,可以做到在不修改目标对象的功能前提下,对目标功能扩展。但因为代理对象需要与目标对象实现一样的接口,所以会有很多代理类。同时,一旦接口增加方法,目标对象与代理对象都要维护。

实现动态代理

在动态代理中,代理对象不需要实现接口。代理对象的生成利用了 JDK 的 API,动态的在内存中构建代理对象(需要我们指定创建代理对象/目标对象实现的接口的类型)。

JDK 实现代理只需要使用 newProxyInstance 方法,该方法需要接收三个参数,完整的写法是:

1static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)
  • ClassLoader loader: 指定当前目标对象使用类加载器,获取加载器的方法是固定的
  • Class[] interfaces: 目标对象实现的接口的类型,使用泛型方式确认类型
  • InvocationHandler h: 事件处理,执行目标对象的方法时,会触发事件处理器的方法,会把当前执行目标对象的方法作为参数传入

接口类IUserDao.java以及接口实现类,目标对象UserDao是一样的,没有做修改.在这个基础上,增加一个代理工厂类(ProxyFactory.java),将代理类写在这个地方,然后在测试类(需要使用到代理的代码)中先建立目标对象和代理对象的联系,然后代用代理对象的中同名方法

定义一个代理工厂类 ProxyFactory,用于方便地生成代理对象:

 1public class ProxyFactory{
 2    //维护一个目标对象
 3    private Object target;
 4    public ProxyFactory(Object target) {
 5        this.target=target;
 6    }
 7
 8   //给目标对象生成代理对象
 9    public Object getProxyInstance() {
10        return Proxy.newProxyInstance(
11                target.getClass().getClassLoader(),
12                target.getClass().getInterfaces(),
13                new InvocationHandler() {
14                    @Override
15                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
16                        System.out.println("before...");
17                        Object returnValue = method.invoke(target, args);
18                        System.out.println("after...");
19                        return returnValue;
20                    }
21                }
22        );
23    }
24}

代理对象不需要实现接口,但是目标对象一定要实现接口,否则不能用动态代理。

Cglib 代理

上面的静态代理和动态代理模式都是要求目标对象是实现一个接口的目标对象,但是有时候目标对象只是一个单独的对象,并没有实现任何的接口,这个时候就可以使用以目标对象子类的方式类实现代理,这种方法就叫做 Cglib 代理。

Cglib 代理,也叫作子类代理,它是在内存中构建一个子类对象从而实现对目标对象功能的扩展。

Cglib 是一个强大的高性能的代码生成包,它可以在运行期扩展类与实现接口,广泛的被许多 AOP 的框架使用,例如 Spring。Cglib 包的底层是通过使用一个小而快的字节码处理框架 ASM 来转换字节码并生成新的类。

Cglib 子类代理实现方法:

  1. 需要引入 cglib 的 jar 文件,但是 Spring 的核心包中已经包括了 Cglib 功能,所以直接引入 spring-core-3.2.5.jar 也可以;
  2. 代理的类不能为 final;
  3. 目标对象的方法如果为 final/static,那么就不会被拦截,即不会执行目标对象额外的业务方法;

创建一个 Cglib 代理工厂 ProxyFactory:

 1public class ProxyFactory implements MethodInterceptor{
 2    // 维护目标对象
 3    private Object target;
 4
 5    public ProxyFactory(Object target) {
 6        this.target = target;
 7    }
 8
 9    // 给目标对象创建一个代理对象
10    public Object getProxyInstance(){
11        // 1.工具类
12        Enhancer en = new Enhancer();
13        // 2.设置父类
14        en.setSuperclass(target.getClass());
15        // 3.设置回调函数
16        en.setCallback(this);
17        // 4.创建子类(代理对象)
18        return en.create();
19    }
20
21    @Override
22    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
23        System.out.println("before...");
24        Object returnValue = method.invoke(target, args);
25        System.out.println("after...");
26        return returnValue;
27    }
28}

在 Spring 的 AOP 编程中:

  • 如果加入容器的目标对象有实现接口,用 JDK 代理
  • 如果目标对象没有实现接口,用 Cglib 代理

参考: Head First 设计模式