生活中的设计模式之装饰器模式

情景

假设,不久前你在饿了么开了一家面店,只卖一种面既原味面,但生意却很火爆。昨天,你查看店铺的留言时发现:有很多客户要求增加鸡蛋和番茄等配菜。
为了不流失客户,你决定通过继承的方式新增鸡蛋面和番茄面,来满足客户需求。扩展后的菜单如下:

avatar

新品推出后,得到了部份客户的一致好评,但也有给差评的。差评的原因大多都是因为无法组合配菜,其中一条差评留言是:无法下单两个鸡蛋的番茄面。
为了不影响店铺的口碑,你决定再次通过继承新增番茄鸡蛋面和两个鸡蛋的番茄面。菜单如下:

avatar

问题

显然,继承将功能(配菜)和对象(面)静态的绑定在了一起,使原本可选的功能失去了动态组合的灵活性,导致无法动态地组合已有的功能来扩展对象的功能。
如果可选的功能(配菜)很多,且可以相互组合;那么通过继承的方式会导致类膨胀,代码重复、难以维护等问题。所以,这时我们应该考虑另一种更灵活的方式——装饰器模式。

方案

avatar

装饰器模式通过一个独立类即装饰器包装对象的方式,给对象扩展新功能。
装饰器即对象的功能,它和对象之间是一种组合关系而非继承关系,且装饰器之间可嵌套。
装饰器实现了和原对象一致的接口,所以它可以替原对象接收客户请求,并在转发请求前后对其功能进行扩展。

avatar

public class Client {

    public static void main(String[] args){
        //制作二个鸡蛋的番茄鸡蛋面

        //原味面
        OriginalNoddles noddles = OriginalNoddles();
        //第一个鸡蛋面装饰器
        EggDecorator eggDecorator = new EggDecorator(noddles);

        //第二个鸡蛋面装饰器
        EggDecorator eggDecoratorTwo = new EggDecorator(eggDecorator);

        //番茄装饰器
        TomatoDecorator noddlesWithTwoEggAndTomato = new TomatoDecorator(eggDecoratorTwo);
    }
}

结构

avatar

抽象构件(Component):定义了具体构件和抽象装饰器需要实现的接口。

具体构件(ConcreteComponent):被包装的原始对象,实现了Component接口。

抽象装饰器(Decorator):抽象类,持有一个指向被包装对象的引用。

具体装饰器(ConcreteDecorator):实现了具体的扩展功能,在被包装的对象方法调用前后进行功能扩展。

第一步:创建抽象构件。


public interface Component {
    public void operation();
}

第二步:创建具体构件。


public class ConcreteComponent implements Component{

    @Override
    public void operation() {
        System.out.println("the original operation");
    }
}

第三步:创建抽象装饰器,并实现抽象构件接口。


public abstract class Decorator implements Component{

    private final Component component;

    public Decorator(Component component){
        this.component = component;
    }

    @Override
    public void operation() {
        this.component.operation();
    }
}

第四步:创建具体装饰器并继承抽象装饰器。


public  class ConcreteDecoratorA extends Decorator{

    public ConcreteDecoratorA(Component component) {
        super(component);
    }

    @Override
    public void operation() {
        super.operation();
        additionalFunction();
    }

    protected void additionalFunction(){
        System.out.println("this is an additional functionA ");
    }
}


第五步:使用具体装饰器包装具体构件。


public  class ConcreteDecoratorA extends Decorator{

    public ConcreteDecoratorA(Component component) {
        super(component);
    }

    @Override
    public void operation() {
        super.operation();
        additionalFunction();
    }

    protected void additionalFunction(){
        System.out.println("this is an additional functionA ");
    }
}


第六步:使用具体装饰器扩展对象的功能。


public class Client2 {

    public static void main(String[] args) {
        //被装饰者
        Component p = new ConcreteComponent();
        //使用装饰器A包装对象p
        Component a = new ConcreteDecoratorA(p);
        //使用装饰器B包装对象a
        Component b = new ConcreteDecoratorB(a);
        //执行操作
        b.operation();
    }
}

应用

假设,我们有一个发送短信的服务类,它可以根据具体的短信通道来发送验证码和活动通知。短信通道我们接入了两个,一个是腾讯短信通道,一个是阿里短信通道。短信服务类如下:


public enum Channel {
    ALI,TENXUN
}

public interface MessageService {

    public String send(Message message,Channel channel);

}

public class SMSService implements MessageService{

    @Override
    public String send(Message message, Channel channel) {
        //......
        if(Channel.ALI.equals(channel)){
            aliChannel.send(message);
        }else if(Channel.TENXUN.equals(channel)){
            tenxunChannel.send(message);
        }
        //......
        //0001表示对方服务异常
        return "0001";
    }
}

现在,我们希望给短信服务扩展一个功能:当阿里或腾讯的短信服务因异常收不到我们发送的短信时,及时使用另一个短信通道重试一次。

第一步:创建抽象的消息服务装饰器MessageServiceDecorator。


public abstract class MessageServiceDecorator implements MessageService{
    private final MessageService messageService;

    public MessageServiceDecorator(MessageService messageService){
        this.messageService = messageService;
    }
    @Override
    public String send(Message message, Channel channel) {
        return messageService.send(message,channel);
    }
}

第二步:创建具体装饰类重试消息服务RetryMessageService。


public class RetryMessageService extends MessageServiceDecorator{

    public RetryMessageService(MessageService messageService) {
        super(messageService);
    }

    @Override
    public String send(Message message, Channel channel) {
        String result = super.send(message, channel);
        //当前短信通道异常,切换另一个通道
        if("0001".equals(result)){
             Channel anotherChannel = Channel.ALI;
             if(anotherChannel==channel){
                 anotherChannel=Channel.TENXUN;
             }
             return super.send(message, anotherChannel);
        }
        return result;
    }
}

第三步:客户类使用带重试功能的短信服务发送短信,使用没有被装饰的短信服务发送营销活动短信。


public class Client {

    public static void main(String[] args){

        SMSService smsService = new SMSService();
        //发送营销短信
        smsService.send(new Message(),Channel.ALI);

        //用重试装饰器包装短信服务对象
        RetryMessageService retryMessageService = new RetryMessageService(smsService);
        retryMessageService.send(new Message(),Channel.ALI);
    }
}

通过装饰器我们就给短信服务添加了一个可选的扩展功能"切换通道重试",其它的功能如黑名单、频次控制等都可以用同样的方式进行扩展。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,539评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,911评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,337评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,723评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,795评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,762评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,742评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,508评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,954评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,247评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,404评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,104评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,736评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,352评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,557评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,371评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,292评论 2 352

推荐阅读更多精彩内容