轻量级领域服务库

目录

介绍

背景

领域服务库代码

使用领域服务库

组件

总结

实用信息


本文提供了一个非常简单且轻量级但有用的领域服务库的具体示例。

介绍

如果您在领域驱动设计DDD)方面拥有多年的经验,最肯定的是,在必须解决的问题类型中,您已经认识到某种总体模式——无论您正在处理的应用程序类型如何。我当然知道我有。

无论您是开发桌面应用程序,Web应用程序还是Web API,您几乎总会发现自己必须建立一种机制来创建、保持和维护应用程序域模型中各种实体的状态。因此,每次启动一个新项目时,您都必须进行大量的工作来建立这种持久性机制,而您真正想要做的就是建立领域模型——应用程序的实际业务功能。

经过各种项目的多次迭代后,我已经建立了一种在几乎任何情况下都适合我的实践。这种做法允许您抽象实体持久性(工作......),以便您可以轻松地隔离这方面的基本实现细节,并专注于开发您的真正的业务功能。最后,当然你必须处理持久层的实现,但是能够开发 ——至少不是测试——你的领域模型而不必关心持久性细节的价值是巨大的。然后,您可以开始针对虚假存储库开发和测试您的领域模型。无论您最终是否最终制作基于文件的简单存储库,还是决定使用完整的RDBMS并不重要。

对于本文,我已将我的这种做法理解为我称之为领域服务库的内容。这个库是超轻量级的,只包含几个普通的C#类。库本身没有任何第三方依赖关系。不涉及ORM——存储库可以是任何东西,从内存中对象到RDBMS

背景

这个实践或库——或者如果你愿意的话——的技术成分只不过是一些通用的面向对象的软件开发原则,它们被称为SOLID存储库模式,特别的是依赖注入

领域服务库代码

让我们深入研究这个问题。总体思路是以领域模型服务和持久层存储库之间的存储库接口的形式创建明确定义的交互点——所谓的接缝。这里以产品服务及其相应存储库的依赖关系图为例:

https://www.codeproject.com/KB/architecture/730191/ttt.png

存储库的抽象在IRepository IReadOnlyRepository接口中定义。

public interface IRepository<TEntity, in TId> : 
    IReadOnlyRepository<TEntity, TId> where TEntity : IEntity<TId>
{
    void Add(TEntity entity);

    void Remove(TId id);

    void Update(TEntity entity);
}

请注意,IRepository继承了IReadOnlyRepository的签名。换句话说,IRepository是一个IReadOnlyRepository的扩展。

public interface IReadOnlyRepository<TEntity, in TId> where TEntity : IEntity<TId>
{
    int Count { get; }
 
    bool Contains(TId id);
 
    E Get(T id);
 
    IQueryable<TEntity> Get(Expression<Func<TEntity, bool>> predicate);
 
    IEnumerable<TEntity> GetAll();
}

显然,存储库抽象可以保存在单个接口中,但是由于接口隔离原则,将它分成(至少)两个独立的接口是有意义的。您可能最终必须实现一个简单的只读存储库,并且不应该强迫消费者为不支持的功能抛出NotImplementedException s。在实际生产代码中,您可以考虑将存储库接口拆分为更多子接口。

领域模型中实体的抽象由通用IEntity接口定义。

public interface IEntity<TId> : INotifyPropertyChanged, INotifyPropertyChanging
{
    TId Id { get; set; }
 
    string Name { get; set; }
}

泛型类型参数TId表示实体的ID的类型,其通常根据实体的类型而变化。您可能更喜欢某些实体的字符串ID,而其他类型的实体可能需要例如整数或GUID ID。还要注意IEntity继承了INotifyPropertyChangedINotifyPropertyChanging接口。这是为了支持可能的数据绑定,这在UI开发期间非常方便。

通用泛型abstract BaseEntity类使实体抽象更进一步,它提供了IEntity接口的基本实现。

public abstract class BaseEntity<TId> : IEntity<TId>
{
    private TId _id;
    private string _name;

    protected BaseEntity(TId id, string name)
    {
        _id = id;
        _name = name;
    }

    ...

}

