本章可以称为“给爱用继承的人一个全新的设计眼界” ,我们即将再度探讨典型的继承滥用问题。你将在本章学到如何使用对象组合的方式,做到在运行时装饰类。一旦你熟悉了装饰的技巧,你将能够在不修改任何底层代码的情况下,给你的(或别人的)对象赋予新的职责。
本章目录如下:
一、阶段一
二、阶段二
三、阶段三
四、java中的装饰者
五、模式问答
六、设计原则总结
本章需求是设计一个星巴克点咖啡应用,并通过代码迭代、优化过程得出前人的设计经验之一装饰者模式。首先我们先分析需求的变化之处:创造新调制方式的咖啡和原料价格变化。我们所设计的系统必须松耦合,且能够很好的适应变化才行。下面以代码的迭代演化过程为线索介绍。
一、阶段一
需求分析(最差设计):首先,我们要明确咖啡是一个对象,是一个由各种原料调配出来的对象,所以不能像超市购物一样罗列所有原料来计算价钱,所以每一种咖啡都对应一个类。阶段一不把公共操作抽象出来继承,每种咖啡各自实现自己的功能。类图如下:
缺点:
1、代码复用度很低,几乎没有。
2、弹性非常低,一点也不能应对变化。比如某一调料价格变化后,更改每个咖啡价格的工作是灾难性的。
二、阶段二
需求分析:把公共操作抽象出来,通过继承实现每种咖啡。类图如下:
缺点:只能适应部分变化,如调料价格变化;但是有些变化不适应,如创造新调制方式的咖啡时不能复用代码。最终要的是违反了“开放—关闭”原则。且子类数量还是爆炸式增长。
三、阶段三
继承的缺点+组合的优点:利用继承设计子类的行为,是在编译时静态决定的,而且所有的子类都会继承到相同的行为。然而,如果能够利用组合的做法扩展对象的行为,就可以在运行时动态地进行扩展。即通过动态地组合对象,可以通过写新代码来添加新功能,而无须修改现有代码。这样就没有改变现有代码,那么引进bug或产生意外副作用的机会将大幅度减少。
我的理解:组合并不是抛弃继承,它是多个继承的组合,等于说以前那个庞大的继承被拆分了。
如何评作设中的好坏?对于变化是否能做到使原有代码免于修改!
何为变化?从不同纬度看增、删、改、查!
设计原则4(开放—关闭原则):类应该对扩展开放,对修改关闭。该原则目标是允许类容易扩展,在不修改现有代码的情况下,就可搭配新的行为。这样的设计具有弹性,可以接受新的功能来应对需求的改变。
阶段三需求分析:以饮料为主体,然后在运行时以调料来“装饰”饮料。可以把装饰对象理解为一个空心管,只要符合指定接口的对象都可以插入空心管进行包装,包装后的对象符合指定接口就可以被再次包装。比如要制作一个加奶泡(Whip)加摩卡(mocha)的深焙咖啡(DarkRoast),那么包装图如下:
阶段三的类图如下:
读者可能不太理解上图,下面我们说明一下装饰者模式是什么,然后读者就明白了。
装饰者模式定义:装饰者模式动态地将责任附加到对象上。若要扩展功能,装饰者提供了比继承更有弹性的替代方案。装饰者模式的类图如下:
下面说明阶段三的代码:
======================类图中的Beverage基类=================
public abstract class Beverage {
String description = "Unknown Beverage";
public String getDescription() {
return description;
}
public abstract double cost();
}
======================类图中的调料抽象类CondimentDecorator=================
public abstract class CondimentDecorator extends Beverage {
public abstract String getDescription();//使所有的调料装饰者都必须重新实现该方法,因为在调料具体实现类中我们要返回调料名+被装饰者的getDescription()对象,所以此处必须抽象化,以保证子类必须实现该方法
}
======================类图中的饮料实现类=================
public class Espresso extends Beverage { //浓缩咖啡
public Espresso() {
description = "Espresso";
}
public double cost() {
return 1.99;
}
}
public class HouseBlend extends Beverage {//综合咖啡
public HouseBlend() {
description = "House Blend Coffee";
}
public double cost() {
return .89;
}
}
public class DarkRoast extends Beverage {//深焙咖啡
public DarkRoast() {
description = "Dark Roast Coffee";
}
public double cost() {
return .99;
}
}
public class Decaf extends Beverage {//低咖啡因
public Decaf() {
description = "Decaf Coffee";
}
public double cost() {
return 1.05;
}
}
======================类图中的装饰者实现类,即调料实现类=================
public class Mocha extends CondimentDecorator {//摩卡
Beverage beverage;
public Mocha(Beverage beverage) {
this.beverage = beverage;
}
public String getDescription() {
return beverage.getDescription() + ", Mocha";
}
public double cost() {
return .20 + beverage.cost();
}
}
public class Milk extends CondimentDecorator {//奶
Beverage beverage;
public Milk(Beverage beverage) {
this.beverage = beverage;
}
public String getDescription() {
return beverage.getDescription() + ", Milk";
}
public double cost() {
return .10 + beverage.cost();
}
}
public class Soy extends CondimentDecorator {//豆浆
Beverage beverage;
public Soy(Beverage beverage) {
this.beverage = beverage;
}
public String getDescription() {
return beverage.getDescription() + ", Soy";
}
public double cost() {
return .15 + beverage.cost();
}
}
public class Whip extends CondimentDecorator {//奶泡
Beverage beverage;
public Whip(Beverage beverage) {
this.beverage = beverage;
}
public String getDescription() {
return beverage.getDescription() + ", Whip";
}
public double cost() {
return .10 + beverage.cost();
}
}
======================供应咖啡,即点餐=================
public class StarbuzzCoffee { public static void main(String args[]) {
//浓缩咖啡,不需要调料
Beverage beverage = new Espresso();
System.out.println(beverage.getDescription() + " $" + beverage.cost());
//深焙咖啡,加双份摩卡,加一份奶泡
Beverage beverage2 = new DarkRoast();
beverage2 = new Mocha(beverage2);
beverage2 = new Mocha(beverage2);
beverage2 = new Whip(beverage2);
System.out.println(beverage2.getDescription() + " $" + beverage2.cost());
//综合咖啡,加豆浆、摩卡、奶泡
Beverage beverage3 = new HouseBlend();
beverage3 = new Soy(beverage3);
beverage3 = new Mocha(beverage3);
beverage3 = new Whip(beverage3);
System.out.println(beverage3.getDescription() + " $" + beverage3.cost());
}
}
四、java中的装饰者
java中最常见的装饰者io类图如下:
Java IO引出装饰者模式的“缺点”:
(1)、装饰者模式常常造成设计中有大量的小类,可能会造成使用此API程序员的困扰。但是,现在你已经了解了装饰者的工作原理,以后当使用别人的大量装饰的API时,就可以很容易地辨别出他们的装饰者类是如何组织的,以方便用包装方式取得想要的行为。
(2)、采用装饰者在实例化组件时将增加代码的复杂度。因为一旦使用装饰者模式,不只需要实例化组件,还要把此组件包装进装饰者中,可能会有很多个。但是,可以通过工厂模式(Factory)/生成器模式(Builder)封装装饰者模式的创建过程来解决该问题。
下面我们编写一个自己的java/io装饰者,代码如下
public class LowerCaseInputStream extends FilterInputStream {
public LowerCaseInputStream(InputStream in) {
super(in);
}
public int read() throws IOException {
int c = in.read();
return (c == -1 ? c : Character.toLowerCase((char)c));
}
public int read(byte[] b, int offset, int len) throws IOException {
int result = in.read(b, offset, len);
for (int i = offset; i < offset+result; i++) {
b[i] = (byte)Character.toLowerCase((char)b[i]);
}
return result;
}
}
public class InputTest {
public static void main(String[] args) throws IOException {
int c;
try {
InputStream in = new LowerCaseInputStream( new BufferedInputStream( new FileInputStream("test.txt")));
while((c = in.read()) >= 0) {
System.out.print((char)c);
}
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
五、模式问答
1、如何让设计的每个部分都遵循开放-关闭原则?
答: 通常,你办不到。要让00设计同时具备开放性和关闭性,又不修改现有的代码,需要花费许多时间和努力。一般来说,我们实在没有闲工夫把设计的每个部分都这么设计(而且,就算做得到,也可能只是一种浪费)。遵循开放-关闭原则,通常会引入新的抽象层次,增加代码的复杂度。你需要把注意力集中在设计中最有可能改变的地方,然后应用开放-关闭原则。
2、使用装饰者模式,你必须管理更多的对象,所以犯错的机会会增加。那么如何避免呢?
装饰者通常是用其他类似于工厂或生成器这样的模式来包装的。一旦我们讲到这两个模式,你就会明白具体的组件及其装饰者的创建过程,它们会“封装得很好” ,所以不会有这种问题。
3、装饰者知道这一连串装饰链条中其他装饰者的存在吗?
不能。装饰者该做的事就是增加行为到被包装对象上。当需要窥视装饰者链中的每一个装饰者时,这就超出了他们的天赋了。
六、设计原则总结
设计原则1:找出应用中可能需要变化之处,把它们独立出来,不要和那些不需要变化 T个T的代码混在一起。该设计原则作用: “把会变化的部分取出并封装起来,以便以后可以轻易地改动或扩充此部分,而不影响不需要变化的其他部分”。
设计原则2:针对接口编程,而不是针对实现编程。===我的理解是这个原则的使用优先级排在“设计原则1:变化原则”之后,即该原则是一个在大模式已确定需要实现具体类时针对实现类采用的原则,针对范围比较小。
设计原则3:为了交互对象之间的松耦合设计而努力。总结为“多用组合少用继承”!目前我见过两种组合方式:实例作为另外实例的属性,如策略模式、装饰者模式;实例作为另外实例的集合属性的成员,如观察者模式!
设计原则4(开放—关闭原则):类应该对扩展开放,对修改关闭。该原则目标是允许类容易扩展,在不修改现有代码的情况下,就可搭配新的行为。这样的设计具有弹性,可以接受新的功能来应对需求的改变。