装饰器模式:为啥我们需要抽象装饰器? [复制]
Posted
技术标签:
【中文标题】装饰器模式:为啥我们需要抽象装饰器? [复制]【英文标题】:Decorator pattern: Why do we need an abstract decorator? [duplicate]装饰器模式:为什么我们需要抽象装饰器? [复制] 【发布时间】:2011-01-02 06:31:49 【问题描述】:这个问题已经被问到here,但不是回答具体问题,而是描述了装饰器模式的工作原理。我想再问一次,因为仅仅通过阅读装饰器模式的工作原理,我并不能立即得到答案(我已经阅读了***的文章和 Head First Design Patterns 一书中的部分)。
基本上,我想知道为什么必须创建一个抽象装饰器类来实现(或扩展)某些接口(或抽象类)。为什么所有新的“装饰类”都不能简单地实现(或扩展)基本抽象对象本身(而不是扩展抽象装饰器类)?
为了更具体,我将使用设计模式书中处理咖啡饮料的示例:
有一个抽象组件类叫做Beverage
简单的饮料类型如HouseBlend
只需扩展Beverage
为了装饰饮料,创建了一个抽象的CondimentDecorator
类,它扩展了Beverage
,并有一个Beverage
的实例
假设我们要添加“牛奶”调味品,创建一个扩展CondimentDecorator
的类Milk
我想了解为什么我们需要CondimentDecorator
类以及为什么Milk
类不能简单地扩展Beverage
类本身并在其构造函数中传递Beverage
的实例。
希望这很清楚......如果不是我只是想知道为什么这个模式需要抽象装饰器类?谢谢。
编辑:我尝试实现这一点,省略了抽象装饰器类,它似乎仍然有效。这个抽象类出现在这个模式的所有描述中,仅仅是因为它为所有新的装饰类提供了一个标准接口吗?
【问题讨论】:
【参考方案1】:它可以使用不同组合中的各种装饰器独立地装饰基类,而不必为每个可能的组合派生一个类。例如,假设您想要您的 Beverage
加牛奶和肉豆蔻。使用基于抽象装饰器类的装饰器,您只需使用Milk
和Nutmeg
装饰器进行包装。如果它是从Beverage
派生的,则必须有一个MilkWithNutmegBeverage
类和一个MilkBeverage
类和一个NutmegBeverage
类。您可以想象随着可能组合数量的增加,这种情况会如何爆炸。使用抽象装饰器实现可以将其减少为每个装饰只有一个装饰器类实现。
【讨论】:
我不确定我是否理解。是的,我从“饮料”派生“牛奶”;但是“牛奶”也被传递了一个“饮料”类的实例,我认为它允许我像使用抽象装饰器类一样包装这些类。 我想说的是,对于抽象装饰器类,所有新的装饰类都继承自它..那么为什么我们不能删除中间人(让它们直接继承自'Beverage' PLUS 像以前一样向他们传递一个饮料实例)? 我想你可以这样做,但作为一个子类,它不需要提供任何额外的行为。从概念上讲,尽管装饰器实现了相同的接口,但它实际上不是被装饰类的实例,而是增强被装饰类的辅助类。以不同的抽象类为基础暴露了这个设计方面,并且更清楚地知道如何添加新的装饰。将装饰实现为子类会模糊这种区别,并使实现对可能无意中破坏装饰模式的子类开放。 @es11:好的,所以你从“饮料”中提取牛奶——“肉豆蔻”呢?如果消费者尝试调用 Nutmeg.Drink() 或更糟的是 Nutmeg.PutOutFire(),会发生什么情况?如果 Nutmeg 是一种饮料,那么它应该能够用作饮料,即使它不被用作饮料的装饰器。通过使 Nutmeg 成为抽象装饰器的实例,您可以防止它被被误认为是抽象类的实例。 从概念上讲,牛奶和肉豆蔻的例子是有道理的。但是,调用者代码怎么会装饰两次呢?比如说,调用 Milk(beverage),然后调用 Nutmeg(beverage),我想它仍然会以肉豆蔻饮料而不是牛奶+肉豆蔻饮料结束..【参考方案2】:实际上我有时也会忽略这个“中间人”抽象(如果你没有很多装饰器组合)。它解耦更多,但也增加了复杂性。在我看来,装饰背后的主要思想是将接口包装在它们自己的实现中。
【讨论】:
【参考方案3】:(你的问题有点晚了..)
我也花了很长时间试图找出答案。在我的例子中,非具体的装饰器扩展了要装饰的类(“decoree”),而不是被装饰器和装饰器共用的接口。
从不同来源阅读后,在我看来,除了 tvanfosson said 之外,让具体装饰器扩展抽象或更一般的装饰器的原因是我们不重复“委托”代码一遍又一遍地。
[Beverage]<———————[(abstract) CondimentDecorator]<—————[Milk]
[decoree ]——————<>[ adds code to efficiently ]
[ forward calls to decorated ]<—————[LemonJuice]
[ instance of Beverage ]
在您的情况下,您的抽象装饰器将扩展被装饰者并实现与被装饰者相同的接口,将所有方法调用委托/转发给它。然后,您可能构建的具体装饰器只需要实现那些您想要做一些不同于将方法调用转发给被装饰者的方法。
我希望我很清楚.. :-S
就我个人而言,我对需要在装饰器中重复被装饰者的界面感到有点失望。这增加了一些耦合,因为任何时候被装饰者的接口发生变化(比如获得更多方法),装饰者都需要跟上。
在 php 5.3.2 中(是的,我知道您的问题与 Java 有关),但是,应该可以动态捕获所有方法调用并将它们全部转发,而装饰器不需要知道正在调用哪些方法(但是,以一种不是非常有效的方式,必须使用反射 API)。我想这在 Ruby 和其他语言中已经是可能的了。
PS:这是我在 SO 中的第一个答案! ^_^
【讨论】:
我有点希望有一个(高效的)功能,比如异常处理,调用堆栈的每一级都有机会“捕获”一个方法调用,如果它没有处理程序,把它传出去。那将是完全解耦的。 (和地狱调试)。 在 Java 中,您可以动态处理带有Dynamic Proxy 的接口的方法调用。或者,或者当你有一个类而不是一个接口时,cglib 的Enhancer 可以在运行时生成实现类,将调用委托给你提供的处理程序。【参考方案4】:我也在想同样的事情。回到源头,GOF 设计模式,我在装饰器章节的“实现”下看到了这一点:
“省略抽象装饰器类。当您只需要添加一个职责时,无需定义抽象装饰器类。当您处理现有的类层次结构而不是设计新的层次结构时,通常会出现这种情况。在在这种情况下,您可以将 Decorator 将请求转发到组件的职责合并到 Concrete Decorator 中。”
所以至少在这种情况下,GOF 似乎同意你的观点 :-)
我不知道“一个责任”是什么意思。我不确定“一个以上的责任”是否意味着一个具体的装饰者有一个以上的责任或多个具体的装饰者,每个都有它的一个责任。不管怎样,我不明白为什么抽象装饰器是必要的。我的猜测是 tvanfosson 的答案(在他对自己的答案的评论中)是正确的——一旦你开始创建许多装饰类,它就阐明了将它们分组到超类下的设计决策。另一方面,在只有一个类的情况下,如果添加第二个类,它只是作为基础组件和装饰器之间的无意义的中间人(话虽如此,很可能您可能希望在某个时候添加更多内容,因此即使在单一情况下也包含抽象装饰器可能会更好......)
无论如何,这似乎与使设计清晰有关,而不是代码工作与否之间的区别。
【讨论】:
【参考方案5】:在装饰器模式的情况下,继承被用于类型匹配。这不是子分类的典型原因。子类化的正常原因是继承行为
为了明确区分,使用 CondimentDecorator 子类 Beverage 是有意义的,因为 CondimentDecorator 类明确区分了饮料实现(如 DarkRoast)和调味品(如 Mocha)。例如,假设您的任务是通过查看代码为 StarBuzz 提供菜单。通过查看基类,您会立即知道哪些是“基础”饮料以及哪些调味品。 DarkRoast 的基类是饮料。 Mocha 的基类是 CondimentDecorator。
不过,我认为在 Java 中将 Beverage 抽象类实现为接口可能更有意义。根据这本书,代码没有使用这种方法,因为 StarBuzz 已经有一个抽象 Beverage 类(第 93 页),而抽象基础组件是实现该模式的历史方法。
【讨论】:
【参考方案6】:迟到一年半总比没有好:
不需要特定接口的装饰器的基类。
但是,拥有以下内容非常有用:
一方面,作为一种记录从它派生的类是相关接口的装饰器的方法
但主要是因为装饰器通常不需要为装饰界面的每个方法添加功能。
因此,基装饰器类允许派生装饰器仅实现它们实际需要添加某些功能的接口的那些方法,而将其余方法留给基类以提供默认实现。 (这只是将调用委托给受邀者。)
与编写从头开始实现装饰接口的装饰器相比,编译器要求您为接口的每个方法提供一个实现,无论您的装饰器是否会为其添加任何功能。
就是这么简单,真的。
【讨论】:
为什么装饰器不能是装饰器的子类?因此,我们不再需要一个提供非装饰实现(简单委托)的抽象装饰器,并且如果一个被装饰者将添加新方法,装饰器仍然可以在不修改它的情况下使用(添加委托)! 嗯?这没有多大意义。从装饰者的角度来看,被装饰者只是一个接口。所以,装饰器不能是被装饰者的子类,因为你不能子类化接口。您可以实现一个接口,而这正是装饰器所做的,根据定义。 现在,如果您在这里尝试建议的是选择一些实现接口的具体类(假设一个已知的)并将其子类化以制作装饰器,这个想法是荒谬的,因为装饰器将继承它不需要、不应该使用并且没有业务处理的字段和功能。我想我不明白你的问题,所以也许你应该开始一个关于堆栈溢出的新问题,并尽量清楚你的意思,以便得到正确的答案。 会更容易。如果您可以借助示例来解释这一点。这似乎很难理解。【参考方案7】:BaseDecorator (CondimentDecorator) 类强制 ConcreteDecortor (Milk) 在构造函数的输入中有一个基本抽象类 (Brevrage)。
如果 BaseDecorator 类不存在,它不会强制您实现需要输入 BaseAbstractClass 的装饰器。强制类的输入和输出是装饰器模式的目标。
(我认为您的示例来自 Head First Design Pattern,在他们的示例中,BaseDecorator 类没有此构造器)。
【讨论】:
实际上,您不能将构造函数添加到抽象类。所以“强制”似乎不是。 @JasonHuang:错了。在 Java 中,您可以将构造函数添加到抽象类。【参考方案8】:基础装饰器可以更轻松地创建额外的装饰器。想象一下,Beverage 有几十个抽象方法,或者是一个接口,比如stir()、getTemperature()、drink()、pour() 等。然后你的装饰器都必须实现这些方法,除了将它们委托给包装好的饮料外,你的 MilkyBeverage 和 SpicyBeverage 都有所有这些方法。
如果您有一个具体的 BeverageDecorator 类,该类通过简单地将每个调用委托给包装的 Beverage 来扩展或实现 Beverage,则子类可以扩展 BeverageDecorator 并仅实现它们关心的方法,而让基类来处理委托。
如果 Beverage 类(或接口)获得新的抽象方法,这也可以保护您:您需要做的就是将该方法添加到 BeverageDecorator 类。如果没有它,您将不得不将该方法添加到您创建的每个装饰器中。
【讨论】:
【参考方案9】:也许为时已晚,但这是 gof 单一责任声明的重点。
假设抽象装饰器还具有组件(抽象)不拥有的附加方法,其中由装饰器负责。
这确保组件始终可重用,因为它应该实现组件的所有抽象/接口,但不能实现装饰器的额外责任。
任何使用组件(抽象)的程序都可以确保组件在使用方面(进入运行对象)和组件(抽象)的具体创建相应地工作。因此,这将确保程序的完整性和易于开发和扩展应用程序/程序。
因此,装饰者将离开那些爱,然后就好像作为他自己的对象以任何可能的方式使用,包括与上述类似的方式。 例如。正在装饰的装饰器。(只是一个想法)
很好,不是吗? (鼓掌)
【讨论】:
我认为您可能正在讨论以下问题:“确保组件始终可重用”,但我不能完全理解您在说什么。你能详细说明一下吗?以上是关于装饰器模式:为啥我们需要抽象装饰器? [复制]的主要内容,如果未能解决你的问题,请参考以下文章