领域模型的隔离

  领域模型的隔离总体目标是将应用中副作用或外部依赖推送到业务操作边缘,或者是业务操作的开始或者是其结束,不可变的核心是领域模型,可变的壳是应用服务层。

领域模型的隔离是将领域中不容易变化的部分放入模型中,容易变化比如对外部环境有依赖的部分放入应用服务层中。

具体来说,从操作行为角度来看,改变领域模型状态的操作与领域模型的状态改变导致文件系统的改变这两个操作应该隔离开来,前者操作属于领域模型的核心,后者因为对文件系统或数据库系统有依赖应该在应用服务层实现,也就是说,领域模型负责进行业务决策决定,而应用服务层将这些决策转换为可见的信息,比如改变数据库中数据。

判断领域模型是否被充分隔离的一种方便方式是:根据实体和值对象周围关联的对象,如果实体和值对象和其他不是实体值对象的对象发生交互,那么说明你的领域模型缺乏隔离。

比如有一个实体Person和指对象Address,如果发现Address依赖于一个API,而Person可能依赖于仓储数据库,那么说明这两个类是通往外部世界的网关,这种与外部世界的交互破坏了领域模型的隔离性。

另外一种判断隔离性的方式是根据架构设计,洋葱式的层层架构我们知道应该是最外层依赖最内层,但是如果位于核心层的领域模型比如Person或Address反而是依赖外层,方向正好相反,那么造成了领域模型抽象泄漏,也就是没有隔离性。

一个隔离领域模型是一种闭环,领域模型中所有操作都应该是有关其最近的实体和指对象,换句话说,这些操作的输入参数或输出结果应该由原始类型或领域模型自己组成。

比如:

public sealed class Address : ValueObject<Address>{

    public string AddressString { get; }
    public string ZipCode { get; }

     public Address(string addressString)    {
        AddressString = addressString;
        ZipCode = newLocationApi().LookupZipCode(addressString);
    }

}

这里Address值对象接受一个构造参数addressString,然后使用Location API寻找相应的邮编ZIP,这里LocationApi是构造器隐式依赖的输入参数,它不是领域类,也不是原始类型,这样,这个领域模型实现将不再是闭环,不是隔离的。

隔离特性只适合实体和指对象,它们是领域模型核心,其他领域类比如工厂,领域服务和特别是仓储通常指向外部世界,下面是Active Record模式:

public class User : Entity{
    public void Save()    {
        /* 保存自己到数据库 */
    }

     public void Load(int id)    {
        /* 从数据库中加载它自己 */
    }

}

实体实现这种模式会直接和数据库交互,因为它需要从数据库保存和加载它自己,这种隐式参数实际使用到数据库,这破坏了隔离性。

下面看看,使用依赖注入将外部世界注入到领域模型的情况,比如下面代码:

public sealed class Address : ValueObject<Address>{

    public string AddressString { get; }
    public string ZipCode { get; }

     public Address(string addressString, ILocationApi locationApi)    {

        AddressString = addressString;
        ZipCode = locationApi.LookupZipCode(addressString);

    }
}

这里依赖 ILocationApi,运行时将ILocationApi实现注入到Address中,虽然导入了一个接口,但实际也是注入了外部概念到实体或指对象,这种黑客方式实际不是一种诚恳的隔离。实际上,真正尊重隔离性是应该注入到领域模型中任何抽象都是和你的领域有关,不是任何来自外部世界,它意味着你注入到实体和值对象的东东也必须是其他实体和值对象实现。

为了防止外部世界污染你的领域模型,最好办法是建立防腐层,将外部数据转换到领域模型类,通过防腐层,能让你的领域模型保持在不同界限上下文中,甚至它们可以共享一个核心。

最后,我们可能疑问,为什么花这么大精力自寻烦恼?答案是为了和复杂性战斗,关注隔离能够让我们减轻我们的认知负担,你的项目越复杂,这种方式就越重要,在一个大型代码系统中,隔离考虑领域模型能够区别于其他关注,这样你就能有效跟随项目日益增长的复杂性。

另外一个理由是可测试性,领域模型形成闭关有利于测试,你很容易进行输出和状态验证。

领域驱动设计

组合性是SOLID的真正目标