基于类型添加行为的最佳方式
本文关键字:最佳 方式 添加行 于类型 类型 | 更新日期: 2023-09-27 18:17:52
我有一个公司实体
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
}
..........
}
老实说,我不喜欢这样。有没有什么设计模式可以帮助克服这种情况?您将做什么?为什么要设计这个场景?
显式实现接口,然后重写强制转换操作符,仅在有效时强制转换到该接口。
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)您已经消除了对您想要调用的每个不是所有公司"全局"的函数的检查。在强制转换时进行一次检查,然后只要将它作为接口类型放在变量中,就可以愉快地与它交互,而无需进一步检查。这意味着引入bug的地方更少,无用的检查也更少。
3)您正在利用语言的特性,并利用类型系统来帮助使代码更加防弹。
4)你不必编写大量的子类来实现各种接口的组合(可能是2^n个子类!),在你的代码中到处都是NotImplementedExceptions
或InvalidOperationException
。
5)你不必使用枚举或"类型"字段,特别是当你要求混合和匹配这些能力集时(你不仅需要一个枚举,还需要一个标志枚举)。使用类型系统来表示不同的类型和行为,而不是enum。
6)它是DRY.
这种方法的缺点:
1)显式接口实现和覆盖显式强制转换操作符并不是基本的c#编码知识,可能会让你的后继者感到困惑。
编辑:嗯,我在没有测试这个想法的情况下回答得太快了,这对接口不起作用。
我会考虑在不同的类中分离所有这些类型的实现。您可以通过使用enum来表示公司类型来开始此操作。
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)
{...}
}
这样,您可以在Invoice
和GetComission
等方法中消除对各种类型的测试。
与大多数DDD
问题一样,它通常归结为Bounded Contexts
。我猜你在这里处理一些不同的有界上下文(这从你的语句中最明显,"公司可以是代理商或供应商,或者两者都是,或者不是。")。至少在一个上下文中,您需要平等地考虑所有Company
实体,无论它们是代理还是供应商。然而,我认为你需要考虑你的Invoice
或GetCommission
操作是否适用于这个更广泛的背景?我想说的是,这些将适用于更专业的环境,其中Agent
和Supplier
之间的区别更为重要。
您可能会遇到麻烦,因为您试图创建一个适用于所有上下文中的所有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的一个教训是,我们将领域划分为这些有界的上下文,并且不试图创建可以做所有事情的"上帝"对象。