【问题标题】:What are the pros and cons of the decorator pattern?装饰器模式的优缺点是什么?
【发布时间】:2013-01-25 16:51:30
【问题描述】:

我正在阅读这篇文章:

http://www.codeproject.com/Articles/479635/UnderstandingplusandplusImplementingplusDecoratorp

我正在考虑在学校项目中实施这种模式。这不是一个要求,所以我可以半途而废。但是,我只是认为这将是一个扩展我的知识和专业知识的好机会。

学校的项目是这样的:创建一个披萨订购应用程序,员工可以在其中输入客户的订单。所以一个披萨,它可以有任意数量的配料。

上面的文章(以及 Head First: Design Patterns 一书中的描述)似乎与我的应用程序完美匹配。

这是我的问题:这似乎不是一个好的模式,原因如下:

每当“披萨店”在他们的菜单中添加新的配料时......他们将不得不添加一个全新的类,并重新编译他们的订购系统并重新分配它们?

我认为问题可能在于我在谷歌上搜索的所有示例都必须处理食物和某种配料。

  1. 我是否只是为这种模式找到了错误类型的示例?
  2. 有哪些更好的例子可以实现这种模式?
  3. 食品行业是其中之一吗,这只是实施 搞砸了?
  4. 这是已经存在但在实际生产代码中几乎没有使用过的模式之一吗?

【问题讨论】:

  • 是的,这是一个很好的应用程序,可以扩展您与装饰器模式有关的知识和专业知识。不,如果您要实现一个实际的商业比萨订购应用程序,您会希望它更多地由数据驱动,而不是由代码驱动,并与您的网站挂钩(反之亦然),以便从菜单中添加/删除配料“实时”(支持挂单,例如下周生日派对的订单,现在已经过时了蔬菜培根浇头)。
  • @franji1 我怎样才能使这个更加数据驱动?我正在考虑一个“浇头”装饰器,而不是每个浇头的单独类,然后根据需要从数据库中创建浇头。但我觉得这会是某种反模式
  • 想一个列出当天所有浇头的文本文件。序列化您的 MenuTopping 对象。阅读该文件并将其显示为 MenuTopping 对象的列表,但这没有使用装饰器模式。但是,我喜欢lazybereovsky 的回答,因为它允许您添加 IPizza 的其他实现,这与数据驱动的实现一样好(如果不是更好的话)。

标签: design-patterns decorator


【解决方案1】:

实际上装饰器允许你在不重新编译源代码的情况下添加一些行为。您可以在您的披萨域中声明IPizza 接口,并在您的应用程序中使用此抽象。然后您可以添加另一个程序集,如果 IPizza 装饰器将具有其他实现,并通过依赖注入将这些实现注入您的应用程序。因此,您无需重新编译您的应用程序或域。

顺便说一句,添加新类比修改现有类更好。你总是可以打破在你修改之前工作的东西。这就是引入单一职责原则的原因。

你的另一个问题:

  1. 不,您正在找到使用装饰器模式的好示例(尤其是 Head First 书中的示例)。另请查看 IO Stream 类及其装饰器以获取灵感。
  2. 模式应该可以解决问题。如果您没有问题,则定位哪个模式,而不是模式的错误使用。
  3. 模式不拘泥于行业。他们坚持你的代码问题(通常是关于代码重复)
  4. 不,这是很好的模式。再想想 .Net 中的 Streams。是生产代码吗?

【讨论】:

  • 感谢您的回答,它让我更深入地了解了这样的事情是如何实现的 (DI)
  • @Steve 我已经为您的其他问题添加了答案,请参阅更新 :)
  • 再次感谢,我猜你回答了我真正试图解决的问题,甚至没有意识到:这是如何在生产环境中实现的。
  • @SergeyBerezovskiy 你是说开闭原则吗?
【解决方案2】:

通常在现实世界的应用程序中,您将处理更抽象的对象(即不是比萨饼和咖啡之类的东西 :))

装饰器模式的一个真实示例是 Java BufferedReader 类。例如,它为FileReader 添加了附加功能。

这样做的好处是您可以在运行时更改行为,并且您不会被许多不同的对象所束缚。

在你的例子中,如果我有四个对象:

Pizza
Tomatoes
Cheese
Mushrooms

然后我可以用四种成分的任意组合制作披萨。否则我将不得不有大量的类来允许这种行为,例如PizzaWithTomatoesAndCheese, PizzaWithTomatoesAndMushrooms

【讨论】:

  • @RomanYakimchuk 是的,Head First 简直太糟糕了。但其他地方的模式示例也往往是人为的、幼稚的,最终毫无意义(无论如何,做披萨意味着什么)。如果没有一个实际的例子,很难判断它应该是一个 Builder 还是一个 Decorator 或其他东西。
【解决方案3】:

我能想到的关于装饰器模式的一个“缺点”是设计是僵化的。

当您装饰一个对象时,您通常不太了解该对象,因为您正在实现一个精简的通用接口。当您第一次实现该接口时,该接口可能适合您的算法,但更改可能会出现问题。

