结构型设计模式--装饰模式
结构型设计模式–装饰模式
欢迎长按图片加好友,我会第一时间和你分享持续更多的开发知识,面试资源,学习方法等等。
装饰模式(Decorator Pattern)是一种结构型设计模式,它允许向一个现有对象添加新的功能,而不改变其结构。这种模式可以将功能拆分成多个独立的类,使得它们可以以灵活的方式进行组合,从而增强代码的可扩展性和维护性。
装饰模式的生活案例:咖啡馆点单
假设我们来到一家咖啡馆点咖啡,这家咖啡馆的菜单上有各种基础咖啡,比如美式咖啡、浓缩咖啡等。除了基础咖啡,顾客还可以为咖啡添加各种配料,例如牛奶、糖浆、奶油等。每种配料都可以单独添加,互不干扰,价格也会有所不同。
用装饰模式来解释这个案例:
- 基础组件(Component):
咖啡是一个“基础组件”,比如一杯美式咖啡或一杯浓缩咖啡。每种基础咖啡都有其固定的价格和描述。
- 具体组件(Concrete Component):
美式咖啡和浓缩咖啡都是具体组件,它们分别代表不同的基础咖啡种类。
- 装饰类(Decorator):
配料(如牛奶、糖浆、奶油等)就是装饰类。它们负责为基础咖啡添加新的功能(即口味或价格)。
- 具体装饰类(Concrete Decorator):
每一种配料(如牛奶、糖浆等)都是一个具体装饰类,它们都继承自装饰类,并为基础咖啡对象添加具体的额外功能(如增加描述、增加价格)。
实际操作示例:
顾客A点了一杯美式咖啡(基础组件),并选择加牛奶(具体装饰类),最后的订单描述可能是“美式咖啡,添加牛奶”,价格会是基础美式咖啡的价格加上牛奶的价格。
顾客B点了一杯浓缩咖啡(基础组件),选择加奶油和糖浆(两个具体装饰类),最后的订单描述可能是“浓缩咖啡,添加奶油,添加糖浆”,价格会是基础浓缩咖啡的价格加上奶油和糖浆的价格。
优点:
- 灵活性:可以根据需求组合不同的配料,无需修改基础咖啡的代码。
- 可扩展性:可以轻松添加新的装饰类(配料),不会影响现有的代码结构。
- 职责单一:将每个装饰功能独立封装在类中,遵循单一职责原则。
这个咖啡馆点单的例子很好地展示了装饰模式的核心思想:在不改变对象结构的前提下,灵活地扩展功能,给对象动态地添加新行为。
装饰模式概述
装饰模式可以在不改变一个对象本身功能的基础上给对象增加额外的新行为。在现实生活中,这种情况也到处存在。例如一张照片,可以不改变照片本身,给它增加一个相框,使得它具有防潮的功能,而且用户可以根据需要给它增加不同类型的相框,甚至可以在一个小相框的外面再套一个大相框。
**装饰模式是一种用于替代继承的技术,它通过一种无须定义子类的方式来给对象动态增加职责,使用对象之间的关联关系取代类之间的继承关系。**在装饰模式中引入了装饰类,在装饰类中既可以调用待装饰的原有类的方法,还可以增加新的方法,以扩充原有类的功能。
装饰模式定义如下:装饰模式(Decorator Pattern),动态地给一个对象增加一些额外的职责,就增加对象功能来说,装饰模式比生成子类实现更为灵活。
装饰模式是一种对象结构型模式。在装饰模式中,为了让系统具有更好的灵活性和可扩展性,通常会定义一个抽象装饰类,而将具体的装饰类作为它的子类。装饰结构图如图所示:
在装饰模式结构图中包含以下4个角色:
Component
(抽象构件):它是具体构件和抽象装饰类的共同父类,声明了在具体构件中实现的业务方法。它的引入可以使客户端以一致的方式处理未被装饰的对象以及装饰之后的对象,实现客户端的透明操作。ConcreteComponent
(具体构件):它是抽象构件类的子类,用于定义具体的构件对象,实现了在抽象构件中声明的方法,装饰器可以给它增加额外的职责(方法)。Decorator
(抽象装饰类):它也是抽象构件类的子类,用于给具体构件增加职责,但是具体职责在其子类中实现。它维护一个指向抽象构件对象的引用,通过该引用可以调用装饰之前构件对象的方法,并通过其子类扩展该方法,以达到装饰的目的。ConcreteDecorator
(具体装饰类):它是抽象装饰类的子类,负责向构件添加新的职责。每一个具体装饰类都定义了一些新的行为,可以调用在抽象装饰类中定义的方法,并可以增加新的方法用以扩充对象的行为。
由于具体构件类和装饰类都实现了相同的抽象构件接口,因此装饰模式以对客户透明的方式动态地给一个对象附加上更多的责任。换言之,客户端并不会觉得对象在装饰前和装饰后有什么不同。装饰模式可以在不需要创造更多子类的情况下,将对象的功能加以扩展。
Java 实现装饰模式(基于继承)
在这个例子中,我们将创建一个基础类(Coffee),然后通过继承这个类来创建具体的咖啡和装饰类。
- 基础组件类(
Base Component
):Coffee
是一个基本的咖啡类,定义了咖啡的描述和价格。 - 具体组件类(
Concrete Components
):Americano
和Espresso
是两种具体的咖啡类型,分别设置了各自的描述和价格。 - 装饰类(
Decorator
):CoffeeDecorator
是一个抽象类,继承自Coffee
并持有一个Coffee
对象的引用,用于在不改变原有咖啡对象的情况下添加新功能。 - 具体装饰类(
Concrete Decorators
):MilkDecorator
、SyrupDecorator
和WhipCreamDecorator
是具体的装饰类,它们在构造函数中调用父类的方法以获取基础咖啡的描述和价格,然后添加各自的描述和价格。
通过使用继承来实现装饰模式,扩展了咖啡类的功能,增加了不同的装饰选项。这样不仅保留了装饰模式的灵活性,还展示了如何通过继承来增强对象的行为。
Java 实现装饰模式(基于组合)
通过组合实现装饰模式是最常见的方式,因为这种方式提供了最大的灵活性和可扩展性。下面是一个基于组合的Java实现,仍然使用咖啡馆点单的案例。
- 基础组件接口(
Component
):Coffee
接口定义了咖啡的基本行为(获取描述和价格)。 - 具体组件类(
Concrete Components
):Americano
和Espresso
是两种具体的咖啡类型,它们实现了Coffee
接口,分别定义了自己的描述和价格。 - 装饰器基类(
Decorator
):CoffeeDecorator
是装饰器基类,实现了Coffee
接口,并持有一个Coffee
对象的引用。装饰器类将所有Coffee
方法委托给被装饰的咖啡对象,并提供一个扩展功能的框架。 - 具体装饰类(
Concrete Decorators
):MilkDecorator
、SyrupDecorator
和WhipCreamDecorator
是具体的装饰类。它们通过组合基础组件对象来动态地添加新功能(例如添加牛奶、糖浆或奶油)。
通过这种基于组合的实现,装饰模式可以动态地向对象添加功能,而不需要修改现有的类。相比基于继承的实现方式,基于组合的装饰模式具有更好的灵活性和可扩展性,因为它允许在运行时对对象的行为进行更细粒度的控制和更动态的组合。
能否在装饰模式中找出两个独立变化的维度?试比较装饰模式和桥接模式的相同之处和不同之处。
在装饰模式中,我们可以找到两个独立变化的维度:
- 基础对象的类型:例如,在咖啡店的案例中,基础对象可以是不同类型的咖啡,如美式咖啡 (
Americano
)、浓缩咖啡 (Espresso
) 等。 - 装饰器的类型:这些是可以动态添加到基础对象上的装饰,如牛奶 (
MilkDecorator
)、糖浆 (SyrupDecorator
)、奶油 (WhipCreamDecorator
) 等。
这两个维度相互独立地变化:可以在任意一种咖啡类型上添加任意组合的装饰器,而不必更改基础类或装饰器类的实现。装饰模式通过组合这些维度,使得程序可以在运行时灵活地增加功能。
回想一下上篇文章的桥接模式。桥接模式是用于分离抽象和实现,使它们能够独立变化,强调结构的解耦。而装饰模式用于动态扩展对象的功能,强调增强功能的灵活性。
相同点
- 解耦抽象与实现:
两种模式都旨在解耦不同的概念或维度,使其可以独立变化。装饰模式通过将基础对象与装饰功能分离来实现功能扩展;桥接模式通过将抽象部分与实现部分分离来允许它们各自独立变化。
- 使用组合:
两者都使用了组合而非继承的方式来实现扩展性。装饰模式通过将装饰器对象组合到基础对象中来扩展其功能;桥接模式通过将实现对象组合到抽象类中来实现它们的分离。
- 实现动态扩展:
两种模式都支持在运行时动态扩展或更改对象的行为。装饰模式通过动态添加装饰器来扩展功能;桥接模式通过不同的实现组合来改变抽象的行为。
不同点
-
意图
- 装饰模式的主要目的是动态地为对象添加新功能,而不改变对象的结构。装饰模式着重于“扩展功能”。
- 桥接模式的主要目的是将抽象与其实现分离,使它们可以独立变化。桥接模式着重于“分离接口和实现”。
-
应用场景:
- 装饰模式适用于需要在运行时根据需求动态地添加或删除对象功能的场景,如UI组件、数据流处理等。
- 桥接模式适用于一个类存在多个变化维度的场景,并且需要独立地变化这些维度,如操作系统与设备的兼容性、图形抽象与渲染方式的实现等。
-
结构差异:
- 装饰模式通常会有一个基础组件接口(或基类)和多个具体组件以及多个装饰器类。装饰器类会继承基础组件接口,并在运行时将装饰器组合到组件中。
- 桥接模式通常包含两个层次的类结构:抽象部分和实现部分。抽象部分持有一个指向实现部分的引用,两者通过接口或抽象类进行分离和关联。
-
使用方式:
- 在装饰模式中,装饰器和基础组件是同一种类型,装饰器的作用是增强或者扩展基础组件的功能。
- 在桥接模式中,抽象和实现是两个独立的层次,抽象定义了高层次的控制逻辑,具体实现类负责底层的具体行为。
两者都是灵活设计模式的重要组成部分,通过组合而非继承的方式提高了系统的可扩展性和灵活性。
装饰模式总结
装饰模式降低了系统的耦合度,可以动态地增加或删除对象的职责,并使得需要装饰的具体构件类和具体装饰类可以独立变化,以便增加新的具体构件类和具体装饰类。在软件开发中,装饰模式应用较为广泛,例如在Java IO中的输入流和输出流的设计、javax.swing包中一些图形界面构件功能的增强等地方都运用了装饰模式。
主要优点
装饰模式的主要优点如下:
- 对于扩展一个对象的功能,装饰模式比继承更加灵活性,不会导致类的个数急剧增加。
- 可以通过一种动态的方式来扩展一个对象的功能。通过配置文件可以在运行时选择不同的具体装饰类,从而实现不同的行为。
- 可以对一个对象进行多次装饰。通过使用不同的具体装饰类以及这些装饰类的排列组合,可以创造出很多不同行为的组合,得到功能更为强大的对象。
- 具体构件类与具体装饰类可以独立变化,用户可以根据需要增加新的具体构件类和具体装饰类,原有类库代码无须改变,符合开闭原则。
主要缺点
装饰模式的主要缺点如下:
- 使用装饰模式进行系统设计时将产生很多小对象。这些对象的区别在于它们之间相互连接的方式有所不同,而不是它们的类或者属性值有所不同。大量小对象的产生势必会占用更多的系统资源,在一定程度上影响程序的性能。
- 装饰模式提供了一种比继承更加灵活机动的解决方案,但同时也意味着比继承更加易于出错,排错也很困难。对于多次装饰的对象,调试时寻找错误可能需要逐级排查,较为烦琐。
适用场景
在以下情况下可以考虑使用装饰模式:
- 在不影响其他对象的情况下,以动态、透明的方式给单个对象添加职责。
- 当不能采用继承的方式对系统进行扩展或者采用继承不利于系统扩展和维护时可以使用装饰模式。不能采用继承的情况主要有两类:第1类是系统中存在大量独立的扩展,为支持每一种扩展或者扩展之间的组合将产生大量的子类,使得子类数目呈爆炸性增长;第2类是因为类已定义为不能被继承(如Java语言中的final类)。
案例
Sunny软件公司欲开发一个数据加密模块,可以对字符串进行加密。最简单的加密算法通过对字母进行移位来实现,同时还提供了稍复杂的逆向输出加密和更为高级的求模加密。用户先使用最简单的加密算法对字符串进行加密,如果觉得还不够,可以对加密之后的结果使用其他加密算法进行二次加密,当然也可以进行第3次加密。试使用装饰模式设计该多重加密系统。
设计思路
- 基础组件接口(
Component
):定义一个Encryptor
接口,该接口提供一个encrypt
方法用于加密字符串。 - 具体组件(
Concrete Component
):实现一个基础的简单加密类SimpleEncryptor
,使用移位加密算法。 - 装饰器基类(
Decorator
):实现一个装饰器基类EncryptorDecorator
,持有一个Encryptor
对象的引用,用于在不修改原始对象的情况下添加新功能。 - 具体装饰类(
Concrete Decorators
):实现具体的装饰类ReverseEncryptor
和ModuloEncryptor
,分别用于逆向输出加密和求模加密。
解释
- 基础组件接口(
Component
):Encryptor
定义了加密的通用接口。 - 具体组件(
Concrete Component
):SimpleEncryptor
实现了简单的移位加密算法。 - 装饰器基类(
Decorator
):EncryptorDecorator
是一个抽象类,它持有一个Encryptor
对象的引用,并在encrypt
方法中调用被装饰对象的方法。 - 具体装饰类(
Concrete Decorators
):ReverseEncryptor
和ModuloEncryptor
分别实现了逆向输出加密和求模加密。它们通过组合基础组件对象来动态地添加新功能。
总结
通过使用装饰模式设计多重加密系统,我们可以灵活地组合不同的加密算法,并在运行时动态地增加或改变加密方式,而无需修改现有的加密算法代码。这种设计模式非常适合于需要动态扩展或组合功能的场景。