【问题标题】:Best way to add behaviors based on type根据类型添加行为的最佳方式
【发布时间】:2014-10-05 19:29:24
【问题描述】:

我有一个公司实体

public class Company : Entity<Company>
{
     public CompanyIdentifier Id { get; private set; }
     public string Name { get; private set; }
     ..............
     ..........
}

公司可以是代理商或供应商,也可以是两者都不是。 (有更多类型)它的行为应该基于类型而改变。代理可以获得佣金,供应商可以开具发票。 设计实体或实体或值对象的最佳方式是什么?我可以选择添加一些布尔类型并检查方法中的这些值,

 public class Company : Entity<Company>
    {
         public CompanyIdentifier Id { get; private set; }
         public string Name { get; private set; }
         public bool IsAgent { get; private set; }
         public bool IsSupplier { get; private set; }
         ..........

         public void Invoice()
        {
                if(!IsSupplier)
                {
                    throw exception.....;
                }
                //do something
        }

        public void GetCommission(int month)
        {
                if(!IsAgent)
                {
                    throw exception.....;
                }
                //do something
        }
         ..........
    }

说实话,我不喜欢这样。是否有任何设计模式可能有助于克服这种情况?你会做什么以及为什么要设计这个场景?

【问题讨论】:

    标签: c# class design-patterns entity


    【解决方案1】:

    显式实现接口,然后覆盖强制转换运算符以仅在有效时强制转换为该接口。

    public class Company : ...., IAgentCompany, ISupplierCompany ... {
    
        public double IAgentCompany.GetCommission(int month) {
                /*do stuff */
        }
    
        public static explicit operator IAgentCompany(Company c)  {
            if(!c.IsAgent)
                throw new InvalidOperationException();
            return this;
        }
    }
    

    接口的显式实现必须通过接口调用,而不是具体类型:

    // Will not compile
    new Company().GetCommission(5);
    
    // Will compile
    ((IAgentCompany)new Company()).GetCommission(5)
    

    但是,现在我们已经重载了显式转换运算符。那是什么意思?我们不能在不强制转换为 IAgentCompany 的情况下调用 GetCommission,现在我们有一个守卫来防止对未标记为代理的公司进行强制转换。

    这种方法的好处:

    1) 您拥有定义不同类型公司的各个方面以及它们可以做什么的接口。接口隔离是个好东西,可以明确各类公司的能力/职责。

    2) 您已经取消了对您要调用的每个函数的检查,这些函数不是对所有公司都“全局”的。您在转换时进行一次检查,然后只要将它放在一个类型为接口的变量中,您就可以愉快地与它进行交互而无需任何进一步的检查。这意味着引入错误的地方更少,无用的检查也更少。

    3) 您正在利用语言特性,并利用类型系统来帮助使代码更加安全。

    4) 您不必在代码中随处使用 NotImplementedExceptionsInvalidOperationException 编写大量实现各种接口组合的子类(可能是 2^n 个子类!)。

    5) 您不必使用枚举或“类型”字段,尤其是当您要求混合和匹配这些能力集时(您不仅需要枚举,还需要标志枚举)。使用类型系统来表示不同的类型和行为,而不是枚举。

    6) 它是干的。

    这种方法的坏处:

    1) 显式接口实现和覆盖显式强制转换运算符并不完全是 C# 编码知识,可能会让你之后的人感到困惑。

    编辑:

    好吧,我没有测试这个想法就回答得太快了,这对接口不起作用。但是,请参阅我的其他答案以了解其他想法。

    【讨论】:

    • 我真的很喜欢这个主意。对于仅对特定组合有效的某种操作,我应该怎么做。例如,如果公司既是代理又是(任何)客户,则不允许为自己的购买收取佣金。这只是一个例子,还有更多的行为取决于类型的组合
    • 我在玩你的方法,结果我得到了 “在 c# 中,你不能使用带有隐式或显式运算符的接口” link。 @CleverNeologism:我错过了什么吗?
    • 我添加了另一个类 AgentCompany,它也实现了 IAgentCompany。然后下面的转换运算符转换为 agentcompany 类在 Company 类中工作 code public static explicit operator AgentCompany(Company c) { if (!c._isAgent) throw new InvalidOperationException(); return ((AgentCompany)c); } @clever-neologism 可以吗?
    • 你可以这样做,当然。在这种情况下,Company 类实现了接口的定义,但没有声明它实现了,而它的
    • 可以吗?好吧,您需要像使用库一样针对使用情况对其进行测试,但它看起来是正确的。在您的示例中,Company 对象将实现所有接口函数,但不会在 class 语句中声明它,而虚拟子类(AgentCompany : Company 没有覆盖)会。
    【解决方案2】:

    我会考虑将所有这些类型的实现分离到不同的类中。您可以通过使用枚举来表示公司类型来开始执行此操作。

    public enum CompanyType
    {
      Agent = 0,
      Supplier
    }
    
    public abstract class Company : Entity<Company>
    {
       public CompanyIdentifier Id { get; private set; }
       public string Name { get; private set; }
       public CompanyType EntityType { get; private set; }
    
       public abstract void Invoice();
       public abstract void GetCommission(int month);
       ...
    

    这样可以减少公共属性。

    接下来,我将为供应商和代理实现专门的类(然后两者都没有)。您可以将 Company 抽象化,也可以将任何专门的方法抽象化。

    这将允许您区分每种实体的不同行为。当您返回它进行维护时会派上用场。它还使代码更易于阅读/理解。

    public class SupplierCompany : Company
    {
       public SupplierCompany()
       {
         EntityType = CompanyType.Supplier;
       }
    
       public override void Invoice()
       {...}
       public override void GetComission(int month)
       {...}
    }
    
    public class AgentCompany : Company
    {
       public AgentCompany()
       {
         EntityType = EntityType.Agent;
       }
    
       public override void Invoice()
       {...}
       public override void GetComission(int month)
       {...}
    }
    

    有了这个,您可以消除在 InvoiceGetComission 等方法中对各种类型的测试。

    【讨论】:

    • @MJK:如果您添加第三个枚举成员 Both 并实现第三个专用类 SupplierAndAgentCompany,则可以。这些类可以非常小并且只包含特定的代码。所有常用代码都可以放到基类中。
    • @MJK:另外,这样每个班级都专注于一件事情,而不是做一堆事情(关注点分离)。
    • 我有很多其他类型的公司,比如运营商、电话客户、在线客户等等。可能需要添加更多。如果我开始为各种排列创建,那么将来添加任何新类型将非常困难。
    • @MJK:我明白,但是如果你在一个班级里完成所有这些,那么你最终会得到一个庞大的班级。更难添加对新类型的支持。
    • @MJK:你甚至可以更进一步,通过接口定义每个公司的行为。然后也许使用 DI 容器来帮助您更直接地解决特定公司问题。
    【解决方案3】:

    与大多数DDD 问题一样,它通常归结为Bounded Contexts。我猜您在这里处理的是一些不同的有界上下文(这从您的陈述中最为明显“公司可以是代理或供应商,或两者兼而有之,或都不是。”)。在至少一种情况下,您需要平等地考虑所有Company 实体,无论它们是代理还是供应商。但是我认为您需要考虑您的InvoiceGetCommission 操作是否适用于更广泛的背景?我想说这些将适用于更专业的环境,其中AgentSupplier 之间的区别更为重要。

    您可能会遇到麻烦,因为您正在尝试创建一个适用于 所有 上下文的包罗万象的 Company 实体...如果没有奇怪的代码结构,这几乎是不可能实现的 &与类型系统作斗争(正如您在其他答案中所建议的那样)。

    请阅读http://martinfowler.com/bliki/BoundedContext.html

    大致了解您的上下文的外观:

    Broad "Company" Context
    {
        Entity Company
        {
            ID : CompanyIdentifier
            Name : String
        }
    }
    
    Specialized "Procurement" Context
    {
        Entity Supplier
        {
            ID : CompanyIdentifier
            Name : String
            Invoice()
        }
    }
    
    Specialized "Sales" Context
    {
        Entity Agent
        {
            ID : CompanyIdentifier
            Name : String
            GetComission()
        }
    }
    

    尝试在采购和销售上下文中使用相同的对象是否有意义?毕竟,这些上下文有非常不同的要求。 DDD 的一个教训是,我们将领域划分为这些有界上下文,不要试图制造无所不能的“上帝”对象。

    【讨论】:

    • 也许你适合更广泛的系统。这些类型基本上是一些主要用于一两个动作和报告的标志。在多个有界上下文中分离是个好主意吗?还有一些操作只允许组合使用,例如允许在线和电话客户获得代金券。你如何看待@CleverNeologism 的溶胶?
    • “这些类型基本上是一些主要用于一两个动作和报告的标志。” 没关系,DDD 的主要课程之一是知道何时 使用它。当然,对于简单的 CRUD 应用程序,DDD 不是必需的。我建议你从你的问题中删除domain-driven-design 标签:)
    猜你喜欢
    • 2012-04-24
    • 2015-04-17
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-11-04
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多