考虑this example 的绘画程序。我喜欢它,因为它说明了我在该模式中看到的两个问题。

装饰器之一是填充形状。如果您对形状唯一了解的是通用界面,您打算如何填充形状?至少如果它是一个只绘制矩形的程序,那么可以在保持相同的类设计的情况下完成一些不太丑的事情,但绘制矩形似乎是一个人为的例子......

第二个是,画一个形状可以做多少事情?基本上只是不同类型的边框和不同类型的填充。也许阴影和文字。关键是,如果只有一种或两种可能的装饰,Decorator 可能是一个过度设计的解决方案。

我认为,一个更重要的问题是每个包装器加上对象都执行其操作(抽象装饰器类中目标方法的实现是delegate.operation())。那么当需求发生变化时会发生什么,我们不应该一个接一个地执行,而是一个应该取代其他的,或者包装器的行为应该取决于被包装对象的某些值。

正如你所说,我怀疑这种模式几乎从未使用过,因为你需要稳定的需求、几种类型的装饰器来证明开销和真正的包装行为,其中被包装者的价值总是无关紧要的。

事实上,我能找到的唯一合理示例是 Java 核心库中的示例,例如 InputStream。

另一个荒谬的例子出现在你提到的 Head First 书中,他们用它来计算咖啡的价格,在那里他们创建了一连串的类来计算咖啡的价格(每个类都专门用于为总成本增加新的成本!)

对我来说,这表明在野外很难找到这种模式的合法用途。

【讨论】:

    【解决方案4】:

    我同意使用装饰器模式仅出于一个原因:

    • 代码更改管理方案中的 SRP。请参阅有效使用遗留代码第 72 页上的优秀示例。当您希望在单元测试上下文中划分您的更改时(尤其是对于脆弱的遗留代码),Decorator 非常棒。它非常适合关注点分离,但与所有此类更改一样,它是有代价的(见下文)。

    我不同意使用装饰器模式有几个原因,在更一般的情况下:

    • 它可能很慢,尤其是在代码的性能关键部分使用时。重复 许多装饰器的嵌套会导致生成的程序集中出现重复的JMPs,这可能会对性能产生严重影响。
    • 当您必须读取/解析多重嵌套的装饰器并确定其执行顺序时,这就像剥洋葱的层一样(按堆栈术语,先序或后序深度优先遍历 - 或甚至两者兼而有之)。
    • 这种“哇,您可以使用装饰器组装任何您喜欢的排列组合!”的想法!可以通过其他方式解决,例如优先组合而不是继承,在这种方式中,我们详尽地组合structs 或类,以便它们可以容纳该类所需的所有组合可能性;例如游戏中的玩家角色可能拥有枪支和/或他们驾驶的车辆;所以要知道这些是什么,我们为两者提供字段(这些可能是嵌套的 structs 或指向其他 structs 类的指针/引用,单独分配),无论是否使用。这在游戏的实体组件系统中很常见。
    • 避免了类的泛滥,这对您来说可能是一件好事,也可能不是一件好事。

    归根结底,组合是非常专业化的,忽略了 SRP,但归根结底,大多数代码都是为专门目的而编写的,而不是为了无休止的重复使用。

    归根结底,我很乐意在积极开发/原型设计功能期间使用 Decorator,但我的目标是在功能集确定后将其切换为更具体的硬编码解决方案。

    【讨论】:

    • 根据定义装饰器是指attach additional responsibilities to an object dynamically. Decorators provide a flexible alternative to subclassing for extending funtionality。装饰器是关于多态性和动态组合。您不能像您在第 3 点中描述的那样替换这种模式 - 这是一个完全不同的解决方案。使用装饰器,您可以在运行时组合不同的对象链,而无需预先硬编码任何东西,并且您通常会实现一个接口并通过组合使用它
    • @ironstone13 就该级别的静态/动态而言,没有区别:您仍然必须在编译之前编写类,以启用新的运行时动态功能。由于粒度,gamedev 解决方案在运行时可能的排列更加动态。唯一的区别是装饰器的子类化将事物更多地分散到内存和(代码)缓存中;如果你曾经在 C/ASM (OOless) 中实现过基本的 OO,你就会明白这一点。尝试查找并了解 vtables 和子类化如何在幕后工作(在本机代码中)。想了解更多?取得联系。
    • 我仍然认为从静态/动态的角度来看还是有区别的:1)您可以通过插件模式使用任何程序集中的类,您不仅限于您的代码 2)装饰器的组合可以是动态的,基于配置、用户输入等 3)动态组合的组合数量远大于静态子类化。现在关于内存布局——这对于游戏开发来说可能很关键,但对于典型的企业架构来说,这是可以忽略不计的,总是有更多的完全影响瓶颈。 with the gamedev solution, due to granularity 是什么意思?
    猜你喜欢
    • 2012-08-15
    • 2010-10-06
    • 2012-01-09
    • 1970-01-01
    • 2014-01-24
    • 2023-04-06
    • 2010-10-11
    • 2016-11-25
    • 2010-09-10
    相关资源
    最近更新 更多