Osheep

时光不回头,当下最重要。

设计模式学习(三)——装饰者模式

《设计模式学习(三)——装饰者模式》

一.需求

小王的便利店里卖柴鸡蛋,而且很受欢迎,一下子让小王夫妇打开了思路:原来只要细心观察小区居民的需求,可以卖的东西还有很多。小王的媳妇儿小芳琢磨着在便利店门口卖煎饼果子!小区里住着不少上班族,他们每天早上急匆匆去上班,一个热乎的煎饼果子又营养,又美味,一定会受到大家喜欢的!

说干就干,一周后,小芳的煎饼摊就正式开始营业了。果然,不一会就排起来长队……一份标准的煎饼果子是5元,放一个鸡蛋,一块薄脆,再撒上葱花,香菜,小芳又别处心裁,加了少许榨菜,咬一口,薄脆的酥脆,煎饼的松软,再加上榨菜的清爽……绝了!

民以食为天,小小的煎饼果子也能吃出不少名堂。有的想多加一个鸡蛋,有的想要两块薄脆,有的想多加一根火腿……不一而足,加一个鸡蛋要多加5角,加一块薄脆5角,加一个火腿1元,大家口味各异,把小芳忙的不可开交。算账容易出错不说,还大大影响了效率。

小王不愧是程序员出身,看到老婆的难处,当即决定给老婆写一个软件用来给煎饼果子收款。

二.初步尝试

尝试一

在仔细分析了面临的问题之后,小王觉得:问题主要在于很多顾客想要一些“豪华版”的煎饼果子,例如加一个鸡蛋,加一个火腿,加一块薄脆,在标准的煎饼果子之外,只要把常见的搭配设计好就可以了。小王的设计如下:

// 标准煎饼果子
class ChineseHamburger {
    private static final float price = 5.0f;

    public float cost() {
        return price;
    }
}

// 标准煎饼果子加一份薄脆
class CHAddCrisp extends  ChineseHamburger {
    private static final float price = 0.5f;

    public float cost() {
        return super.cost() + price;
    }
}

// 标准煎饼果子加一个鸡蛋
class CHAddEgg extends  ChineseHamburger {
    private static final float price = 1.0f;

    public float cost() {
        return super.cost() + price;
    }
}

// 标准煎饼果子加一根火腿
class CHAddHam extends  ChineseHamburger {
    private static final float price = 1.0f;

    public float cost() {
        return super.cost() + price;
    }
}
public class ChineseHamburgerAdmin {
    public static void main(String[] args) {
        ChineseHamburger ch = new ChineseHamburger();
        System.out.println("标准煎饼果子售价" + ch.cost());

        ChineseHamburger chAddEgg = new CHAddEgg();
        System.out.println("标准煎饼果子加鸡蛋售价" + chAddEgg.cost());

        ChineseHamburger chAddCrisp = new CHAddCrisp();
        System.out.println("标准煎饼果子加薄脆售价" + chAddCrisp.cost());
    }
}

类图如下:

《设计模式学习(三)——装饰者模式》

运行程序:

标准煎饼果子售价5.0
标准煎饼果子加鸡蛋售价6.0
标准煎饼果子加薄脆售价5.5

这样就大大方便卖夹饼果子了,只要根据客户的需要选择相应的煎饼果子,就可以直接显示价格,不用自己算了,媳妇用着很顺手,小王脸上乐开了花……

没过多久,新的问题又出现了:有的客户想加两根火腿,有的客户想加一个鸡蛋再加一个火腿……客户的需求总是多种多样,又让小王的媳妇应接不暇。

看来,之前设计的那些组合根本不够用,难道还要再加CHAddHamAddEgg、CHAddTwoHam……?显示这不是一个理想的方案,天知道顾客们还会有什么特殊的口味呢?

尝试二
// 标准煎饼果子
class ChineseHamburger {
    private static final float PRICE = 5.0f; // 煎饼果子价格

    private static final float CRISP_PRICE = 0.5f; // 薄脆价格

    private static final float EGG_PRICE = 1.0f; // 鸡蛋价格

    private static final float HAM_PRICE = 1.0f; // 火腿价格

    private int addCrispNum; // 需要加多少薄脆

    private int addEggNum; // 需要加多少鸡蛋

    private int addHamNum; // 需要加多少火腿

    public ChineseHamburger(int addCrispNum, int addEggNum, int addHamNum) {
        this.addCrispNum = addCrispNum;
        this.addEggNum = addEggNum;
        this.addHamNum = addHamNum;
    }

    // 计算煎饼以及附加的原料的价格
    public float cost() {
        return PRICE
                + addCrispNum * CRISP_PRICE
                + addEggNum * EGG_PRICE
                + addHamNum * HAM_PRICE;
    }
}

public class ChineseHamburgerAdmin {
    public static void main(String[] args) {
        ChineseHamburger ch = new ChineseHamburger(0, 0, 0);
        System.out.println("标准煎饼果子售价" + ch.cost());

        ChineseHamburger chAddEgg = new ChineseHamburger(0, 1, 0);
        System.out.println("标准煎饼果子加鸡蛋售价" + chAddEgg.cost());

        ChineseHamburger chAddCrisp = new ChineseHamburger(1, 0, 0);
        System.out.println("标准煎饼果子加薄脆售价" + chAddCrisp.cost());

        ChineseHamburger chAddCrispAddEgg = new ChineseHamburger(1, 1, 0);
        System.out.println("标准煎饼果子加薄脆加鸡蛋售价" + chAddCrispAddEgg.cost());
    }
}

