设计模式之装饰模式:如何在基础组件上扩展新功能?

在前面几讲中,我们已经学习了结构型模式中的适配器模式、桥接模式和组合模式。而今天我们要介绍的装饰模式看上去和适配器模式、桥接模式很相似,都是使用组合方式来扩展原有类的,但其实本质上却相差甚远呢。

简单来说,适配器模式侧重于转换,而装饰模式侧重于动态扩展;桥接模式侧重于横向宽度的扩展,而装饰模式侧重于纵向深度的扩展。那么装饰模式到底有哪些需要我们重点学习的地方呢?

话不多说,就让我们带着这个问题开始今天的学习吧!

模式原理分析

装饰模式的原始定义是:允许动态地向一个现有的对象添加新的功能,同时又不改变其结构,相当于对现有的对象进行了一个包装。

这个定义非常清晰易懂,因为不能直接修改原有对象的功能,只能在外层进行功能的添加,所以装饰模式又叫包装器模式

下面我们还是直接来看看装饰模式的 UML 图:

图片1.png

从 UML 图中,我们能发现装饰模式的四个关键角色。

  • 组件:作为装饰器类包装的目标类。

  • 具体组件:实现组件的基础子类。

  • 装饰器:一个抽象类,其中包含对组件的引用,并且还重写了组件接口方法。

  • 具体装饰器:继承扩展了装饰器,并重写组件接口方法,同时可以添加附加功能。

接下来我们再来看看它的代码实现:

//组件

public interface Component {

    void excute();

} 

//具体组件

public class BaseComponent implements Component {

    @Override

    public void excute() {

        //do something

    }

}

//装饰器

public class BaseDecorator implements Component {

    private Component wrapper;

    public BaseDecorator(Component wrapper) {

        this.wrapper = wrapper;

    }

    @Override

    public void excute() {

        wrapper.excute();

    }

}

//具体装饰器A

public class DecoratorA extends  BaseDecorator {

    public DecoratorA(Component wrapper) {

        super(wrapper);

    }

    @Override

    public void excute() {

        super.excute();

    }

}

//具体装饰器B

public class DecoratorB extends  BaseDecorator {

    public DecoratorB(Component wrapper) {

        super(wrapper);

    }

    @Override

    public void excute() {

        super.excute();

    }

}

这段代码实现比较简单,组件 Component 定义了组件具备的基本功能,具体组件 BaseComponent 是对组件(接口)的一种基础功能的实现,装饰器 BaseDecorator 中包含 Component 的抽象实例对象,作为装饰器装饰的目标对象,具体装饰器 DecoratorA 和 DecoratorB 继承装饰器 BaseDecorator 来进行具体附加功能的沿用与扩展。

所以说,装饰模式本质上就是给已有不可修改的类附加新的功能,同时还能很方便地撤销

使用场景分析

一般来讲,装饰模式常用的使用场景有以下几种。

  • 快速动态扩展和撤销一个类的功能场景。 比如,有的场景下对 API 接口的安全性要求较高,那么就可以使用装饰模式对传输的字符串数据进行压缩或加密。如果安全性要求不高,则可以不使用。

  • 可以通过顺序组合包装的方式来附加扩张功能的场景。 比如,加解密的装饰器外层可以包装压缩解压缩的装饰器,而压缩解压缩装饰器外层又可以包装特殊字符的筛选过滤的装饰器等。

  • 不支持继承扩展类的场景。 比如,使用 final 关键字的类,或者系统中存在大量通过继承产生的子类。

在现实中有一个很形象的关于装饰器使用场景的例子,那就是单反相机镜头前的滤镜。用过单反相机的同学应该知道,不加滤镜其实不会影响拍照,而滤镜实际上就是一个装饰器,滤镜上又可以加滤镜,这样就做到了不改变镜头而又给镜头增加了附加功能。

这里我们还是通过一个简单的例子来帮助你理解装饰模式的使用场景。假设我们要创建一个文件读写器程序,能够读字符串,又能将读入的字符串写入文件,那具体该怎么做呢?

首先,创建一个抽象的文件读取接口DataLoader,代码如下:

public interface DataLoader {

    String read();

    void write(String data);

}

之后,创建一个具体组件BaseFileDataLoader,重写组件 DataLoader 的读写方法。

