设计模式之装饰者模式

模式定义

装饰模式(Decorator Pattern) :动态地给一个对象增加一些额外的职责(Responsibility),就增加对象功能来说,装饰模式比生成子类实现更为灵活。其别名也可以称为包装器(Wrapper),与适配器模式的别名相同,但它们适用于不同的场合。根据翻译的不同,装饰模式也有人称之为“油漆工模式”,它是一种对象结构型模式。
装饰者模式能够完美实现“对修改关闭,对扩展开放”的原则,也就是说我们可以在不修改被装饰者的前提下,扩展被装饰者的功能。再来看看我们的文件操作代码:

InputStream in = new BufferedInputStream(new FileInputStream(file));

被“包裹”在最内层的InputStream对象是new FileInputStream(file),是基本的文件输入流,用BufferedInputStream对象来扩展它的功能,甚至我们还可以这样:

P.S.注意上面用到的动词——“包裹”,这就是装饰者模式的核心了

InputStream in = new LineNumberInputStream(new BufferedInputStream(new FileInputStream(file)));

继续添加一层新的装饰LineNumberInputStream,以扩展获取行号的功能,当然,我们还可以扩展更多的功能,只要继续添加新的装饰就好了。回过头来想想,我们给FileInputStream添加了一层层装饰,获得了一个个功能,在此过程中,我们实现了功能的动态扩展,但并没有修改被装饰者FileInputStream的任何东西。这就是所谓的“对修改关闭,对扩展开放”原则

模式的动机

一般有两种方式可以实现给一个类或对象增加行为:

  • 继承机制,使用继承机制是给现有类添加功能的一种有效途径,通过继承一个现有类可以使得子类在拥有自身方法的同时还拥有父类的方法。但是这种方法是静态的,用户不能控制增加行为的方式和时机。
  • 关联机制,即将一个类的对象嵌入另一个对象中,由另一个对象来决定是否调用嵌入对象的行为以便扩展自己的行为,我们称这个嵌入的对象为装饰器(Decorator)

装饰模式以对客户透明的方式动态地给一个对象附加上更多的责任,换言之,客户端并不会觉得对象在装饰前和装饰后有什么不同。装饰模式可以在不需要创造更多子类的情况下,将对象的功能加以扩展。这就是装饰模式的模式动机。

模式的结构

装饰模式包含如下角色:
- Component: 抽象构件
- ConcreteComponent: 具体构件
- Decorator: 抽象装饰类
- ConcreteDecorator: 具体装饰类

模式结构图

注意这里一个很重要的点,装饰者与被装饰者要具有相同的超类?后面通过例子来解释这个问题。

一个例子

假设我们要开店卖Milk,可选的配料有摩卡Mocha(巧克力味),咖啡Coffee,冰水IceWater,当然,如果卖得好的话我们还打算引进新的饮料(Orange、Yoghurt等等)以及新的配料(Salt。。玩笑)Milk本身有价格,并且会在节假日打折,各种不同的配料价格也不同,当然,我也可以点一杯加双份IceWater的Milk。。最容易想到的解决方案是:

定义一个Milk类,包含很多属性,例如hasMocha, hasCoffee, hasIceWater(用来表示已添加的配料),还需要discount属性(用来表示折扣信息)、cost属性(用来表示价格)。这就好了吗?不,除此之外我们还需要MochaNum, CoffeeNum, IceWaterNum(用来表示配料的份数,重口味顾客需要双份或者更多的配料。。)

问题解决了,可是这样做真的好吗?

考虑一下这些情况:1.引进一种新的饮料Orange(我们需要定义一个Orange,几乎没有复用的部分,从零开始。。或者,我们可以定义一个Beverage基类,把饮料共有的部分放进去);2.引进一种新的配料Salt(我们必须修改Milk类,添加hasSalt, SaltNum属性以满足加盐Milk需求。。)

现在看来我们的解决方案很差,不能适应任何变化,要扩展功能就可能必须修改已有的封装好的代码,而且还存在一个性能上的问题:计算饮料价格部分需要大量的if…else…结构,使得我们的代码很臃肿,且难以复用(不同饮料配料可能不同,计算价格的方法也不同)。那么,是时候尝试装饰者模式了

所以,我们定义下面的Beverage基类:
Beverage.java

/**
 *
 * 定义Beverage超类,所有具体Beverage和Ingredient都必须扩展自此类
 *
 */
public abstract class Beverage {
    String desc = "Unknown Beverage";//定义饮料相关描述信息
    float cost;//定义饮料的价格

    public abstract float getCost();//定义cost方法返回该饮料的价格,子类必须实现此方法

    public String getDesc(){
        return desc;
    }
}

有了Beverage就可以开始定义被装饰者——Milk:
Milk.java

/**
 * 
 * 定义具体Beverage:Milk类
 *
 */
public class Milk extends Beverage{

    float discount = 1;//定义折扣,节假日Milk可能会打折(默认不打折)

    public float getDiscount() {
        return discount;
    }

    public void setDiscount(float discount) {
        this.discount = discount;
    }

