使用列表而不是装饰器模式?

Posted

技术标签:

【中文标题】使用列表而不是装饰器模式?【英文标题】:Using Lists Instead of Decorator Pattern? 【发布时间】:2017-09-19 19:12:10 【问题描述】:

“Head First: Design Patterns”一书中的装饰器模式用例让我产生了这个问题。我会试着写下来:

这是一个咖啡店系统,有一些咖啡和很多调味品 你可以把它们放进去(额外收费),你需要能够订购 并收取客户想要的任何调味品的咖啡,以及 以避免完全混乱(例如布尔值来跟踪 调味品)使用装饰图案。我们有一个抽象的饮料 类,每种类型的咖啡作为具体成分和每种调味品 作为包装饮料的混凝土装饰师,像这样:

因此我们有以下流程返回咖啡成本:

我的问题是:为什么不用列表而不是装饰器来实现呢?我们可以在每个饮料中有一个调味品列表,并通过遍历列表来计算成本。要订购咖啡,我们只需要实例化一次并添加所需的调味品,避免以下声明:

// Using second image example
Beverage beverage = new DarkRoast(beverage);
beverage = new Mocha(beverage);
beverage = new Whip(beverage);

除此之外,我们将在操作方面拥有更大的灵活性,例如为不包括调味品的咖啡提供折扣,一旦我们没有装饰员来包装咖啡。这是一个长期研究的问题,我知道我遗漏了一些东西或者我做错了什么,所以如果你对此有任何想法,我很想知道并进一步讨论。

【问题讨论】:

这与您必须按成本减去或增加的折扣或税费清单无关。这是关于您使用装饰器模式处理的行为。而且,相信我,使用列表而不是继承来处理这些行为更难。 你能举个例子吗?或者可能是另一个随机案例?我真的无法想象会发生这种情况。 这似乎是一个更适合softwareengineering.stackexchange.com 的问题。就我个人而言,我认为这是对装饰器的糟糕使用(我认为***也有这样的例子)并且绝对可以通过更易于管理的方式通过列表来解决。 java.io.InputStream 是一个很好用的装饰器示例。但是,您已经可以看到有一个用户不同意我的观点,这就是为什么这可能与 SO 无关。 这是一个非常人为的例子。书本练习经常是。你的直觉很好,这表明你理解了这个例子。使用继承来模拟调味品对我来说是一种反模式,但作为一个易于获取的示例,它可以工作。在现实生活中,我也会使用列表。当您增强现有对象的行为时,最好使用装饰器模式。例如将哈希映射组合在一起或将默认值添加到哈希映射。 InputStream 很好,因为您正在用更高级别的抽象包装一个较低级别的源代码。您从字节数组、URL、文件等源开始,然后说“忘记那个。现在我只想从中获取对象”并将其包装在 ObjectInputStream 中。使用 InputStream,您实际上希望或多或少地忘记您正在装饰的对象。以饮料为例,在某些时候您将需要例如迭代所有的成分,然后把它们放在一个表什么的,然后装饰器只是一个笨重的链表。 【参考方案1】:

Decorator 模式是关于在运行时装饰(增强)具有附加功能的对象。

假设你已经有一个类,我们称之为类A,它实现了一个接口IA。现在,如果需要添加一个附加功能,我们希望它有一个方法,someAlignFeatureToA(),这在A 中没有。现在您可以选择扩展类A。另一种方法是Composition,它应该优于Inheritance。您可以将A 类的对象包装在另一个类B 中,实现与A 相同的Interface,即IA。这种方式对于客户端代码来说很容易接受 B 类的对象,因为它具有与 A 相同的接口。(假设代码写得很好,这取决于抽象(接口IA)而不是具体类A)。

这样继承链不会太长,您可以在运行时轻松添加附加功能。

现在来回答您的问题:是的,在您提出问题的示例中,列表更适合,但我认为作者的意图是通过一个简单的示例来解释装饰器模式的用法. 假设咖啡是由牛奶、咖啡混合物和糖组成的。咖啡混合物进一步由更小的成分组成。这里的解决方案类似于复合模式。

装饰器模式是基于行为增强的设计模式。 Java IO 流使用这种装饰类来增强行为(方法实现)(一个流被另一个流包裹以增强前一个流)

【讨论】:

能否请您详细解释一下装饰器模式在 Java IO 流的情况下是如何有帮助的并且不是矫枉过正的? 如果您想通过 java 现有的 io 包实现与实现相同的功能,这将非常有趣。您如何看待一个简单的流可以添加缓冲区 amd 等功能,以及装饰器模式的替代方法。 查看问题中的咖啡店示例。在此页面的许多地方,包括您自己的评论,都提到装饰器模式对于该咖啡店来说是一种过度杀伤力,并且存在替代解决方案。所以,我想知道是否有可能以另一种方式实现 iostreams,就像我们可以为咖啡店做的那样。我还想知道为什么装饰器是 IOStreams 的最佳/正确设计。 如果我需要方法 someAlignFeatureToA(),我会直接将它添加到类 A 中或创建一个类 AHelper,而不扩展 IA,只需从类 A 调用 someAlignFeatureToA()。这些方法有问题吗?您的方法似乎比装饰器模式更支持继承。 @Imad 假设您有一个包含类 A 的库,在这种情况下您将无法添加其他方法。通过在现有类中包含另一个方法来修改现有测试代码也会破坏 OCP。现在关于继承,请重新阅读我的答案,我已经明确提到不要继承,而是选择组合选项。 B 类构成 A 类的引用,B 实现与 A 类实现相同的接口。B 类现在可以通过装饰将方法委托给 A(如果需要,添加一些额外的代码)。【参考方案2】:

您所说的是一种计算饮料总成本的方法。更一般地说,您提出了一种替代方法来合并和执行装饰器模式假定的每个派生对象的主要行为,方法是将它们添加到列表中。而且它也不完全是一种面向对象的方法。

但Decorator Pattern 的初衷远不止于此。它是动态地向对象添加扩展功能。这意味着我有一些对象 O1 并且我希望它将它转换为另一个具有扩展功能的对象 O2 并且这两个对象是可互换的。

所以这是关于我们如何管理 Object EvolutionObject Lifecycle。希望你明白了。 :))

【讨论】:

【参考方案3】:

我知道我为时已晚,但我会尽力解释这一点。

我认为 Head First 书中的示例很好地解释了这种模式,尽管许多人在找到简单的替代方案时感到困惑,即该示例的集合。

我认为更好的例子是送餐应用 (WhySoHungry)。他们列出了多个优惠,例如

    生日优惠 周日晚间优惠 10 年的应用发布优惠。 客户的第 100 个订单报价

并且列表还在继续。

如果某人有资格获得多个优惠,而WhySoHungry 希望他们同时应用所有优惠,那么计算部分就会变得困难。这是应用装饰器模式的最佳场所。

【讨论】:

【参考方案4】:

试图帮助 OP 理解装饰器正在扩展行为,这在 head first book 中的示例中并不那么明显。

引用 freedev 的评论:这与您必须按成本减去或增加的折扣或税费列表无关。这是关于您使用装饰器模式处理的行为。而且,相信我,与继承相比,使用列表处理这些行为更难。

分享一个多租户真实电子商务应用程序的示例,该应用程序在 20 个国家/地区(= 20 个租户)开展业务。

用例:构建一个可由 UI 呈现的购物车(或篮子)对象。

要求:

    购物车视图需要显示在购物车页面以及订单审核页面上。 购物车可能包含一些正在升级/降级的项目(您正在升级现有的宽带计划)。 因此我们可能会显示一些附加信息。 (您的升级计划将在接下来的 x 小时内激活。您将被收取 2 美元的罚款。等等)。 此外,我们可能会显示将升级/降级的旧计划。 (向用户显示为购物车项目,但显示为灰色)。 以上内容再次取决于租户。 如果租户允许库存限制,则考虑购物车中每件商品的库存状态。但是,当我们在订单审核页面上看到购物车对象时,这不是必需的,因为我们已经在结帐时预订了购物车。 同一个购物车对象内部也有购物车摘要(组合),但我们在订单审核页面上不需要相同。 我们还使用相同的购物车对象构建了另一个视图 - 当用户将鼠标悬停在 UI 标题中的购物篮图标上时显示的轻量级购物车视图。

现在这几乎类似于 OP 描述的咖啡问题。需要处理的风格太多(特定于租户 + 特定于页面的行为)。 我们能做什么?我们可以使用装饰器模式来根据需要用不同的行为来装饰我们的购物车。

可能的装饰器:

    CartStatusDecorator 购物车附加信息装饰器 CartExistingItemsDecorator CartSummaryDecorator