这个方案比之前的方案代码少了很多,既避免了类爆炸,又使得逻辑更灵活,现在随便加多少鸡蛋,加多少火腿都可以从容应对了。

谁知,又过了几周,大家对加火腿,加鸡蛋渐渐的也吃腻了,顾客就是上帝,这帮上帝可是真难伺候啊!小王和媳妇绞尽脑汁想丰富口味,他们又尝试了在煎饼里加肉松,加培根,加土豆丝……

但是这样一来,之前的程序就又需要修改了:需要在ChineseHamburger类中再添加AddMeatFlossNum(加肉松数量)、AddBaconNum(加培根数量)、AddPotatoesNum(加土豆丝数量),并且需要修改cost方法。

这无疑违反了编程的一个基本原则:开闭原则。即代码应该对扩展开放,对修改关闭。换句话说,每当逻辑升级,最好通过新增代码实现,而不修改现有代码。

三.更好的方案

下面我们来看另一种实现。

// 抽象类:面饼,可以是煎饼果子,也可以是卷饼,手抓饼等等
abstract class Biscuit {

    abstract float cost();
}


// 煎饼果子继承面饼抽象类
class ChineseHamburger extends Biscuit{
    private static final float PRICE = 5.0f; // 煎饼果子价格

    @Override
    public float cost() {
        return PRICE;
    }
}

// 面饼装饰者,针对当前场景,没有声明其他方法,只继承Biscuit中的方法
abstract class  BiscuitDecorator extends Biscuit{

}

// 面饼装饰者之一:薄脆
class Crisp extends BiscuitDecorator {

    private static final float PRICE = 0.5f;

    // 需要装饰的面饼
    private Biscuit biscuit;

    public Crisp(Biscuit biscuit) {
        this.biscuit = biscuit;
    }

    @Override
    float cost() {
        return biscuit.cost() + PRICE;
    }
}

// 面饼装饰者之一:鸡蛋
class Egg extends BiscuitDecorator {

    private static final float PRICE = 1.0f;

    // 需要装饰的面饼
    private Biscuit biscuit;

    public Egg(Biscuit biscuit) {
        this.biscuit = biscuit;
    }

    @Override
    float cost() {
        return biscuit.cost() + PRICE;
    }
}

// 面饼装饰者之一:火腿
class Ham extends BiscuitDecorator {

    private static final float PRICE = 0.5f;

    // 需要装饰的面饼
    private Biscuit biscuit;

    public Ham(Biscuit biscuit) {
        this.biscuit = biscuit;
    }

    @Override
    float cost() {
        return biscuit.cost() + PRICE;
    }
}
public class ChineseHamburgerAdmin {
    public static void main(String[] args) {
        Biscuit ch = new ChineseHamburger();
        System.out.println("标准煎饼果子售价" + ch.cost());

        // 用鸡蛋装饰煎饼果子
        Biscuit chAddEgg = new Egg(ch);
        System.out.println("标准煎饼果子加鸡蛋售价" + chAddEgg.cost());

        // 用薄脆装饰煎饼果子
        Biscuit chAddCrisp = new Crisp(ch);
        System.out.println("标准煎饼果子加薄脆售价" + chAddCrisp.cost());

        // 用薄脆装饰已经加了鸡蛋的煎饼果子
        Biscuit chAddCrispAddEgg = new Crisp(chAddEgg);
        System.out.println("标准煎饼果子加薄脆加鸡蛋售价" + chAddCrispAddEgg.cost());
    }
}

当前这种实现的类图如下:

《设计模式学习(三)——装饰者模式》

这种实现的好处在于:灵活、扩展性强。煎饼果子和煎饼果子装饰者都继承自面饼类,装饰者可以随意对煎饼果子进行装饰,如果需要加肉松,之前的代码都无需修改,只要再实现一个肉松类继承BiscuitDecorator即可。同样,如果以后想卖卷饼、手抓饼也没问题,只要实现卷饼类实现Biscuit ,然后用装饰者装饰就可以了。

四.模式总结

想必大家已经看出来了,我们最后使用的方式就是装饰者模式了。

使用场景

当需要动态地增加责任和行为到对象上时。

例如我们都很熟悉了java IO类就大量的使用了装饰者模式,我们一般这样来声明一个输入流:

BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream("file.txt")));

使用InputStreamReader类装饰FileInputStream类,从而完成字节流到字符流的转换,再用BufferedReader类装饰InputStreamReader,实现有缓冲的读,优化性能。

类图
《设计模式学习(三)——装饰者模式》

在装饰者模式中,主要有两种角色:组件和装饰者,装设者和组件继承自同一个父类,因此装饰者可以任意装饰其他组件,充分的利用多态的特性来进行行为扩展。

优点

1.可扩展性,添加新的行为无需修改已有代码
2.灵活性,可以根据自己的需求随意对组件进行装饰,动态的改变行为

缺点

1.会增加很多小类,每一种装饰行为需要实现一个特定的装饰类,增加维护成本
2.如果装饰者链很长,则增加了程序的复杂性,出现问题时排查成本高

参考资料:

1.《Head First设计模式》
2.设计模式读书笔记—–装饰者模式

点赞