【问题标题】:Anaemic domain models or "where to put logic"贫血的领域模型或“将逻辑放在哪里”
【发布时间】:2009-09-15 07:50:02
【问题描述】:

这是“分析瘫痪”似乎已经站稳脚跟的场景之一,所以请指教!

项目

一个相当简单的汽车产品列表,其中包括零件参考、适合的车辆等详细信息。

前端是一个asp.net MVC应用程序。

后端是 SQL,使用 Subsonic 将产品投影到领域对象中。

功能

我们的屏幕之一是产品详细信息屏幕。一个 ASP.NET MVC 控制器调用产品存储库来检索产品详细信息,将这些详细信息(通过一些自动映射到 viewModel)返回到视图。

现在的杀手锏是我们有两个或三个频道进入网站,根据频道的不同,用户需要看到不同的零件编号。

例如,如果是零售渠道,则零件编号与数据库中的零件编号相同,但如果用户通过贸易渠道访问该站点,则零件参考的开头将替换为替代编号。

例如0900876 如果通过贸易频道查看,则变为 1700876。

我正在努力决定在哪里封装有关部件引用(以及其他可能改变的细节)的“通道规则”。

我已经考虑过这些替代方案。

将逻辑直接写入域对象中

在 Product 类中,我们可以有一个方法/属性来获取翻译后的部分参考。

public string TranslatedPartRef()
    {
        if (this.Channel == "Trade")
        {
            return PartRef.Replace("0900", "1700");
        }
        else
        {
            return PartRef;
        }
    }      

在这种情况下,Product 实例必须知道通道,这对我来说似乎是错误的。

将逻辑封装在另一个对象中

我们可以编写一个类来处理这部分参考翻译,或者创建一个包含这个逻辑的Channel类。

我不明白的是如何协调这两个类。

如果控制器调用存储库来检索产品,那么它是否应该计算出使用的通道并翻译部件参考?如果是这样,我该如何将带有翻译后的部件参考的产品发送回视图?

同样值得注意的是,这部分引用也必须出现在搜索结果和其他场景中,因此我认为它需要整齐地包含在某个域中。

