`

装饰器模式--继承的另一种选择

阅读更多

java.io包的困惑

 

对于初识java的程序员来说,甚至已经工作三五年的java老鸟们,对java.io包中各种“流”以及五花八门的api都是浑浑噩噩搞不清(笔者在刚接触java时也经历过同样的迷茫)。但如果你已经熟悉了“装饰器模式”的话,再来看一遍java.ioAPI,就会有一种豁然开朗的感觉。

 

继承是实现类复用的重要手段,但却不是唯一的手段,通过类的关联组合同样可以做到,而且如果使用得当 比通过继承更富有弹性。“装饰器模式”就是通过组合来实现类似复用和包装,这就是OO设计的另一个原则“合成复用原则”:则是尽量使用合成/聚合的方式,而不是使用继承。

 

在讲解java.io包之前,先来熟悉下“装饰器模式”。

 

装饰器模式

 

装饰器模式可以动态的把新的职责添加到对象上,在扩展性方面比通过继承实现扩展更富有弹性。这里关键点是动态,也就是运行时;而继承在编译的时候已经确定了。先来看下类图:

 



 

从类图中可以看到该模式有4类角色:

其中 Component抽象的接口ConcreteComponent0ConcreteComponent1是具体的实现 也就是具体被装饰者;Decorator抽象的装饰者可以是接口或抽象类,它跟被装饰者处在同一层级上;ConcreteDecoratorAConcreteDecoratorBConcreteDecoratorC具体的装饰者在同 他们的主要作用就是对“具体的被装饰者”进行包装,附件新的功能。

 

该模式的核心就是,抽象的装饰者Decorator,它起到承上启下的作用:继承(或实现) Component,使得所有的装饰器被装饰器类型相同(实现针对接口编程);同时Decorator中定义了一个Component类型的成员变量,指向具体的被装饰者,通过“组合”的方式实现对其进行包装。

 

也许你会说这里Decorator是继承自Component,也用到了继承。但这里继承的作用仅仅是为了跟具体的被装饰者类型保持相同,主要还是通过组合的方式(即成员变量)对component成员进行包装。

 

“具体的装饰器”不仅可以包装具体的被装饰者,它还可以多层嵌套包装“具体的装饰器”(最内层必须是具体的被装饰者),从而实现动态的多个附加功能的添加。

 

示例展示

 

本示例场景是做web开发中都会遇到的页面渲染场景:业务需求是要渲染PC页面(电脑版页面)、M页面(移动端页面);同时针对不同的页面需要 缓存到redis、缓存cdn、抓取页面上的sku。这时就产生了一系列的组合,PC页面+reddis缓存+抓取skuPC页面+redis缓存、M页面、M页面+redis缓存 等等一系列的组合。解决办法有三种:

 

方式一:继承,如果为每个类型创建新的子类,势必会产生一些列的子类,而且毫无复用性可言。直接放弃。

 

方式二:继承,当然也许你会想到另外一种解决方式:只创建 PC页面、M页面渲染类,把 reddis缓存、cdn缓存、sku抓取作为单独的方法,再进行一些列的判断 来实现复用:



 

 

这里以及PCRenderServcie实现为例:

public class PCRenderServcie extends RenderService{
    private boolean isCdn;
    private boolean isRedis;
    private boolean isGetSku;
    public void render(){
        System.out.println("完成pc基础页面渲染");
        if(isCdn){
            cdnHandle();
            System.out.println("cdn缓存处理");
        }
 
        if(isRedis){
            redisHandle();
            System.out.println("redis缓存处理");
        }
 
        if(isGetSku){
            skuHandle();
            System.out.println("抓取页面上的sku");
        }
    }
   
    //省略getter setter方法
}

 

这里把RenderService设计成抽象类,并把这些方法cdnHandle()redisHandle()skuHandle()公共方法提取到RenderService中,MRenderServcie的实现与上述PCRenderServcie类似 可以共用这些方法。

 

这种方式从一定程度上 实现了部分代码复用,并且解决方式一中的“子类泛滥”的问题。但缺点也很明显,假设现在要新增一个操作,所有RenderServicePCRenderServcieMRenderServcie都会涉及到修改。无法满足开闭原则,对以前的代码造成破坏。

 

前面两种方式都是尝试使用继承来解决问题,我们可以看到效果都不是很理想。下面来看看今天的主角装饰器模式,是如何解决上述问题的。

 

方式三:装饰器模式,采用装饰器模式,我们首先要区分出业务场景中 那些是被装饰者,哪些是装饰者。从方式二中 其实就可以区分开来:PCRenderServcieMRenderServcie是基础操作 可以被看做是被装饰者;是否cdn缓存、是否redis缓存、是否抓取sku,这些动作可以看做是可有可无装饰者,用来增强被装饰者的基础操作。

 

区分出被装饰者装饰者”后,就可以开始实现了,按照类图的4个角色 分别来实现:

 

抽象的接口 角色

 

首先来看抽象的接口,本实例中该接口中只定义了一个render方法:

public interface RenderService {
    void render();
}
 

 

具体的被装饰者 角色

 

具体的被装饰者角色有两个:PcRenderServcie MRenderServcie,实现了pcm页面渲染的基础操作逻辑:

public class PcRenderService implements RenderService{
public void render() {
    //省略渲染过程
        System.out.println("pc页面渲染");
    }
}
 
public class MRenderServcie implements RenderService{
public void render() {
    //省略渲染过程
        System.out.println("m页面渲染");
    }
 
}
 

 

抽象的装饰者 角色

 

根据上述类图介绍,抽象的装饰者需要继承抽象的接口”RenderService,并且拥有一个“具体的被装饰者”成员变量:

 

public abstract class AbstractDecorator implements RenderService{
    protected RenderService renderService; //“具体的被装饰者”也是RenderService类型
 
    protected AbstractDecorator(RenderService renderService){
        this.renderService=renderService;
    }
}
 

 

具体的装饰者 角色

 

本示例中有三个具体的装饰者:CacheDecorator(增加redis缓存处理)CdnDecorator(增加cdn缓存处理)GetSkuDecorator(增加sku抓取处理)。可以看到装饰者角色的作用 其实就是对被装饰者进行增强,并且可以在运行期选择性的增强。下面分别来看看具体的实现:

 

//redis缓存装饰器
public class CacheDecorator extends AbstractDecorator {
 
    public CacheDecorator(RenderService renderService){
        super(renderService);
    }
 
    public void render() {
        renderService.render();
        redisHandle();
    }
 
    private void redisHandle(){
        System.out.println("渲染完成后缓存到redis");
    }
}
 
public class CdnDecorator extends AbstractDecorator {
//cdn缓存装饰器
    public CdnDecorator(RenderService renderService){
        super(renderService);
    }
 
    public void render() {
        renderService.render();
        cdnHandle();
    }
 
    private void cdnHandle(){
        System.out.println("渲染完成后推送cdn");
    }
}
 
//页面sku抓取装饰器
public class GetSkuDecorator extends AbstractDecorator {
 
    public GetSkuDecorator(RenderService renderService){
        super(renderService);
    }
 
    public void render() {
        renderService.render();
        getSku();
    }
 
    private void getSku(){
        System.out.println("渲染完成后抓取页面sku");
    }
}
 

 

到这里装饰器模式4个角色都已实现完毕,下面来进行测试,测试内容为(其实可以任意的组合):首先创建一个带redis缓存+cdn缓存+页面sku抓取的“m页面;再创建一个带redis缓存+cdn缓存的“pc页面”。测试代码:

 

public class Main {
    public static void main(String[] args) {
 
        //渲染一个 redis缓存+cdn缓存+sku抓取的 m页面
        RenderService mRenderServcie = new MRenderServcie();
        mRenderServcie = new CacheDecorator(mRenderServcie);
        mRenderServcie = new GetSkuDecorator(mRenderServcie);
        mRenderServcie = new CdnDecorator(mRenderServcie);
        mRenderServcie.render();
        System.out.println("        ");
 
        //渲染一个 redis缓存+cdn缓存 pc页面
        RenderService pcRenderServcie = new PcRenderService();
        pcRenderServcie = new CacheDecorator(pcRenderServcie);
        pcRenderServcie = new CdnDecorator(pcRenderServcie);
        pcRenderServcie.render();
    }
}
 

 

运行上述main方法,执行结果如下:

m页面渲染
渲染完成后缓存到redis
渲染完成后抓取页面sku
渲染完成后推送cdn
       
pc页面渲染
渲染完成后缓存到redis
渲染完成后推送cdn

 

 

到这里,也许你已经看到装饰器模式的威力(如果还没看到,只能说明我没有讲好)。现在假设需要为pc页面添加头尾,只需要在加一个“头尾处理装饰器”即可,以前的代码完全不用做任何调整。如果需要再渲染一个微信手Q”页面,只需要新增一个被装饰者,可以复用已有的三个“装饰者”进行任意组合的页面渲染。

 

java.io

 

熟悉了装饰器模式,我们再回顾一下javaio包中的APIJavaio包中的api结构大致如下:

 



 

( 本图来自百度图片,没有列全)

 

把上图分为三层:

第一层:ReaderWriterInputStreamOutputStream,对应“抽象的接口”角色。

 

第二层:FilterOutputStreamFilterInputStreamFilterWriterFilterReader这些带有Filter的类对应抽象的装饰者角色,这里也不一定要使用接口和抽象类,比如FilterOutputStream就是具体的实现类。这也属于装饰器模式。另外InputstreamReader也可以算抽象的装饰者,只是这里只对StreamDecoder类型进行装饰。

 

第二层其他:第二层中除了上述提到的5个类,其他的都可以看做是具体的被装饰者

 

第三层:第三层都是具体的装饰者比如:PrintStreamDateOutputStreamBufferInputSteam等。可以使用这些装饰器,对第二层中的类进行装饰。

 

如果把这个图顺时针旋转90度,你会发现它跟装饰器模式的类图很相似。

最后简单的看下,java.io流的用法:

public static void main(String[] args) throws Exception {

        InputStream inputStream = null;

        try{

 

            //使用第二层中的类 创建“被装饰者”

            inputStream = new FileInputStream("D://persion.txt");

 

            //使用第三层中的类 创建两个“具体的装饰者”

            inputStream = new BufferedInputStream(inputStream);//添加buffer读功能

            inputStream = new DataInputStream(inputStream);//添加读取基本java数据类型功能

 

            //开始读文件

            byte[] bs = new byte[inputStream.available()];

            inputStream.read(bs);

            String content = new String(bs);

            System.out.println(content);

        }finally{

            inputStream.close();

        }

    }

 

可以看到跟上述装饰器模式示例中的用法是一样的。想添加什么功能,就加什么装饰器进行包装即可。

 

小结

 

装饰器模式 可以在满足开闭原则的前提下,对被装饰者在运行时进行动态的包装(功能增强),是使用继承进行扩展的另一种选择。但由此也会产生很多具体的装饰者小类,让人困惑(比如 javaio包),但如果你已经熟悉了装饰器模式,那就不是问题了。

 

最后需要注意的是有些装饰器,是需要有序的进行包装的,这时最好把这部分独立出来进行复用(比如 可以使用工厂方法模式等),防止包装顺序出错。

 

 

  • 大小: 16 KB
  • 大小: 8.4 KB
  • 大小: 161.3 KB
0
0
分享到:
评论

相关推荐

    设计模式专题之(七)装饰模式---设计模式装饰模式示例代码(python--c++)

    在Python中,装饰器是一种特殊类型的函数,可以用来修改其他函数的功能或行为。装饰器函数接收一个函数作为参数,并返回一个新的函数。以下是一个简单的装饰器例子: ```python def my_decorator(func): def ...

    设计模式精解-GoF 23种设计模式解析.pdf

    - **应用场景**:当需要给一个对象添加职责时,且不想通过继承机制来实现时,可以使用装饰器模式。 - **优缺点**:优点在于可以动态地给对象添加功能,不会影响其他对象;缺点是过多的装饰器类会导致代码难以维护。 ...

    设计模式2-结构型模式.doc

    - 意图是将一个类的接口转换为客户期望的另一种接口。 - 主要解决的是在现有对象不能满足新环境接口需求时的问题。 - 实现方法可以是继承或依赖,推荐使用依赖,以保持松耦合。 - 优点包括提高复用性、透明度和...

    后端-设计模式-java-精讲

    - **装饰器模式**: 动态地给一个对象添加一些额外的职责。 - **代理模式**: 提供一个代理对象来控制对目标对象的访问。 - **外观模式**: 为子系统中的一组接口提供一个一致的界面。 - **桥接模式**: 将抽象部分...

    装饰设计模式

    3. **装饰器**:`C2` 和 `C3` 类是具体的装饰器,它们都继承自抽象组件 `A`,并且内部持有另一个 `A` 类型的引用。 4. **具体装饰器**:在 `C2` 和 `C3` 的 `show` 方法中,通过调用内部持有的 `A` 对象的 `show` ...

    《设计模式--基于C#的工程化实现及扩展》.(王翔).rar_0517

    本书可能涵盖了如单例模式、工厂模式、观察者模式、装饰器模式、代理模式、建造者模式、策略模式、状态模式、适配器模式、桥接模式、组合模式、享元模式、模板方法模式、职责链模式等常见的设计模式。每种模式都详细...

    设计模式精解-GoF 23种设计模式解析附

    - **Interpreter模式**:给定一个语言,定义它的文法的一种表示,并定义一个解释器,这个解释器使用该表示来解释语言中的句子。 - **应用场景**:当需要定义一种简单的语言时。 - **优点**:易于扩展文法规则。 -...

    23种设计模式-C++实现.zip

    - **解释器模式(Interpreter)**:给定一种语言,定义它的文法表示,并提供一个解释器来解释该文法中的句子。 - **迭代器模式(Iterator)**:提供一种方法顺序访问聚合对象的元素,而又不暴露其底层表示。 - **...

    python设计模式-练习代码

    5. **装饰器模式**:动态地给一个对象添加一些额外的职责。在Python中,装饰器是一种特殊类型的函数,可以用来修改其他函数的功能或行为。 6. **适配器模式**:将一个类的接口转换成客户希望的另一个接口。适配器使...

    《设计模式--基于C#的工程化实现及扩展》.(王翔)_(0601).rar

    装饰器模式可以作为继承的替代,提供更大的灵活性。 8. 外观模式:提供一个统一的接口,用来访问子系统中的一组接口。它使客户代码与子系统的耦合度降低。 9. 策略模式:定义一系列的算法,并将每一个算法封装起来...

    设计模式-创建型模式、结构型模式和行为型模式

    2. **装饰器模式**: - **简介**:动态地给一个对象添加一些额外的职责。 - **应用场景**:当需要增加对象的功能,而又不想通过继承的方式来扩展功能时。 - **优点**:比生成子类更加灵活。 3. **代理模式**: ...

    抽象工厂模式 - PHP版

    接下来,我们探讨装饰器模式,这是另一个重要的设计模式。装饰器模式允许我们在运行时动态地给对象添加新的行为或责任,而无需修改原有对象的代码。它通过包装一个对象并提供相同的接口来实现这一点,这样就可以在...

    java设计模式选择题复习

    - **装饰器模式** - **外观模式** - **享元模式** - **代理模式** - **迭代器模式** - **观察者模式** - **协调者模式** - **模板方法模式** - **策略模式** - **责任链模式** - **命令模式** - **空对象模式** - **...

    设计模式精解-GoF 23种设计模式解析

    - **3.10 Iterator模式**:迭代器模式提供一种方法顺序访问一个聚合对象中各个元素,而又不暴露该对象的内部表示。在C++中,可以通过定义一个Iterator接口和多个实现了该接口的迭代器类来实现迭代器模式。 - **3.11...

    head first 设计模式-C++实现.zip

    这些代码演示了如何在C++环境中应用设计模式,包括单例模式、工厂模式、观察者模式、装饰器模式、代理模式、命令模式、适配器模式、桥接模式、建造者模式、组合模式、享元模式、策略模式、状态模式、结构型模式(如...

    设计模式(JAVA语言实现)--20种设计模式附带源码PPT模板.pptx

    11. 适配器对象模式:适配器模式的另一种形式,是通过组合的方式,将原有类包装在一个新的对象中,使得新对象符合目标接口的要求。 12. 适配器接口模式:适配器模式的第三种形式,是通过继承或实现一个抽象类或接口...

    设计模式-javaOOAD

    - **迭代器模式**:提供一种方法顺序访问一个聚合对象中各个元素,而又不暴露该对象的内部表示。 #### 结论 设计模式是Java OOAD中的核心组成部分,它们为开发者提供了解决常见问题的标准化方法,有助于构建更健壮...

    java设计模式的应用

    - **迭代器模式**:提供一种方法顺序访问一个聚合对象中各个元素,而又不暴露该对象的内部表示。 #### 三、总结 设计模式不仅是面向对象编程的基础,更是提升代码质量、减少维护成本的有效工具。通过对设计模式的...

    设计模式精解-GoF 23种设计模式解析附C++实现源码

    这种类型的模式属于创建型模式,因为它为创建对象提供了另一种方法。 - **应用场景**: 类初始化需要消耗大量的资源,这个资源包括数据、硬件资源等。 - **优点**: 可以给对象的新实例提供一个初始状态,复用现有...

    PHP设计模式之装饰器模式定义与用法详解

    本文实例讲述了PHP设计模式之...有些设计设计模式包含一个抽象类,而且该抽象类还继承了另一个抽象类,这种设计模式为数不多,而装饰器就是其中之一. 什么时候使用装饰器模式 基本说来, 如果想为现有对象增加新功能而不

Global site tag (gtag.js) - Google Analytics