领域服务抽象由两个泛型abstract基类提供,这两个基类使用存储库和只读存储库分别调用BaseServiceBaseReadOnlyService毫不奇怪的,BaseService扩展了BaseReadOnlyService。通过构造函数注入存储库实例来处理存储库依赖性。除了满足IRepository接口定义的契约之外,服务不需要知道有关存储库的具体实现的任何信息。这是依赖注入的实际应用。

public abstract class BaseService<TEntity, TId> : BaseReadOnlyService<TEntity, TId> 
                                                           where TEntity : IEntity<TId>
{
    private readonly IRepository<TEntity, TId> _repository;
 
    protected BaseService(IRepository<TEntity, TId> repository)
        : base(repository)
    {
        _repository = repository;
    }

    ...

}

添加实体时的BaseService触发AddingAdded事件。Adding事件支持取消。更新或删除实体时会引发类似事件。

所有abstract基类都提供了某些接口成员的默认实现。这样做的好处是,您可以通过仅实施强制abstract成员来快速建立这些抽象的功能实现。如果您可以自己提出更好的实现,那么基类中的默认实现通常会被标记为virtual,以便您可以在自己的实现中override它们。

为了支持单元测试,一个泛型FakeRepository被提供。在这里,持久性是在内存中的对象——一个Dictionary对象中完成的。这在实际应用程序中显然是无用的,但是作为单元测试的虚假存储库是完美的。

public class FakeRepository<TEntity, TId> : IRepository<TEntity, TId> where TEntity : IEntity<TId>
{
    public FakeRepository()
    {
        Entities = new Dictionary<TId, TEntity>();
    }

    protected Dictionary<TId, TEntity> Entities { get; }

    ...

}

使用领域服务库

现在,让我们看一下领域服务库的使用示例。让我们添加对产品管理的支持。首先,您将通过abstract BaseEntity类的派生类来创建一Product类。定义Id-属性类型的泛型类型参数设置为Guid。在IEntity类中除了定义的Id-Name属性之外,添加至少一个Price属性到Product类中似乎是合理的。为简单起见,Price属性将忽略数据绑定支持:

public class Product : BaseEntity<Guid>
{
    public Product(string name)
        : base(Guid.NewGuid(), name)
    {
    }

    public decimal Price { get; set; }
}

现在,假设您要使用一种方法扩展产品存储库,以检测存储库是否已包含具有相同名称的产品。然后,您可以通过扩展IRepository接口来创建IProductRepository接口:

public interface IProductRepository : IRepository<Product, Guid>
{
    bool ContainsName(string name);
}

最后,是时候创建产品服务了。这基本上是通过从泛型abstract BaseService类派生来完成的。由于BaseService类中的所有方法都声明为virtual,因此您可以决定重写它们——例如,在添加产品时添加更多约束。如果你试图用一个已经存在的ID添加一个实体,BaseService已经抛出一个异常,但在下面的实现中,异常已经抛出,如果产品名称为空或未定义或者如果具有给定名称的产品已经存在。

public class Products : BaseService<Product, Guid>
{
    private readonly IProductRepository _repository;
 
    public Products(IProductRepository _repository)
        : base(repository)
    {
        _repository = repository;
    }
 
    public override void Add(Product product)
    {
        if (_repository.ContainsName(product.Name))
        {
            throw new ArgumentException
              ($"There is already a product with the name '{product.Name}'.", nameof(product));
        }
 
        if (string.IsNullOrEmpty(product.Name))
        {
            throw new ArgumentException
              ("Product name cannot be null or empty string.", nameof(product));
        }
 
        base.Add(product);
    }
}

在实际生产代码中,您现在可能会使用其他功能扩展产品服务——例如计算折扣价格,货币管理等。

不用说,用于为其他实体(例如用户、客户、活动等)建立类似服务的模式完全相同。

现在,让我们编写一些测试代码。因为您将IProductRepository作为IRepository接口的扩展,所以首先需要创建一个FakeProductRepository类作为FakeRepository类的扩展。ContainsName()方法必须实现——至少如果您想根据它测试功能。

internal class FakeProductRepository : FakeRepository<Product, Guid>, IProductRepository
{
    public bool ContainsName(string name)
    {
        return Entities.Values.Any(p => p.Name.Equals(name));
    }
}

