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

        继承是实现代码重用的有力手段,但它并非永远是完成这项工作的最佳工具,使用不当会导致软件变得很脆弱,同时会使子类继承打破了封装性。换句话说,子类依赖于其超类中特定功能的实现细节。超类的实现有可能会随着发行版本的不同而有所变化,如果真的发生了变化,子类可能会遭到破坏,即使它的代码完全没有改变。因而,子类必须要跟着其超类的更新而演变,除非超类是专门为了扩展而设计的,并且具有很好的文档说明。

        比如说现在我有一个做咖啡的需求,根据不同款式的咖啡收费。一开始的设计如下:

设计模式之装饰者模式

        Beverage是咖啡基类,所有做出来的咖啡都需要继承此类。该类的getDescription方法返回description描述,由子类来维护,表示此时做的是何种咖啡;cost方法是抽象方法,需要子类去实现。cost方法表示的是每种咖啡的收费规则。

        当咖啡中可以添加调料的时候,比如:蒸奶、豆浆、摩卡或覆盖奶泡等。咖啡+调料的组合会变得越来越多,如果还用上述的模式,会造成子类爆炸的情况,难于维护。比如说,我新增一种调料,那么又需要添加很多的子类;某种调料的价钱发生变动,则需要修改所有添加了这种调料的咖啡子类的实现。

        此时可以考虑使用装饰者模式来改造代码,类图如下:

 

设计模式之装饰者模式

        CondimentDecorator是装饰者基类,也可以做成接口,它去继承Beverage类(这个例子出自《Head First 设计模式(中文版)》。笔者认为这个例子不太恰当。调料并不是饮料,所以不能用调料基类去继承饮料基类,这是对继承的滥用)。由它派生的装饰者都有一个Beverage的实例变量,用来进行包装。装饰者可以在原有操作的基础上进行改造,添加新的方法实现。

        部分代码如下:

public abstract class Beverage {
 
    String description = "Unknown Beverage";
 
    public String getDescription() {
        return description;
    }
 
    public abstract double cost();
}
public class HouseBlend extends Beverage {
 
    public HouseBlend() {
        description = "House Blend Coffee";
    }
 
    @Override
    public double cost() {
        return .89;
    }
}
public abstract class CondimentDecorator extends Beverage {
 
    @Override
    public abstract String getDescription();
}
public class Mocha extends CondimentDecorator {
 
    Beverage beverage;
 
    public Mocha(Beverage beverage) {
        this.beverage = beverage;
    }
 
    @Override
    public String getDescription() {
        return beverage.getDescription() + ", Mocha";
    }
 
    @Override
    public double cost() {
        return .20 + beverage.cost();
    }
}

        最后的测试代码如下:

public class StarBuzzCoffee {
 
    public static void main(String[] args) {
        Beverage beverage1 = new Espresso();
        System.out.println(beverage1.getDescription() + " $" + beverage1.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());
    }
}

        第一杯Espresso饮料没有添加调料(装饰者);第二杯DarkRoast和第三杯HouseBlend都各自添加了自己的调料。改用装饰者模式来实现,这样的好处是毋庸置疑的:将咖啡和调料分开来实现,能达到解耦的目的。咖啡和调料只需要关注自己的内部实现就可以了。同时装饰者模式符合开放-关闭原则(对扩展开放,对修改关闭),如果我新添加一种咖啡或调料,不需要去修改之前已经实现的代码,只需要继承饮料基类或调料基类就行了。

/**
 * 
 * <p>Classname: BracketsStack </p>
 * <p>Description: 匹配左右括号的堆栈实现</p>
 * @author houyishuang
 * @date 2018年5月31日
 */
public class BracketsStack {

    private static final Logger    LOGGER       = LoggerFactory.getLogger(BracketsStack.class);
    /**
     * 堆栈用List实现(Stack已过时,不建议使用)
     */
    private static List<Character> bracketsList = new LinkedList<>();