如下进行:

    构造一个具有所有最低要求行为的基本购物车对象。例如,此基本购物车应包含所有购物车商品及其价格、图片和数量。 根据租户决定是否需要物品的状态。如果是,则循环进入 CartStatusDecorator。 决定是否需要显示“附加信息”。如果是,请在 CartAdditionalInformationDecorator 中循环。 决定我们是否需要展示旧计划。如果是,请在 CartExistingItemsDecorator 中循环。 决定是否要显示摘要。调用 CartSummaryDecorator。

顶层可以根据租户和所有装饰器需要参与的页面来决定。想象一下用行为列表来做这件事。现在很明显,列表不是一个好主意。

【讨论】:

【参考方案5】:

你应该区分数据和行为。

装饰器是处理一个非常具体的问题的间接层。数据(在类型、合同、协议等方面)保持不变,但您在实施方面获得了更大的灵活性。使用装饰器,您可以重用现有功能并开始添加更改 - 结合适当的适配器,您基本上可以慢慢开始从 pone API 迁移到不同的 API 并保持兼容。

列表应仅负责以特定方式提供对包含数据的访问。对数据执行任务/处理列表包含的数据应该由具有不同职责的函数/对象/类来完成。

【讨论】:

最后一段对我来说没有意义。 处理数据如何不对其执行任务? 我说得更具体一点。【参考方案6】:

免责声明:我使用的术语和代码 sn-ps 取自this article。

如您所见,“装饰”对象有两种可能的设计。两种方法都可以实现相同的功能,主要区别在于使用的上下文

垂直装饰器

Numbers numbers = new Sorted(
  new Unique(
    new Odds(
      new Positive(
        new ArrayNumbers(
          new Integer[] 
            -1, 78, 4, -34, 98, 4,
          
        )
      )
    )
  )
);

这是实现装饰器的最常用方法。我几乎完全以这种方式实现我的装饰器,因为我发现它更“自然”和简单。它适用于装饰器数量不多的情况(具体多少由程序员解释)。 当某些对象需要动态扩展时,它也非常适合:当在实例化时不知道需要哪些装饰器时。

水平装饰器

Numbers numbers = new Modified(
  new ArrayNumbers(
    new Integer[] 
      -1, 78, 4, -34, 98, 4,
    
  ),
  new Diff[] 
    new Positive(),
    new Odds(),
    new Unique(),
    new Sorted(),
  
);

在极少数情况下(当装饰器的数量开始增长时),我会选择将装饰器“扁平化”为类似列表的结构。这样做的好处是简化了代码的阅读,但缺点是在运行时失去了灵活性:需要在创建对象时知道要应用哪个装饰器(在我试图培养的不变性的上下文中尤其如此) ,因此根据应用程序,这可能是不可行的。

最终,您必须在灵活性和可读性之间做出设计决定。

【讨论】:

【参考方案7】:

Head First 一书所解释的方式似乎是教授装饰器模式的常用方式,但在我看来,这并不是介绍它的最佳方式。 装饰器模式的要点不仅仅是在具体类的方法之前按顺序运行方法列表。

您的问题的答案:装饰器模式是关于将一​​个对象变成另一个类似但具有更多功能的对象。但是,如果您将装饰器作为列表传递并仅运行它们的方法,则与原始对象相比,您将不会获得任何其他具有更多功能的对象。

这是一个使用您建议的列表方法的示例:

例如,您可以使用它在将某些数据写入云端之前对其进行压缩和加密:

    public static void main(String[] args) 
        String data = "Hello World";
        CloudStream stream = new CloudStream();
        stream.addDecorator(new CompressedStream());
        stream.addDecorator(new EncryptedStream());
        
        stream.write(data);
    

但是,这样一来,您将无法访问压缩数据对象和加密数据对象。

相比之下,您可以在 Java 中使用 ObjectInptuStream 对象“装饰” FileInputStream 对象:

var oistream = new ObjectInputStream(new FileInputStream());

现在您也可以访问 ObjectOutputStream 对象,与 FileInputStream 对象相比,它为您提供更多 capabilities。

【讨论】:

以上是关于使用列表而不是装饰器模式?的主要内容,如果未能解决你的问题,请参考以下文章

装饰器模式

装饰器模式

装饰器模式

python-装饰器模式

设计模式入门之装饰器模式Decorator

设计模式-- 手工耿教我写装饰器模式