【问题讨论】:

    标签: domain-driven-design business-logic


    【解决方案1】:

    我不是 C# 人,但我想我会用 Java 中的装饰器来解决这个问题。

    假设您有一个产品接口,那么您可以创建一个装饰器来管理部件号问题。

    class Product implements IProduct {
        public String getProductCode();
        // etc
    }
    
    class ProductChannelDecorator implements IProduct
    {
        // constructor, like this in C#?
        public ProductChannelDecorator(IProduct product, Channel channel) { 
            this.product = product;
            this.channel = channel;
        }
        public String getProductCode() {
            switch (this.channel) {
                case Channel.RETAIL:
                    return this.decorated.getProductCode();
                case Channel.TRADE:
                    return retailToTradeTransformer(this.product.getProductCode());
                // etc
            }
        }
        // etc
    }
    

    【讨论】:

    • +1,但我会将 Channel 类型更改为具有 GetProductCode 方法的抽象类型,该方法将 IProduct 作为输入。然后你可以有两个具体的实现(RetailChannel 和 TradeChannel)以不同的方式实现该方法。
    • 是的,上面的“如何查找/转换代码”很模糊而且很奇怪,因为我对此了解得不够多。你的建议很有道理。
    • 好吧,这对我来说看起来不错,我们现在回到“何时”的问题(我似乎总是不解),我什么时候在从中检索产品的过程中实例化 ProductChannelDecorator存储库并将其呈现给视图? (我认为我缺乏知识在这里踢!)
    • 到目前为止看起来不错,我可以在检索单个产品的详细信息时轻松创建装饰器的新实例。下一个问题是,给定一个产品列表,然后如何用装饰器包装每个产品(避免使用 foreach 可能是可取的)
    • 这可能首先取决于您如何检索列表。就像我说的,C# 不是我的领域,我通常在 Java/Hibernate 中挖掘,所以方法会有所不同。您的 ORM 在为模型补水时是否提供拦截?如果这真的仅限于显示/视图,那么您可以做相当于 JSP 标记的操作吗?抱歉,我不够熟悉,无法在这里提供太多帮助。
    【解决方案2】:

    您需要问自己的第一个问题是 Channel 的概念是否是域概念。您的问题似乎表明它不是,但另一方面,我认为这听起来也不是特定于应用程序的。

    您可能会问的另一个问题是:如果将来我需要在此域模型之上构建另一个应用程序(例如 Web 服务或富客户端),我是否仍需要处理频道的概念?

    我的猜测是答案可能是是的。

    据我了解您的问题,Channel 以某种方式与请求上下文相关。也许它真的是用户的一个属性。或者可能 in 是应用程序配置本身的一个属性。

    无论如何,我会认真考虑它是否真的不是一个领域概念。如果是,那么它可以很好地属于域对象。

    如果不是,ptomli 建议的装饰器实现听起来是个不错的方法。

    【讨论】:

    • 非常正确,我实际上并没有过多考虑这是域、应用程序还是视图,只是一头扎进了实现中。哇!
    • 这就是我认为我遇到困难的地方。由于各种原因(与我们与其他公司达成的协议等有关),我建立频道的唯一选择是基于 URL。因此,当请求进入时,我正在确定我们所处的模式(基于 URL),然后当人们使用网站时,他们需要查看不同的部件号。未来我们很可能需要添加更多渠道并扩展自定义(我们已经在此基础上动态更改网站模板等)
    • 我其实很想把这个逻辑放在控制器和视图之间(基于我们只需要人们看到翻译的部分引用,但可能不需要任何业务基于它们的决定)
    • @jonhilt:好的,但是仅仅因为您通过特定技术(URL)检测到一个频道并且还将该信息用于 UI 目的并不意味着它不是一个域概念。您可以有一个 IChannel 接口来模拟域概念并被其他域对象使用。然后,在传入的 HTTP 管道中,您可以从 URL 检测通道并创建 IChannel 实现的实例,然后使用该实例调用您的域模型。
    • 另请参阅我对 ptomli 答案的评论。
    【解决方案3】:

    会有多少种不同的零件编号。如果只是 Trade v Retail,我很想在 Product 对象中简单地包含两个数字,并让 UI 决定显示哪个。作用于产品时,标识可以是“类型{Trade, Retail}, number”。

    对于一些灵活的模式,我认为你的频道想法很好。但如果它有双向职责,将零售与贸易联系起来,这似乎可行。 Channel 对象可以被视为一个适配器,能够进行其他转换和扩充。

    作为一种实现,我将为每个 Channel 创建一个单独的 Channel 对象,试图避免 case 语句和 if else 逻辑。对于零售,渠道对象可以是贸易的 NOOP 对象,它可以进行映射。工厂可以创建相应的 Channel 对象。

    【讨论】:

      【解决方案4】:

      如果零件编号映射可能发生变化怎么办?现在它是一个会发生变化的前缀,但您是否需要满足其他类型的变化?也许你不需要这个,但是:

      在业务层面,您是说产品可以有不同的部件号,具体取决于渠道(这毕竟是一个基本的业务概念)。因此,这表明在数据库级别,某处可能有一个 PartNumber 表,其中包含 ProductId、ChannelId 和 PartNumber 列。这肯定会涵盖随着时间推移出现更多渠道的情况(今天是零售或贸易,明天他们可能会添加 Web、邮购等。所有这些都可能需要不同的部件号)。

      在对象级别,这映射到具有Dictionary<Channel, PartNumber> 的Product 实例,该实例可用于在给定Channel 的情况下获取适当的部件号。

      【讨论】:

      • 很好,这已经出现在我们对一般规则有例外的地方(令人讨厌的是,由于某种原因,只有三四个产品必须有不同的编号)。这导致我调查在数据库中保存替代零件编号。
      • 我总是用这种事情回到“何时”的问题,即在检索产品时(调用存储库,将数据转换为产品对象,将对象映射到视图,将其返回到视图)我是否告诉它我们正在使用哪个通道并询问它的部分参考?
      【解决方案5】:

      现在的杀手锏是我们有两个或三个频道进入网站,根据频道的不同,用户需要看到不同的零件编号。

      一个简单的解决方案:

      public interface IChannel
          function GetNumber(Part as IPart) as String
      end interface
      

      没有装饰器,没有开关,没有控制反转。

      每次您需要特定通道的部件号时,您都调用此方法。

      dim Channel as IChannel = ...
      dim Part as IPart = ...
      dim PartNumber = Channel.GetNumber(Part)
      

      每次您需要不同的零件编号计算方法时,您只需实现此接口即可。

      public class TradeChannel
          implements IChannel
      
          public function GetNumber(Part as IPart) as String implements IChannel.GetNumber
              return Part.Number.Replace("0900", "1700")
          end function
      end class
      

      【讨论】:

        猜你喜欢
        • 2010-12-20
        • 1970-01-01
        • 2010-12-26
        • 2012-02-04
        • 2014-01-05
        • 2020-01-29
        • 1970-01-01
        • 2011-12-19
        • 1970-01-01
        相关资源
        最近更新 更多