关于装饰器设计模式的新手问题


18

我在阅读编程文章,其中提到了装饰器模式。我已经进行了一段时间编程,但没有进行任何形式的教育或培训,但是我正在尝试学习标准模式等。

因此,我抬起头来查看装饰器,并在上面找到了维基百科的文章。我现在了解了装饰器模式的概念,但是我对这段内容感到有些困惑:

例如,考虑一个窗口系统中的一个窗口。为了允许滚动窗口的内容,我们可能希望适当地向其添加水平或垂直滚动​​条。假定窗口由Window类的实例表示,并且假定此类不具有添加滚动条的功能。我们可以创建一个提供它们的子类ScrollingWindow,也可以创建一个ScrollingWindowDecorator将此功能添加到现有的Window对象中。此时,任何一种解决方案都可以。

现在假设我们也希望能够向窗口添加边框。同样,我们原始的Window类不支持。ScrollingWindow子类现在带来了一个问题,因为它有效地创建了一种新型的窗口。如果希望对所有窗口添加边框支持,则必须创建子类WindowWithBorder和ScrollingWindowWithBorder。显然,随着每个新功能的添加,此问题变得更加严重。对于装饰器解决方案,我们只需创建一个新的BorderedWindowDecorator,在运行时,我们可以根据需要使用ScrollingWindowDecorator或BorderedWindowDecorator或两者来装饰现有窗口。

好的,当他们说要向所有窗口添加边框时,为什么不只是在原始Window类中添加功能以允许该选项呢?我所看到的方式是,子类化仅是为类添加特定功能或重写类方法。如果需要向所有现有对象添加功能,为什么不修改超类来这样做呢?

文章中还有另一行:

装饰器模式是子类的替代方法。子类化在编译时增加了行为,并且更改会影响原始类的所有实例;装饰可以在运行时为单个对象提供新的行为。

我不明白他们说“ ...更改影响原始类的所有实例”的原因-子类化如何更改父类?这不是继承的全部重点吗?

我将假设该文章像许多Wiki一样,书写不清晰。我可以在最后一行看到装饰器的用处-“ ...在运行时为单个对象提供新的行为。”

如果不了解这种模式,那么如果我需要在运行时更改单个对象的行为,则可能会在超类或子类中构建一些方法来启用/禁用所述行为。请帮助我真正了解装饰器的用途,为什么我的新手思维有缺陷?


感谢沃尔特(Walter ...)
吉姆(Jim

编辑只是删除​​一般的问候。在问题中使用一个不是标准的SO / SE协议[不用担心,我们不认为直接跳入问题是不礼貌的]
Farrell

酷,谢谢!只是他把它标记为“消除性别”,我恨任何人都认为我是厌女症或某种东西!:)
Jim Jim

我大量使用它们来避免那些称为“服务”的法律程序性内容:medium.com/@wrong.about/…–
Zapadlo

Answers:


13

装饰器模式是一种支持组合而不是继承的模式(另一种可用于学习的OOP范例)

与子类相比,装饰器模式的主要优点是允许更多混合和匹配选项。例如,如果您具有一个窗口可以具有的10种不同的行为,那么这意味着-通过子类化-您需要创建每个不同的组合,这将不可避免地包括大量的代码重用。
但是,当您决定添加新行为时会发生什么?