public class BaseFileDataLoader implements DataLoader {

    private String filePath;

    public BaseFileDataLoader(String filePath) {

        this.filePath = filePath;

    }

    @Override

    public String read() {

        char[] buffer = null;

        File file = new File(filePath);

        try (FileReader reader = new FileReader(file)) {

            buffer = new char[(int) file.length()];

            reader.read(buffer);

        } catch (IOException e) {

            e.printStackTrace();

        }

        return new String(buffer);

    }

    @Override

    public void write(String data) {

        File file = new File(filePath);

        try (OutputStream fos = new FileOutputStream(file)) {

            fos.write(data.getBytes(), 0, data.length());

        } catch (IOException e) {

            e.printStackTrace();

        }

    }

}

接下来,再创建一个装饰器DataLoaderDecorator,这里要包含一个引用 DataLoader 的对象实例 wrapper,同样是重写 DataLoader 方法,不过这里使用 wrapper 来读写。

public class DataLoaderDecorator implements DataLoader {

    private DataLoader wrapper;

    public DataLoaderDecorator(DataLoader wrapper) {

        this.wrapper = wrapper;

    }

    @Override

    public String read() {

        return wrapper.read();

    }

    @Override

    public void write(String data) {

        wrapper.write(data);

    }



}

紧接着,创建在读写时有加解密功能的具体装饰器EncryptionDataDecorator,它继承了装饰器 DataLoaderDecorator 重写读写方法。不过,需要注意的是,这里新建了 encode 和 dcode 方法来包装它的父类 DataLoaderDecorator 的读写方法,实现在读文件时进行解密、写文件时进行加密的功能。

public class EncryptionDataDecorator extends DataLoaderDecorator{

    public EncryptionDataDecorator(DataLoader wrapper) {

        super(wrapper);

    }

    @Override

    public String read() {

        return decode(super.read());

    }

    @Override

    public void write(String data) {

        super.write(encode(data));

    }

    private String encode(String data) {

        byte[] result = data.getBytes();

        for (int i = 0; i < result.length; i++) {

            result[i] += (byte) 1;

        }

        return Base64.getEncoder().encodeToString(result);

    }

    private String decode(String data) {

        byte[] result = Base64.getDecoder().decode(data);

        for (int i = 0; i < result.length; i++) {

            result[i] -= (byte) 1;

        }

        return new String(result);

    }

}

然后,我们再创建一个压缩和解压的具体装饰器类CompressionDataDecorator,新建 compress 和 decompress 方法用来包装父类 DataLoaderDecorator 的读写方法,也就是在读取时解压、写入时压缩。

public class CompressionDataDecorator extends DataLoaderDecorator{

    public CompressionDataDecorator(DataLoader wrapper) {

        super(wrapper);

    }

    @Override

    public String read() {

        return decompress(super.read());

    }

    @Override

    public void write(String data) {

        super.write(compress(data));

    }

    private String compress(String stringData) {

        byte[] data = stringData.getBytes();

        try {

            ByteArrayOutputStream bout = new ByteArrayOutputStream(512);

            DeflaterOutputStream dos = new DeflaterOutputStream(bout, new Deflater());

            dos.write(data);

            dos.close();

            bout.close();

            return Base64.getEncoder().encodeToString(bout.toByteArray());

        } catch (IOException e) {

            e.printStackTrace();

            return null;

        }

    }

    private String decompress(String stringData) {

        byte[] data = Base64.getDecoder().decode(stringData);

        try {

            InputStream in = new ByteArrayInputStream(data);

            InflaterInputStream iin = new InflaterInputStream(in);

            ByteArrayOutputStream bout = new ByteArrayOutputStream(512);

            int b;

            while ((b = iin.read()) != -1) {

                bout.write(b);

            }

            in.close();

            iin.close();

            bout.close();

            return new String(bout.toByteArray());

        } catch (IOException e) {

            e.printStackTrace();

            return null;

        }

    }

}

最后,我们运行一个单元测试:创建一个具体装饰器,写入的时候先加密再压缩,然后通过普通组件类和具体装饰器类读取进行对比。

