结构型-装饰者模式
# 总览
在实践生产中,新需求在软件的整个生命过程中总是不断出现的。当有新需求出现时,就需要为某些组件添加新的功能来满足这些需求。添加新功能的方式有很多,我们可以直接修改已有组件的代码并添加相应的新功能,这显然会破坏已有组件的稳定性,修改完成后,整个组件需要重新进行测试,才能上线使用。这种方式显然违反了“开放-封闭”原则。
另一种方式是使用继承方式,我们可以创建子类并在子类中添加新功能实现扩展。这种方法是静态的,用户不能控制增加行为的方式和时机。而且有些情况下继承是不可行的,例如已有组件是被final关键字修饰的类。另外,如果待添加的新功能存在多种组合,使用继承方式可能会导致大量子类的出现。
装饰器模式能够帮助我们解决上述问题,装饰器可以动态地为对象添加功能,它是基于组合的方式实现该功能的。在实践中,我们应该尽量使用组合的方式来扩展系统的功能,而非使用继承的方式。通过装饰器模式的介绍,可以帮助读者更好地理解设计模式中常见的一句话:组合优于继承。
核心角色:
Component(组件): 组件接口定义了全部组件实现类以及所有装饰器实现的行为。
ConcreteComponent(具体组件实现类) :具体组件实现类实现了Component接口。通常情况下,具体组件实现类就是被装饰器装饰的原始对象,该类提供了Component接口中定义的最基本的功能,其他高级功能或后续添加的新功能,都是通过装饰器的方式添加到该类的对象之上的。
Decorator( 装饰器) : 所有装饰器的父类, **它是一个实现了Component接口的抽象类,并在其中封装了一个Component对象,也就是被装饰的对象。**而这个被装饰的对象只要是Component类型即可,这就实现了装饰器的组合和复用。
ConcreteDecorator: 具体的装饰器实现类,该实现类要向被装饰对象添加某些功能。
在Java IO包中,大量应用了装饰器模式,我们在使用Java IO包读取文件时,经常会看到如下代码:
FileInputStream并没有缓冲功能,每次调用其read()方法时都会向操作系统发起相应的系统调用,当读取大量数据时,就会导致操作系统在用户态和内核态之间频繁切换,性能较低。BufferedInputStream是提供了缓冲功能的装饰器,每次调用其read()方法时,会预先从文件中获取一部分数据并缓存到BufferedInputStream的缓冲区中,后面连续的几次读取可以直接从缓冲区中获取数据,直到缓冲区数据耗尽才会重新从文件中读取数据,这样就可以减少用户态和内核态的切换,提高了读取的性能。
使用装饰器模式的有两个明显的优点:
相较于继承来说,装饰器模式的灵活性更强,可扩展性也强。正如前面所说,继承方式会导致大量子类的情况。而装饰者模式可以将复杂的功能切分成一个个独立的装饰器,通过多个独立装饰器的动态组合,创建不同功能的组件,从而满足多种不同需求。
当有新功能需要添加时,只需要添加新的装饰器实现类,然后通过组合方式添加这个新装饰器即可,无须修改已有类的代码,符合“开放-封闭”原则。
但是,随着添加的新需求越来越多,可能会创建出嵌套多层装饰器的对象,这增加了系统的复杂性,也增加了理解的难度和定位错误的难度。