使用装饰器,您只需添加一个描述此行为的新类即可,就是这样-模式允许您有效地将其插入,而无需对其余代码进行任何修改。
通过子类化,您将面临噩梦。
您问的一个问题是“子类化如何改变父类?” 不是因为它改变了父类。当它说一个实例时,它意味着您已经“实例化”了任何对象[例如,如果您使用的是Java或C#,则使用new命令]。它所指的是,当您将这些更改添加到类中时,即使实际上并不需要它,也没有选择。

或者,您可以将所有功能通过一个标志打开/关闭,以将其所有功能放到一个单独的类中……但这最终会变成一个单个类,该类随着项目的增长而变得越来越大。
以这种方式启动项目,并在达到有效临界质量后将其重构为装饰器模式并不罕见。

应该提出一个有趣的观点:您可以多次添加相同的功能;因此,例如,您可以有一个带有两倍,三倍或任意数量或边框的窗口。

该模式的重点是启用运行时更改:在程序运行之前,您可能不知道窗口的外观,这使您可以轻松地对其进行修改。当然,这可以通过子类完成,但效果不佳。
最后,它允许将功能添加到您可能无法编辑的类中,例如密封/最终类或其他API提供的类


2
谢谢,法雷尔(Farrell)-当您说“或者,您可以将其所有功能都放在一个类中,并通过标志打开/关闭它……但是最终,随着项目的增长,这个类变得越来越大。” 这让我点击了。我认为问题的一部分是我从来没有参加过如此大的装饰,对我来说这很有意义。大胆的想法,我绝对可以看到好处...谢谢!
吉姆

另外,关于密封类的部分很有道理...谢谢!
吉姆(Jim

3

考虑使用子类添加滚动条和边框的可能性。如果需要各种可能性,您将获得四个类(Python):

class Window(object):
    def draw(self):
        "do actual drawing of window"

class WindowWithScrollBar(Window):
    def draw(self):
        Window.draw(self)
        "do actual drawing of scrollbar"

class WindowWithBorder(Window):
    def draw(self):
        Window.draw(self)
        "do actual drawing of border"

class WindowWithScrollBarAndBorder(Window):
    def draw(self):
        WindowWithScrollBar.draw(self)
        WindowWithBorder.draw(self)

现在,在中WindowWithScrollBarAndBorder.draw(),窗口get被绘制了两次,第二次它可能会也可能不会覆盖已经绘制的滚动条,具体取决于实现方式。因此,您的派生代码与另一个类的实现紧密相关,并且每次更改Window类的行为时都必须注意这一点。一种解决方案是从派生类的超类中复制粘贴代码,并根据派生类的需要对其进行调整,但是对超类的每次更改都必须再次复制粘贴并再次进行调整,因此派生类再次是与基类紧密结合(通过复制粘贴调整来实现)。另一个问题是,如果您需要窗口可能具有或可能不具有的另一个属性,则必须将每个类加倍:

class Window(object):
    ...

class WindowWithGimmick(Window):
    ...

class WindowWithScrollBar(Window):
    ...

class WindowWithBorder(Window):
    ...

class WindowWithScrollBarAndBorder(Window):
    ...

class WindowWithScrollBarAndGimmick(Window):
    ...

class WindowWithBorderAndGimmick(Window):
    ...

class WindowWithScrollBarAndBorderAndGimmick(Window):
    ...

这意味着对于具有| f |的一组相互独立的特征f 作为特征的数量,您必须定义2 ** | f | 类,例如 如果您有10个功能,则可以获得1024个紧密相关的类。如果使用装饰器模式,则每个功能都具有自己的独立且松散耦合的类,并且只有1 + | f |。类(在上面的示例中为11)。


瞧,我的想法不是继续添加类,而是将功能添加到原始(子)类中。因此在Window(object)类中:您具有scrollBar = true,border = false,等等。。。答案Farrell向我显示了哪里可能出问题,但是-导致了classes肿的类,例如...感谢您的回答, +1!
吉姆(Jim

好吧,只要我有代表就可以+1!
吉姆(Jim

2

我不是这个特定模式的专家,但是正如我看到的那样,Decorator模式可以应用于您可能无法修改或子类化的类(例如,它们可能不是您的代码并被密封) )。在您的示例中,如果您没有编写Window类而只是在使用它,该怎么办?只要Window类具有接口,并且您可以针对该接口进行编程,Decorator可以使用相同的接口,但是可以扩展功能。

您提到的示例实际上在这里很整洁地涵盖

可以通过使用继承来静态地(在编译时)扩展对象的功能,但是当使用对象时,可能有必要动态地(在运行时)扩展对象的功能。

考虑图形窗口的典型示例。为了扩展图形窗口的功能(例如,通过向窗口添加框架),将需要扩展窗口类以创建FramedWindow类。要创建框架窗口,必须创建FramedWindow类的对象。但是,不可能从纯窗口开始并在运行时扩展其功能以成为框架窗口。

http://www.oodesign.com/decorator-pattern.html

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.