public static void main(String[] args) {

    String testinfo = "Name, testinfo\nMia, 10000\nMax, 9100";

    DataLoaderDecorator encoded = new CompressionDataDecorator(

            new EncryptionDataDecorator(

                    new BaseFileDataLoader("demo.txt")));

    encoded.write(testinfo);

    DataLoader plain = new BaseFileDataLoader("demo.txt");

    System.out.println("- 输入 ----------------");

    System.out.println(testinfo);

    System.out.println("- 加密+压缩 写入文件--------------");

    System.out.println(plain.read());

    System.out.println("- 解密+解压 --------------");

    System.out.println(encoded.read());

}

运行结果:

- 输入 ----------------

Name, testinfo

Mia, 10000

Max, 9100

- 加密+压缩 写入文件--------------

Zkt7e1Q5eU8yV0ZwVFQxdnpkeU16LGd6e1Z7VlZVQjFCQkp2NDlSTElSV01KQ3RCeXBSSzRoPj4=

- 解密+解压 --------------

Name, testinfo

Mia, 10000

Max, 9100

总结来说,装饰模式适用于一个通用功能需要做扩展而又不想继承原有类的场景,同时还适合一些通过顺序排列组合就能完成扩展的场景。

为什么使用装饰模式?

分析完装饰模式的原理和使用场景后,我们再来说说使用装饰模式的原因,主要有以下两个。

第一个,为了快速动态扩展类功能,降低开发的时间成本。 比如,一个类 A,有子类 A01、A02,然后 A01 又有子类 A001,以此类推,A0001、A00001……这样的设计会带来一个严重的问题,那就是:当需要扩展 A01 时,所有 A01 的子类和父类都会受到影响。但是,如果这时我们使用装饰器 B01、B02、C01、C02,那么扩展 A01 就会变为 A01B01C01、A01B02C02 这样的组合。这样就能快速地扩展类功能,同时还可以按需来任意组合,极大地节省了开发时间。

第二个,希望通过继承的方式扩展老旧功能。 比如,前面我们说到,当类标识有 final 关键字时,要想复用这个类就只能通过重新复制代码的方式,不过通常这样的类又处于需要对外提供功能的状态,不能轻易修改,而梳理上下文逻辑又费时费力,那么采用装饰模式就是一个很好的选择。因为装饰器是在外层进行扩展,即使功能不合适,也能及时地撤销而不影响原有的功能。所以说,在一些维护系统的升级或重构场景中,使用装饰模式来重构代码,在短期内都能达到快速解耦的效果。

收益什么?损失什么?

使用装饰模式主要有以下四个大的优点。

  • 快速扩展对象的功能。 对于一些独立且无法修改的类来说,当需要在短期内扩展功能时,采用装饰模式能快速有效地扩展功能,同时也不会影响原有的功能。

  • 可以动态增删对象实例的功能。 比如,在上面文件读写器的例子中,我们可以在创建对象的时候再决定是一起使用压缩装饰器和加密装饰器,还是分开使用,或者只是用基本的读写功能。

  • 可以在统一行为上组合几种行为。 装饰模式是对某一个接口行为进行的组合扩展,通过包装的方式不断扩展代码的行为,从而实现了更多行为的组合。

  • 满足单一职责原则。 每一个具体装饰器类只实现一个组件的具体行为,即便附加了新的功能也是围绕着组件的职责而做扩展,保证了职责的单一性。

同样,装饰模式也有一些缺点。

  • 在调用链中删除某个装饰器时需要修改代码。 装饰模式的最大弊端在于,当在某个组件上附加了太多装饰器后,想要删除其中的某个装饰器时,就需要修改前后的装饰器的引用位置,这样容易导致上下文中代码都需要修改的情况,大大增加了出错的可能性。

  • 容易导致产生很多装饰对象,增加代码理解难度。 由于使用了组合方式,并且在调用时使用了链式结构,这样间接增加了很多装饰器对象,而一旦不了解装饰模式的特性,就很容易误解为多个对象的参数调用,增加了代码的理解难度。

  • 增加问题定位和后期代码维护成本。 虽然装饰模式使用的组合方式比继承更加灵活,但同时也会增加代码的复杂性,在维护代码时会增加问题定位难度,同时调试时也需要逐级排查,比较烦琐,增加了后期代码维护成本。

总结

装饰模式就像是我们送人礼物时的“包装盒”,我们可以选择各种各样的包装盒,还可以在包装盒里嵌套包装盒。

