重构贫血模型提高代码的DDDness - Alexander


这是一个实用指南:结合DDD和OOP展示如何通过封装构建意图暴露一个类的API?这篇博文中的所有代码都可以在这里找到。
对我来说,DDD 就是构建意图揭示 API。它关于将业务概念和规则封装在对象中,也称为优秀的面向对象编程。
在这篇文章中,我将介绍一些可以帮助您“更好地”封装域逻辑的代码味道和模式。我们将通过重构这些类来做到这一点;

public class Demon
{
public int Age { get; set; }
public bool HasHorns { get; set; }
public string Name { get; set; }
public int PowerDamage { get; set; }
public string PowerName { get; set; }
public List<string> Underlings { get; set; }
}

第一个类是Demon类,这可能是我们大多数人以前使用和见过很多次的。没有封装,只有由客户端代码设置的属性。(失血模型】贫血模型)
这段代码通常会与一些类似的东西耦合:

public Demon CreateDemon(
 string name,
 int age,
 bool hasHorns,
 string powerName,
 int powerDamage)
{
// Validate input and Create demon
}

DemonService类将有上面这个一个工厂方法,用于封装创建和在最好的情况下输入一些验证。
 
沟通和意图
那么我们与这个类Demon能交流什么?
从Demon暴露对外方法API 来看,不是很多。我正在读到的意图是Demon所有这些属性都是可选的,并且可以随时更改。(通过setXXX)
对于DemonService类来说,这将非常相似。我们在这里面临的问题是,开发人员必须知道工厂方法的存在才能获得“有效”的Demon实例。拥有公共setter还鼓励黑客代码修复错误“让我们快速更改它并继续前进”。
代码坏味道:公共setter等于无法强制执行一致性
如果再次查看 Demon 类,您将看到所有属性都有公共 getter 和 setter,这意味着我们无法在实例的生命周期内强制执行一致性。它可以随时更改。
第一步修复:使类不可变:

public class Demon
{
 public Demon(string name,
  int age,
  int powerDamage,
  string powerName,
  bool hasHorns)
 {
  // set properties here
  }
public int Age { get; private set; }
public bool HasHorns { get; private set; }
public string Name { get; private set; }
public int PowerDamage { get; private set; }
public string Power Name { get; private set; }
public List<string> Underlings { get; private set; }
}

这样,至少我们知道Demon 在其生命周期内是一致的,因为属性是不可更改了。(一旦构建就无法修改了)
 
代码异味:“想起需要验证时才去验证”模式
你还记得DemonService带有工厂方法的类吗?
将验证和创建逻辑与类分开,可能看起来不错,关注点分离.. 对吗?.. 对吗?不......这将再次留给hacky代码,其中“我们只需要一个有名字的Demon ”或类似的语义将在代码库中抛出。问题是; 它不是一个有效的Demon.  
IsValid 与始终验证
IsValid方法只能是我们的类中有一个检查有效性的方法(当我们调用它时)。
而”始终验证“是一个实例应该始终是一个有效实例的想法。这样,我们知道如果我们通过一个实例,它总是“正确的”。
修复:将验证逻辑移至构造函数
将验证逻辑从工厂方法移到Demon类的构造函数中。我们现在就实现始终有效:
public class Demon
{
 public Demon( string name,
  int age,
  int powerDamage,
  string powerName,
  bool hasHorns)

 if (string.IsNullOrWhiteSpace( name))
 {
      throw new ArgumentException($"'{nameof(name)}' cannot be null or whitespace.", nameof(name));
 }
  // more validation before setters.
}

  
代码异味:原始变量的痴迷
再次看Demon类;
public class Demon
{
public int Age { get; set; }
public bool HasHorns { get; set; }
public string Name { get; set; }
public int PowerDamage { get; set; }
public string PowerName { get; set; }
public List<string> Underlings { get; set; }
}

