理解设计模式:使用 Long Night (GOT) 示例的装饰器!
经典的设计模式有23种,详见原著《设计模式:可复用面向对象软件的元素》。这些模式为软件开发中经常遇到的特定问题提供了解决方案。
在本文中,我将描述装饰器模式是什么;以及如何以及何时应用它。
装饰器模式:基本思想
在面向对象编程中,装饰器模式是一种设计模式,它允许动态地将行为添加到单个对象,而不会影响同一类中其他对象的行为。——维基百科
动态地为对象附加额外的职责。装饰器提供了一种灵活的替代子类化方法来扩展功能——设计模式:可复用面向对象软件的元素
此模式的主要特点是它允许你动态地(在运行时)为对象附加额外的职责。因此,此模式解决了两个问题:
-
当您需要能够在运行时为对象分配额外的行为而不破坏使用这些对象的代码时。
-
当无法使用继承来扩展类时。
总而言之,装饰器模式允许在运行时使用聚合而不是继承为对象添加新行为。该模式的 UML 图如下:
类是一个接口,它定义了每个组件或系列Component
中必须实现的不同操作。 类使用组合而非继承来改进组件。因此,类包装了 ,定义了每个组件的通用接口,并将公共操作委托给。最后,被实现用于在运行时向对象添加、修改或删除行为。ConcreteComponent
Decorator
Decorator
Decorator
Component
Decorator
Component
ConcreteDecorator
-
您需要动态、透明地向各个对象添加职责,也就是说,不影响其他对象。
-
您需要添加可以随时撤销的职责。
-
当使用继承的行为非常复杂时,因为必须创建大量的类。
装饰器模式有几个优点,总结如下:
-
由于装饰器使用单一职责,因此代码更易于使用、理解和测试,因为您可以将行为分成几个较小的类(装饰器)。
-
由于使用了聚合,因此对象的行为得到了扩展,而无需创建新的子类。
-
可以在运行时添加或删除对象的职责。
-
可以通过将对象包装到多个装饰器中来组合职责。
现在我将向您展示如何使用 JavaScript/TypeScript 实现此模式。在应用此模式之前,务必先了解您要解决的问题。如果您查看下面的 UML 图,它展示了三个组件(ComponentA、ComponentB 和 ComponentC)的经典继承关系,这些组件继承自一个实现了 Component 接口的组件(ComponentBase)。每个组件都实现了特定的行为和属性,并且彼此不同(遵循里氏替换原则)。
软件不断发展,我们需要具有不同组件的属性和行为的对象。
因此,我们得到以下 UML 图。
出现的第一个问题是类的数量激增。每个组件之间都有一个类来关联。如果出现一个新的组件(ComponentD),由于我们问题的架构基于继承,类的数量会继续激增。最后,针对类数量激增问题,我们可以进行一个小的改进,即重新组织类的继承关系,使所有类都继承自同一个类,如与我们正在解决的问题相关的最后一张图所示。
相关的Component和ComponentBase代码如下:
最后,与每个类相关的代码如下:
解决方案是使用装饰器模式。使用此模式的新 UML 图如下所示:
因此,解决方案是使用聚合而不是继承。在此模式中,组件接口被维护,该接口定义了装饰器和具体组件必须执行的操作。需要注意的是,ConcreteComponent 和 Decorator 类都实现了组件接口。此外,Decorator 类还通过依赖注入的方式拥有一个组件实例。从注释中可以看出,责任委托是通过注入的对象来执行的,或者说是行为的补充。
最后,每个装饰器都实现一个具体的行为,这些行为可以根据需要进行组合。此时,我们应用了单一职责原则,因为每个装饰器只执行一项任务,并承担唯一的职责。
我们现在来看看通过实现此模式生成的代码:
与 ConcreteComponent 组件(装饰器将应用在该组件上的基类)相关的代码如下:
最后,每个装饰器都实现单一功能,例如基于继承的解决方案,但不会出现类的爆炸式增长。
最后,每个装饰器都实现单一功能,与使用基于继承的解决方案完全一样,其优点是不会出现之前的类爆炸式增长。
我创建了几个 npm 脚本,在应用迭代器模式后运行此处显示的代码示例。
npm 运行 example1-问题
npm 运行 example1-装饰器解决方案-1
装饰器模式——示例 2:权力的游戏:漫漫长夜!
想象一下,我们必须模拟《权力的游戏》(GOT)中的漫漫长夜之战,其中我们有以下先决条件:
-
有一些简单的角色(人类)可以攻击、防御,并且随着战斗的进行其生命会减少。
-
最初有一位夜之王,他是一个特殊的异鬼,拥有强大的力量和生命。
-
当人类(简单角色)死亡时,它会在运行时重新转变为异鬼,战斗继续。
-
有两支军队会战斗,直到其中一支被彻底消灭。
-
最初,异鬼的军队仅由夜之君组成。
装饰器模式将允许我们在运行时将 SimpleCharacter 的行为更改为 WhiteWalker。
我们不会展示多个具有不同功能的装饰器,而是展示一个装饰器扩展另一个装饰器的示例(LordNight 从 WhiteWalker 扩展而来)。
在下面的 UML 图中,您可以看到针对该问题提出的解决方案:
好的,第一步是定义将由 SimpleCharacter 和 CharacterDecorator 实现的 Character 接口,如下面的代码所示:
SimpleCharacter 类代表一个基本角色(人类),我们将使用装饰器向其添加/修改行为。
战斗中要用到的方法是 receiveHit,它计算角色被削弱的伤害。这个方法会告诉我们是否需要将 SimpleCharacter 转换为 WhiteWalker。
因此,与 CharacterDecorator 相关的代码如下,将责任委托给 Character:
现在,我们需要实现装饰器的具体实现来解决我们的问题。
WhiteWalker 的攻击修正值总是小于 SimpleCharacter 的修正值。
最后,与夜之主相关的装饰器继承了异鬼的行为,可以在运行时改变简单角色 (SimpleCharacter) 的力量和生命值。请注意,我们没有此类对象的静态类。也就是说,任何基本角色都可以成为夜之主。
我们只需要查看与客户端相关的代码,其中我们已经实现了模拟战斗的基本代码,但真正有趣的是查看如何在运行时将 WhiteWalker 装饰器应用于对象以改变其行为。
我需要一支150人的军队才能打败夜之领主。这比现实系列里的东西更有趣:-P。希望你已经感受到装饰者赋予我们的力量,尤其是在职业爆炸方面。
然而,装饰器的错误使用会导致我们遇到此模式当前存在的问题,因为它被过度使用,而不是创建类或应用更适合问题情况的其他模式。
我创建了一个 npm 脚本,该脚本在应用装饰器模式和 CLI 界面后运行此处显示的示例。
npm 运行 example2-decorator-solution1
结论
装饰器模式可以避免项目中出现大量不必要且僵化的类。这种模式允许我们在运行时更改对象的行为,并允许我们应用两个著名的原则,例如单一职责和开放/封闭。
你可以避免项目中出现大量不必要且僵化的类。此模式允许你在运行时更改对象的行为,并允许你应用两个著名的
最重要的不是像我展示的那样去实现这个模式,而是能够识别这个特定模式能够解决的问题,以及何时应该或不应该实现该模式。这一点至关重要,因为具体实现会根据你使用的编程语言而有所不同。
更多更多更多…
-
《设计模式:可复用面向对象软件的元素》,作者:Gamma、Helm、Johnson 和 Vlissides,Addison Wesley,1995 年
-
https://www.dofactory.com/javascript/decorator-design-pattern
-
https://github.com/sohamkamani/javascript-design-patterns-for-humans#-decorator
-
这篇文章的GitHub分支是https://github.com/Caballerog/blog/tree/master/decorator-pattern
最初于 2019 年 6 月 29 日发布于https://www.carloscaballero.io。
文章来源:https://dev.to/carlillo/understanding-design-patterns-decorator-using-long-night-got-example-276c