    private static class BracketsStackLazyHolder {

        private static final BracketsStack INSTANCE = new BracketsStack();
    }

    private BracketsStack() {}

    public static BracketsStack getInstance() {
        return BracketsStackLazyHolder.INSTANCE;
    }

    /**
     * 
    * <p>Title: push </p>
    * <p>Description: 在堆栈顶部插入一个新元素 </p>
    * @param bracket    参数说明
    * @author houyishuang
    * @date 2018年5月31日
     */
    public void push(char bracket) {
        bracketsList.add(bracket);
    }

    /**
     * 
    * <p>Title: pop </p>
    * <p>Description: 删除堆栈顶部元素并返回 </p>
    * @return    参数说明
    * @author houyishuang
    * @date 2018年5月31日
     */
    public Character pop() {
        if (isEmpty()) {
            if (LOGGER.isErrorEnabled()) {
                LOGGER.error("堆栈元素为空,删除失败");
            }
            throw new IndexOutOfBoundsException("堆栈元素为空,删除失败");
        }
        return bracketsList.remove(bracketsList.size() - 1);
    }

    /**
     * 
    * <p>Title: isEmpty </p>
    * <p>Description: 判断堆栈是否为空 </p>
    * @return    参数说明
    * @author houyishuang
    * @date 2018年5月31日
     */
    public boolean isEmpty() {
        return bracketsList.isEmpty();
    }
}

        上述代码是笔者在实际项目中写过的代码,用BracketsStack包装了LinkedList的部分方法实现,使代码更加灵活、健壮。所以这也被称作包装类(wrapper class)

        在java中也有很多地方用到了装饰者模式,最典型的是java的I/O。通过对输入/输出流进行包装,可以扩展其功能,如下所示:

public class TestFileStream {
 
    public static void main(String[] args) {
        File file = new File("./TestFileStream.java");
        try (FileReader fr = new FileReader(file); BufferedReader br = new BufferedReader(fr)) {
            String line = br.readLine();
            while (null != line) {
                System.out.println(line);
                line = br.readLine();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

       BufferedReader类扩展了FileReader类的读方法,提供了很实用的readLine方法,读取一个文本行,从字符输入流中读取文本,缓冲各个字符,从而提供字符、数组和行的高效读取(这里使用了jdk7中开始的try-with-resource语句,不用再显示写finally子句来关闭资源,try括号内的资源会按顺序自动被关闭,详情请查看相关文章)。

        通过继承来复用代码是面向对象程序设计中被滥用得最多的东西,因为所有的教科书都无一例外的对继承进行了鼓吹从而误导了初学者。类与类之间简单的说有三种关系,Is-A关系、Has-A关系、Use-A关系,分别代表继承、关联和依赖。需要说明的是,即使在Java的API中也有不少滥用继承的例子,例如Properties类继承了Hashtable类,Stack类继承了Vector类,这些继承明显就是错误的,更好的做法是在Properties类中放置一个Hashtable类型的成员并且将其键和值都设置为字符串来存储数据,而Stack类的设计也应该是在Stack类中放一个Vector对象来存储数据。

        只有当子类真正是超类的子类型(subtype)时,才适合用继承。换句话说,对于两个类A和B,只有当两者之间确实存在“is-a”关系的时候,类B才应该扩展类A。如果你打算让类B扩展类A,就应该问问自己:每个B确实也是A吗?如果你不能够确定这个问题的答案是肯定的,那么B就不应该扩展A。如果答案是否定的,通常情况下,B应该包含A的一个私有实例,并且暴露一个较小的、较简单的API:A本质上不是B的一部分,只是它的实现细节而已。

        记住:任何时候都不要继承工具类,工具是可以拥有并可以使用的,而不是拿来继承的。为了防止工具类被继承,可以考虑将工具类的构造器设置为私有的,如下图所示:

设计模式之装饰者模式

 

相关文章: