装饰者模式
1 . 前言
假设现在需要你给一个面馆设计一个订单系统,你会如何设计?
下面列举一个教科书级别的例子,类的设计结构图如下:
- NoodleStore ( 面馆 )是一个抽象类,面馆内所提供的面食都必须继承自此类。
- 这个名为 description ( 叙述 ) 的实力变量,由每个子类设置,用来描述面食,例如:“ Ramen Noodle ”,利用 getDescription ( ) 方法返回此叙述。
- cost ( ) 方法是抽象的,子类必须定义自己的实现。
- 每个子类实现 cost ( ) 来返回面食的价钱。
下面来分析一下这个设计,首先这样的设计模式相信是教科书的典型的例子,用来讲解抽象的知识点,但是把这样的设计模式放在项目中,就可能会发生很多潜在的问题,比如:面馆由于经营很火爆,相继推出了很多新的配菜,那么子类的数量就会无限的增加;在比如:如果面食的价钱改变,那么我们就需要修改现有的代码等,由于项目是变化的,也就是项目需求是随时变化的,我们应该有这样的忧患意识,以便更好的应对后期的维护,所以,对于装饰者模式,就很有必要了解一下了。
2 . 开放-关闭原则
设计原则:类应该对扩展开放,对修改关闭。
解析:我们的目标是允许类容易扩展,在不修改现有代码的情况下,就可搭配新的行为。这样的设计具有弹性可以应对改变,可以接受新的功能来应对改变的需求。
装饰者模式是结构型设计模式之一,其在不必改变类文件和使用继承的情况下,动态地扩展一个对象的功能,是继承的替代方案之一。它通过创建一个包装对象,也就是装饰者来包装真实的对象。
3 . 认识装饰者模式
下面通过一个例子来一步一步解析装饰者模式的实际运用,这里以面食为主体,然后在运行时以配菜来 “装饰” ( decorate )饮料,比如:如果顾客想要鸡蛋牛肉拉面,那么,流程就应该如下所示:
- 拿一个拉面 ( Ramen Noodle ) 对象
- 以鸡蛋 ( Egg ) 对象装饰它
- 以牛肉 ( Beef ) 对象装饰它
- 调用 cost ( ) 方法,并依赖委托 ( delegate ) 将面食的价钱加上去
通过上面的分析之后,我们可以用一个草图来分析上面的流程:
1 . 以 Ramen Noodle 对象开始
2 . 顾客想要鸡蛋 ( Egg ) ,所以建立一个Egg 对象,并用它将 Ramen Noodle 对象包起来
3 . 顾客也想要牛肉 ( Beef ) ,所以需要建立一个 Beef 装饰者,并用它将 Ramen Noodle 对象包起来。注意:Ramen Noodle 继承自 NoodleStore ,且有一个 cost ( ) 方法,用来计算面食的价钱。
4 . 通过调用最外圈装饰者 ( Beef ) 的 cost ( ) 就可以计算出顾客消费的金额了,Beef 的 cost ( ) 会先委托它装饰的对象(也就是 Egg )计算出价钱,然后在加上牛肉 ( Beef ) 的钱。
- 步骤 1 :首先,调用最外层装饰者的 Beef 的 cost ( ) 。
- 步骤 2 :Beef 调用 Egg 的 cost ( ) 。
- 步骤 3 :Egg 调用 Ramen Noodle 的 cost ( ) 。
- 步骤 4 : Ramen Noodle 返回它的价钱8元。
- 步骤 5 : Egg 在 Ramen Noodle 的结果上,加上自己的价钱2元,返回新的价钱为10元。
- 步骤 6:Beef 的 Egg 返回结果上加上自己的价钱5元,然后返回最后的结果15元。
通过上面例子的分析,装饰者模式已经呼之欲出了,下面详解讲解该模式的各个知识点。
4 . 装饰者模式的定义
定义:动态地将责任附加到对象上,若要扩展功能,装饰者提供了比继承更有弹性的替代方案。
下面是装饰者模式的类图:
装饰中模式各个角色的介绍:
- Component :抽象组件,可以是接口或是抽象类,被装饰的原始对象。
- ConcreteComponent :组件具体实现类,Component 的具体实现类,被装饰的具体对象。
- Decorator :抽象装饰者,从外类来拓展 Component 类的功能,但对于 Component 对象来说则无须知道 Decorator 的存在。在它的属性中必然有一个 private 变量指向 Component 抽象组件。
- ConcreteDecorator :装饰者的具体实现类。
5 . 装饰者模式的实际应用
结合上面的例子,首先对号入座,按照装饰者模式的定义来设计类图,如下:
6 . 代码
(1) . 首先是 NoodleStore 类,代码如下:
/*
* NoodleStore 是一个抽象类,有两个方法:getDescription() 和 cost()
* 其中 getDescription() 已经在此实现了,但是 cost() 必须在子类中实现
* */
public abstract class NoodleStore {
String description="this is noodleStore";
public String getDescription()
{
return description;
}
public abstract double cost();
}
(2) . 下面是 FixingsDecorator 类的实现代码:
/*
* 这是 FixingsDecorator(配菜)的实现
* 所有的配菜装饰者都必须重新实现 getDescription() 方法。
* */
public abstract class FixingsDecorator extends NoodleStore{
public abstract String getDescription();
}
(3) . 下面是具体组件实现类 RamenNoodle 类的代码实现:
/*
* 首先让 Ramen_Noodle 扩展自 NoodleStore 类,因为 Egg 是一种配菜
* 为了要设置配菜的描述,所以写了一个构造器
* 最后需要计算 Ramen_Noodle 的价钱,现在不需要管配菜的价钱,直接把 Ramen_Noodle 的价钱 5.0 返回即可
* */
public class RamenNoodle extends NoodleStore{
public RamenNoodle()
{
description="Ramen Noodle";
}
public double cost()
{
return 5.0;
}
}
(4). 下面是具体组件实现类 ShavedNoodles 类的代码实现:
/*
* 首先让 ShavedNoodles 扩展自 NoodleStore 类,因为 Egg 是一种配菜
* 为了要设置配菜的描述,所以写了一个构造器
* 最后需要计算 Ramen_Noodle 的价钱,现在不需要管配菜的价钱,直接把 ShavedNoodles 的价钱 6.0 返回即可
* */
public class ShavedNoodles extends NoodleStore{
public ShavedNoodles()
{
description="Shaved Noodles";
}
public double cost()
{
return 6.0;
}
}
(5). 下面是具体装饰者(配菜)的实现类 类的代码实现:
/*
* Egg 是一个装饰类,所以让它扩展 FixingsDecorator
* FixingsDecorator 扩展自 NoodleStore
* 要让 Egg 能够引用一个 NoodleStore 做法如下:
* (1)用一个实力变量记录NoodleStore(面食),也就是被装饰者
* (2)想办法让被装饰者NoodleStore(面食)被记录到实例变量中,这里的做法是:
* 把面食当作构造器,再由构造器将此面食记录在实例变量中
*
* 要计算带 Egg 配菜的价钱,首先把调用委托给被装饰对象,以计算价钱,然后再加上 Egg 的价钱,得到最后的结果
* */
public class Egg extends FixingsDecorator{
NoodleStore ns;
public Egg(NoodleStore ns)
{
this.ns=ns;
}
public String getDescription()
{
return ns.getDescription()+", Egg ";
}
public double cost()
{
return 2.0+ns.cost();
}
}
(6) . 下面是测试代码:
public class OrderNooleTestDemo {
public static void main(String[] args) {
//点一份拉面,不需要配菜,打印出它的描述和价钱
NoodleStore ns=new RamenNoodle();
System.out.println(ns.getDescription()+"RMB:"+ns.cost());
//点一份刀削面,加鸡蛋和牛肉配菜,打印出它的描述和价钱
NoodleStore ns2=new ShavedNoodles();
ns2=new Egg(ns2);
ns2=new Egg(ns2);
ns2=new Beef(ns2);
System.out.println(ns2.getDescription()+"RMB:"+ns2.cost());
NoodleStore ns3=new RiceNoodles();
ns3=new Egg(ns3);
ns3=new Beef(ns3);
ns3=new Sausage(ns3);
ns3=new Chicken(ns3);
System.out.println(ns3.getDescription()+"RMB:"+ns3.cost());
}
}
运行结果如下图:
7 . 总结
(1)知识要点。
- 继承属于扩展形式之一,但不见得是达到弹性设计的最佳方式。
- 在我们的设计中,应该允许行为可以被扩展,而无须修改现有的代码。
- 组合和委托可用于在运行时动态地加上新的行为。
- 除了继承,装饰者模式也可以让我们扩展行为。
- 装饰者模式意味着一群装饰者类,这些类用来包装具体组件。
- 装饰者类反映出被装饰者的组件类型(事实上,他们具有相同的类型,都经过接口或继承实现)。
- 装饰者可以在被装饰者的行为前面与/或后面加上自己的行为,甚至将被装饰者的行为整个取代掉,而达到特定的目的。
- 你可以用无数个装饰者包装一个组件。
- 装饰者一般对组件的用户是透明的,除非用户程序依赖于组件的具体类型。
- 装饰者会导致设计中出现许多小对象,如果过度使用,会让程序变得很复杂。
(2)使用场景
- 在不影响其他对象的情况下,以动态、透明的方式给单个对象添加职责。
- 需要动态给一个对象增加功能,这些功能可以动态地撤销。
- 当不能采用继承的方式对系统进行扩充或者采用继承不利于系统扩展和维护时。
(3)优点
- 通过组合而非继承的方式,动态地扩展一个对象的功能,在运行时选择不同的装饰器,从而实现不同的行为。
- 有效避免了使用继承的方式扩展对象功能而带来的灵活性差、子类无限制扩张的问题。
- 具体组件类与具体装饰类可以独立变化,用户可以根据需要增加新的具体组件类和具体装饰类,在使用时再对其进行组合,原有代码无须改变,符合“开放封闭原则”。
(4)缺点
- 因为所有对象均继承于 Component ,所以如果 Component 内部结构发生改变,则不可避免地影响所有子类(装饰者和被装饰者)。如果基类改变,则势必影响对象的内部。
- 比继承更加灵活弹性的特性,也同时意味着装饰者模式比继承更加易于出错,排错也很困难。对于多次装饰的对象,调试时寻找错误可能需要逐级查错,较为烦琐。所以,是在必要的时候使用装饰者模式。
- 装饰层数不能过多,否则会影响效率。