现在,您可以测试例子,如果你尝试添加具有已存在名称的产品,则会引发预期的异常。注意如何使用构造函数将FakeProductRepository注入到Products服务:

[Fact]
public void AddWithExistingNameThrows()
{
    // Setup fixture
    var products = new Products(new FakeProductRepository());
    var product = new Product("MyProduct name");
    products.Add(product);
    var productWithSameName = new Product(product.Name);
 
    // Exercise system and verify outcome
    Assert.Throws<ArgumentException>(() => products.Add(productWithSameName));
}

下面是一个测试,验证删除产品时是否正确引发了DeletingDeleted事件:

[Fact]
public void EventsAreRaisedOnRemove()
{
    // Setup fixture
    var raisedEvents = new List<string>();
    var products = new Products(new FakeProductRepository());
    products.Deleting += (s, e) => { raisedEvents.Add("Deleting"); };
    products.Deleted += (s, e) => { raisedEvents.Add("Deleted"); };
    var product = new Product("MyProduct name");
    products.Add(product);
 
    // Exercise system
    products.Remove(product.Id);
 
    // Verify outcome
    Assert.Equal("Deleting", raisedEvents[0]);
    Assert.Equal("Deleted", raisedEvents[1]);
}

最后,这是一个测试,证明使用lambda表达式的查询机制按预期工作:

[Fact]
public void GetQueryableIsOk()
{
    // Setup fixture
    var products = new Products(new FakeProductRepository());
    var coke = new Product("Coke") {Price = 9.95M};
    var cokeLight = new Product("Coke Light") {Price = 10.95M};
    var fanta = new Product("Fanta") {Price = 8.95M};
    products.Add(coke);
    products.Add(cokeLight);
    products.Add(fanta);
 
    // Exercise system
    var cheapest = products.Get(p => p.Price < 10M).ToList();
    var cokes = products.Get(p => p.Name.Contains("Coke")).ToList();
 
    // Verify outcome
    Assert.Equal(2, cheapest.Count());
    Assert.Equal(2, cokes.Count());
}

示例代码中提供了更多测试。

组件

源代码分为以下DLL(项目):

https://www.codeproject.com/KB/architecture/730191/component-dependencies.png

DomainServices项目以通用接口和abstract类的形式包含基本抽象——例如,abstract BaseService类和泛型IRepository接口。这是领域服务库本身。

MyServices项目包含DomainServices抽象的一些具体实现和扩展——例如,Products类和IProductRepository接口。

MyServices.Test项目包含MyServices类型的单元测试类。

MyServices.Data项目包含定义在MyServices中的存储库接口的具体实现——例如,一个JsonProductRepository,它是IProductRepository存储产品的实现,在JSON文件中序列化。

总结

本文提供了一个非常简单且轻量级但有用的领域服务库的具体示例。这些组成部分是一些面向对象的软件开发原则以及存储库模式和依赖注入。该库仅包含普通的C#类。

通过使用依赖注入实现的松散耦合使得使用伪存储库对象很容易建立领域功能的单元测试。为此目的提供了泛型FakeRepository类。

实用信息

测试是使用xUnit.NET编写的,这是我个人的最爱。xUnit.NET可通过Visual Studio中的NuGet包管理器获得。在生产代码中,您应该认真考虑使用辅助单元测试框架(如MoqAutofixture来帮助您简化模拟和固定设置。这两个库都可以通过NuGet获得。

生产代码显然还需要具体实现各种存储库。在示例代码中,我在JSON文件中添加了一个简单的JsonProductRepository产品持久性。该存储库使用可通过NuGet获得的Json.NET库。我没有为这个存储库提供单元测试。

由于领域服务库正在利用依赖注入,因此非常适合与依赖注入容器(例如NInjectUnity)一起使用 - 两者都可通过NuGet获得。

示例代码使用.NET Framework 4.6.1Visual Studio 2017C7)中生成。

 

原文地址:https://www.codeproject.com/Articles/730191/Lightweight-Domain-Services-Library

猜你喜欢

转载自blog.csdn.net/mzl87/article/details/87991418