装饰模式在结构上体现为链式结构,通过在外层不断地添加具体装饰器类来对原有的组件类进行扩展,这样在保证原有功能的情况下,还能额外附加新的功能。这也是学习和理解装饰模式的核心所在。

虽然装饰模式的原理和使用都很简单,但是有时链式结构本身会让代码调用链条变得很长,变成了一种对原有组件接口的定制化开发。因此,一般情况下不建议装饰器超过 10 个,如果超过还是要考虑重构组件功能。除此之外,对于没有上下逻辑的装饰器,也要尽量避免使用装饰模式

课后思考

装饰模式的使用非常简单,但是为什么不应该频繁地使用装饰模式呢?

欢迎留言分享,我会第一时间给你回复。

在下一讲,我会接着与你分享“门面模式:如何实现 API 网关的高可用性?”这个话题,记得按时来听课!

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
+----------------------------------+ webIM(即时网络聊天服务)软件iWebIM简介 +----------------------------------+ 作为一款以解决大型站点高并发高负载问题为主要任务的开源webIM软件,iWebIM功能强大,易于集成。主动易用的可嵌入式设计使他可以方便的和现有所有社区系统集成在一起。 与iwebAx系列其他产品一样,它也基于iweb SuperInteraction(简称iweb SI)框架开发。借助iwebSI平台,站点可以轻松获得支持热插拔及快速增加节点的集群计算与处理能力(分布式计算与存储/高可用性/负载均衡),并且无论交互式服务的请求是来自计算机还是移动终端。 另外,iWebIM还可以通过Jooyea技术团队提供的轻量级的支持库iweb_mini_lib轻松地部署在虚拟主机上。 iWebIM为站点用户提供一个良好的web模式即时聊天扩展应用。iWebIM不仅可以集成到Jooyea团队开发的产品(iweb SNS, iweb Mall&Shop;)中,还可以集成到到任何以用户交流为中心的网站系统上(论坛,博客,闻系统,社区交友等)。 使用它,可以为用户构建一个以好友关系为核心的即时交流网络,也可以把它作为web客服软件来使用。 +----------------------------------+ iwebIM软件环境需求 +----------------------------------+ 1. 可用的 www 服务器,如Apache 、IIS、Lihttpd 等 2. php 5.x 3. MySQL 5.0.x 及以上 推荐使用环境:Apache2.2.x + php 5.2.x + MySQL 5.1.x +----------------------------------+ iwebIM软件的安装 +----------------------------------+ 安装前请先认真阅读license.txt文件的全部内容然后开始安装 请确认本软件包的更日期,如果安装包陈旧或者安装过程中产生问题,建议您的从我们的技术主站上下载最的软件包安装~ 安装过程请参见本目录下 iwebSNS0.9下iwebIM安装步骤 文件夹下的相关内容 jooyea.net主站供用户下载的软件包每天都有更。其他具体问题或建议,请到开发者论坛讨论。
装饰者模式是一种结构型设计模式,它允许将行为动态地添加到一个对象中,同时又不改变其原有的实现。装饰者模式通过创建一个包装对象,将原始对象进行包装,从而在运行时动态地添加功能。 在 JavaScript 中,装饰者模式可以通过对象扩展、类继承或者函数包装等方式来实现。下面以对象扩展为例进行说明: ```javascript // 定义一个基础组件类 class Component { operation() { return "基础功能"; } } // 定义一个装饰者类 class Decorator { constructor(component) { this.component = component; } operation() { return `${this.component.operation()},附加装饰功能`; } } // 使用示例 const component = new Component(); console.log(component.operation()); // 输出:基础功能 const decoratedComponent = new Decorator(component); console.log(decoratedComponent.operation()); // 输出:基础功能,附加装饰功能 ``` 在上面的示例中,`Component` 是基础组件类,`Decorator` 是装饰者类,它接收一个 `Component` 对象作为参数,并在其 `operation` 方法中调用原始对象的 `operation` 方法,并添加了额外的功能。 通过使用装饰者模式,我们可以动态地增加、组合和删除功能,而无需修改现有的代码。这种扩展性和灵活性使得装饰者模式在一些场景下非常有用,比如在不破坏原有类结构的情况下,对现有对象进行功能增强或行为修改。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值