    public Milk(){
        cost = 4.5f;//初始化Milk的价格
        desc = "Milk";//初始化描述信息
    }

    @Override
    public float getCost(){
        return discount * cost;//返回打折后的价格
    }
}

接下来是装饰者,因为装饰者具有一些不同于Beverage的特性,所以我们对其进行抽象:
Ingredient.java

/**
 * 
 * 定义Ingredient佐料类,继承自Beverage(在装饰者模式中,装饰者与被装饰者必须具有相同的超类)
 *
 */
public abstract class Ingredient extends Beverage{

    Beverage beverage;//需要添加该佐料的饮料

    @Override
    public String getDesc() {
        return "(" + desc + ")" + beverage.getDesc();//佐料的描述应当带上括号,以区别佐料与饮料
    }

    @Override
    public float getCost() {
        return cost + beverage.getCost();//配料没有折扣,直接返回其价格 + 饮料价格
    }

    //在此添加其它Ingredient不同于Beverage的属性与行为
}

注意上面的getDesc与getCost方法,我们把计算价格与生成描述信息的责任完全委托给方法调用机制了,以至于代码是如此的简洁。
下面定义具体配料——IceWater,Coffee,Mocha:
IceWater.java

/**
 *
 * 定义配料IceWater冰水
 *
 */
public class IceWater extends Ingredient{

    public IceWater(Beverage bev)
    {
        cost = 0.5f;
        desc = "IceWater";
        beverage = bev;
    }
}

Coffee.java

/**
 * 
 * 定义配料Coffee咖啡
 *
 */
public class Coffee extends Ingredient{

    public Coffee(Beverage bev)
    {
        cost = 3;
        desc = "Coffee";
        beverage = bev;
    }
}

Mocha.java

/**
 * 
 * 定义配料Mocha摩卡
 *
 */
public class Mocha extends Ingredient{

    public Mocha(Beverage bev)
    {
        cost = 2;
        desc = "Mocha";
        beverage = bev;
    }
}

一切准备就绪,我们的Milk小店可以开张了。。

结果示例

public class Test {
    public static void main(String[] args){
        Beverage bev;

        System.out.println("做一杯加摩卡加咖啡的Milk。。");
        bev = new Milk();//先做一杯Milk
        bev = new Mocha(bev);//添加Mocha
        bev = new Coffee(bev);//添加Coffee
        System.out.println(bev.getDesc() + " " + bev.getCost() + "¥");

        System.out.println("做一杯加双份冰水双份摩卡的咖啡Milk。。");
        bev = new Milk();//重新做一杯Milk
        bev = new IceWater(bev);//添加冰水
        bev = new IceWater(bev);//添加冰水
        bev = new Mocha(bev);//添加Mocha
        bev = new Mocha(bev);//添加Mocha
        bev = new Coffee(bev);//添加Coffee
        System.out.println(bev.getDesc() + " " + bev.getCost() + "¥");
        //当然也可以这样写: Beverage bev = new Coffee(new Mocha(new Milk()));
        //对比我们熟悉的方法链: InputStream in = new BufferedInputStream(new FileInputStream(file));
    }
}

结果:

做一杯加摩卡加咖啡的Milk。。
(Coffee)(Mocha)Milk 9.5¥
做一杯加双份冰水双份摩卡的咖啡Milk。。
(Coffee)(Mocha)(Mocha)(IceWater)(IceWater)Milk 12.5¥

设计模式优缺点

缺点其实显而易见——你见过这么长的代码吗?

XObject o = new XDecorator(new XXDecorator(new XXXDecorator(new XXXXDecorator())));

嗯,它只是给被装饰对象做了三次功能扩展而已,当然,还可以更多。。也就意味着可以更长。而且,我们在使用时创建了很多小对象,就像这样:

bev = new Milk();//重新做一杯Milk
bev = new IceWater(bev);//添加冰水
bev = new IceWater(bev);//添加冰水
bev = new Mocha(bev);//添加Mocha
bev = new Mocha(bev);//添加Mocha
bev = new Coffee(bev);//添加Coffee

让一个不熟悉装饰者模式的人来读上面的代码,他能很快弄明白吗?

注意,上面的代码就解释了开篇提到的动词——“包裹”,对吗?

优点:

除了上面提到的动态扩展优点,还有一个更重要的优点就是前面提到的getDesc与getCost方法

没错,我们可以利用这种调用机制来完成我们的操作(在装饰动作前或者装饰动作后添加我们的自定义操作就好了,例子里其实属于在装饰动作后添加操作),我们很轻易的达到了类似于递归的效果

这也就解释了“为什么装饰者与被装饰者要具有相同的超类?”,还需要更多一点的解释:

有一种设计原则是“多用组合,少用继承”,这里我们好像违背了这个原则吧

其实并没有违背原则,装饰者模式中的继承是为了获得类型的匹配,而不是为了利用继承来扩展类的行为,而“多用组合,少用继承”原则省略掉的前提条件是“(当我们需要扩展类的行为时)多用组合,少用继承”。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值