请注意我们如何为所有事物使用原始类型?int的和string的到处都是。
原始类型问题在于它们可能具有比您实际接受和想要的更多的不变量。让我们以List<string> Underlings为例。string有多少个不变量?什么是“有效”underling变量?特别是当我们将 List作为参数时。我们如何验证个输入条目?
修复:将原始类型封装在一个类中(ValueObject)

public class Underling
{
  public Underling(string name)
   {
      // Validate according to rules
   }
}

现在我们可以验证 Underling 作为它自己的东西,并且我们肯定有一个“总是有效”实例 ,因为我们遵循Always Valid总是验证规则。
 
代码异味:通过命名来传达关系
public class Demon
{
public int Age { get; set; }
public bool HasHorns { get; set; }
public string Name { get; set; }
public int PowerDamage { get; set; }
public string PowerName { get; set; }
public List<string> Underlings { get; set; }
}

这种代码异味与Primitive obsession(原始类型痴迷 上节)密切相关。看看PowerDamage和 PowerName,它们似乎在它们之间传达某种形式的关系,但没有任何强制执行它,确定它可以通过构造函数中的验证来强制执行,但是消费者呢?我们如何更好地沟通这种关系?
修复:将其(关系)封装在一个类中(ValueObject)
public class Power
{
 public Power(string name,
  ushort damage)
 {
  // Validation
 }
 // properties and other logic
}

现在它是它自己的一个对象,可以这样使用,始终有效和一切......并且在Demon构造函数中

public class Demon
{
 public Demon(string name,
  ushort age,
  bool hasHorns,
  Power power)
 {
   // Validation
 }
}

 
代码异味:暴露 List API
如果您在Demon类中注意到我们可能已将Underlings的setter 设为私有,但由于它是一种List类型,因此我们需要ListAPI。
修复:封装List列表并公开一个 IEnumerable
通过封装 List 并且只公开一个IEnumerable,我们停止公开List的API。同时,我们将使用我们Demon域中无处不在的语言来创建一个新的Underling. 由于我们将验证逻辑移到了Underling类中,因此我们可以安全地“新建”它。

public class Demon
{
 private readonly IList<Underling> underlings = new List<Underling>();
 // ctor and other logic
 public IEnumerable<Underling> Underlings => underlings;

 public void Enslaveunderling(string name)
 {
   underlings.Add(new Underling(name));
 }
}

 
代码异味:无丰富业务行为
public class Demon
{
 public Demon(string name,
  ushort age,
  bool hasHorns,
  Power power)
 {
   // Validation
 }
}
行为应该只对它封装的状态进行操作。这意味着 Demon 类中的行为应该只改变 Demon 类及其封装的类的状态。— 然而,这可能有点棘手,由设计人员和领域专家认为变量hasHorns决定Demon是否应该直接更改 ,现在应该封装此逻辑并由Demon简单地调用该逻辑。
修复:使用无处不在的语言封装逻辑:
我们只是将这个逻辑封装在一个方法中GrowHorns()。这样,消费调用的客户端代码总是可以添加horns到我们的Demon实例中。域是完整的!

public class Demon
{
 public Demon(string name, ushort age, Power power)
 {
  // Validation
 }
 // Horns can be grown, not shed.
 public void GrowHorns()
 {
  HasHorns = true;
 }
}

 
创建模式
如果我们看一下Demon类,构造函数就是用来创建新实例的。这通常很好,但可以使用一些方法可以使它更加普遍。
私有构造函数+静态工厂方法:
private Demon(string name, ushort age, Power power)
{
// Validation and business rules.
// Set properties.
}
public static Demon Summon(string name, ushort age, Power power)
{
return new Demon( name, age, power);
}

这意味着对消费者隐藏构造函数,并有一个调用构造函数的静态方法。
其他创建模式还有”私有构造函数+静态实例“。
通过创建模式我们有效地传达了我们领域允许的内容和无处不在的语言。
你可以在这里找到所有的代码