Seedwork(领域模型的可复用基类和接口)

The solution folder contains a SeedWork folder. This folder contains custom base classes that you can use as a base for your domain entities and value objects. Use these base classes so you don't have redundant code in each domain's object class. The folder for these types of classes is called SeedWork and not something like Framework. It's called SeedWork because the folder contains just a small subset of reusable classes that cannot really be considered a framework. Seedwork is a term introduced by Michael Feathers and popularized by Martin Fowler, but you could also name that folder Common, SharedKernel, or similar.

解决方案文件夹中包含一个 Seedwork 文件夹。该文件夹包含了自定义的基类,你可以将这些基类作为领域实体和值对象的基础。使用这些基类,可以避免在每个领域对象类中编写重复的代码。存放这类代码的文件夹被命名为 Seedwork,而不是 Framework(框架)之类的名字。之所以叫 Seedwork,是因为这个文件夹只包含了一小部分可复用的类,还远不足以称得上是一个真正的框架。Seedwork 这个术语最初由 Michael Feathers 提出,并由 Martin Fowler 推广普及,不过你也可以把这个文件夹命名为 Common(公共)、SharedKernel(共享内核)或类似的名称。

Figure 7-12 shows the classes that form the seedwork of the domain model in the ordering microservice. It has a few custom base classes like EntityValueObject, and Enumeration, plus a few interfaces. These interfaces (IRepository and IUnitOfWork) inform the infrastructure layer about what needs to be implemented. Those interfaces are also used through Dependency Injection from the application layer.

图 7-12 展示了订购微服务中构成领域模型 Seedwork 的那些类。它包含了一些自定义基类,比如 Entity(实体)、ValueObject(值对象)和 Enumeration(枚举),以及一些接口。这些接口(IRepository 和 IUnitOfWork)用于告知基础设施层需要实现哪些功能。此外,这些接口也会通过应用层的依赖注入(Dependency Injection)来使用。

Screenshot of the classes contained in the SeedWork folder.

The detailed contents of the SeedWork folder, containing base classes and interfaces: Entity.cs, Enumeration.cs, IAggregateRoot.cs, IRepository.cs, IUnitOfWork.cs, and ValueObject.cs.

SeedWork 文件夹的详细内容,其中包含了基类和接口:Entity.cs、Enumeration.cs、IAggregateRoot.cs、IRepository.cs、IUnitOfWork.cs 以及 ValueObject.cs。

Figure 7-12. A sample set of domain model "seedwork" base classes and interfaces

图 7-12. 一组领域模型“Seedwork”基类和接口的示例

This is the type of copy and paste reuse that many developers share between projects, not a formal framework. You can have seedworks in any layer or library. However, if the set of classes and interfaces gets large enough, you might want to create a single class library.

这属于那种许多开发者在不同项目之间共享的、类似于“复制粘贴”式的复用代码,而不是一个正式的框架。你可以在任何层级或类库中拥有 Seedwork。不过,如果这些类和接口的数量变得足够多,你可能就需要考虑将它们单独提取出来,创建一个独立的类库了。

The custom Entity base class    自定义 Entity 基类

The following code is an example of an Entity base class where you can place code that can be used the same way by any domain entity, such as the entity ID, equality operators, a domain event list per entity, etc.

下面的代码是一个 Entity 基类的示例。你可以在这个基类中放置任何领域实体都能以相同方式使用的代码,例如实体 ID、相等性运算符、每个实体对应的领域事件列表等等。

// COMPATIBLE WITH ENTITY FRAMEWORK CORE (1.1 and later)
/// <summary>
/// 领域实体基类,提供唯一标识、领域事件管理和基于ID的相等性比较逻辑。
/// 继承此类的实体将自动具备值对象比较特性和生命周期管理能力。
/// </summary>
public abstract class Entity
{
    // 用于缓存非临时实体的哈希码,提升性能
    int? _requestedHashCode;
    
    // 实体的唯一标识符私有字段
    int _Id;

    // 存储当前实体产生的领域事件列表
    private List<INotification> _domainEvents;

    /// <summary>
    /// 获取或设置实体的唯一标识符(ID)。
    /// </summary>
    public virtual int Id
    {
        get
        {
            return _Id;
        }
        protected set
        {
            _Id = value;
        }
    }

    /// <summary>
    /// 获取当前实体关联的领域事件只读列表。
    /// </summary>
    public List<INotification> DomainEvents => _domainEvents;

    /// <summary>
    /// 向当前实体添加一个领域事件。
    /// </summary>
    /// <param name="eventItem">要添加的事件对象</param>
    public void AddDomainEvent(INotification eventItem)
    {
        // 延迟初始化:如果事件列表为空,则创建新列表
        _domainEvents = _domainEvents ?? new List<INotification>();
        _domainEvents.Add(eventItem);
    }

    /// <summary>
    /// 从当前实体中移除一个领域事件。
    /// </summary>
    /// <param name="eventItem">要移除的事件对象</param>
    public void RemoveDomainEvent(INotification eventItem)
    {
        // 防御性编程:如果列表为空则直接返回
        if (_domainEvents is null) return;
        _domainEvents.Remove(eventItem);
    }

    /// <summary>
    /// 判断当前实体是否为临时实体(即尚未持久化,ID为默认值)。
    /// </summary>
    /// <returns>如果是新创建的实体返回 true,否则返回 false</returns>
    public bool IsTransient()
    {
        return this.Id == default(Int32);
    }

    /// <summary>
    /// 判断当前实体是否与另一个对象相等。
    /// 实现了基于身份(ID)的逻辑相等性,而非引用相等性。
    /// </summary>
    /// <param name="obj">要比较的对象</param>
    /// <returns>如果两个实体ID相同且均非临时实体,则返回 true</returns>
    public override bool Equals(object obj)
    {
        // 如果传入对象为空 或 类型不匹配,返回 false
        if (obj == null || !(obj is Entity))
            return false;
        
        // 如果是同一个引用,直接返回 true(提升性能)
        if (Object.ReferenceEquals(this, obj))
            return true;
        
        // 如果类型不同(例如子类与父类比较),返回 false
        if (this.GetType() != obj.GetType())
            return false;

        // 转换为实体对象进行比较
        Entity item = (Entity)obj;

        // 如果任一实体是临时的(ID为0),则认为它们不相等(临时实体每次都是新的)
        if (item.IsTransient() || this.IsTransient())
            return false;
        else
            // 核心逻辑:比较两个实体的 ID 是否相等
            return item.Id == this.Id;
    }

    /// <summary>
    /// 获取实体的哈希码。
    /// 重写此方法以确保逻辑相等的实体具有相同的哈希码,满足哈希表存储要求。
    /// </summary>
    /// <returns>基于实体ID计算的哈希码</returns>
    public override int GetHashCode()
    {
        // 如果实体不是临时的(即有有效的 ID)
        if (!IsTransient())
        {
            // 只有在尚未计算过的情况下才进行计算(缓存结果)
            if (!_requestedHashCode.HasValue)
            {
                // 使用 XOR (^) 运算符结合质数 (31) 来打乱比特位,确保哈希值的随机分布
                // 参考:Eric Lippert 关于 GetHashCode 的规则与指南
                // 链接:https://learn.microsoft.com/archive/blogs/ericlippert/guidelines-and-rules-for-gethashcode
                _requestedHashCode = this.Id.GetHashCode() ^ 31;
            }
            return _requestedHashCode.Value;
        }
        else
        {
            // 临时实体直接使用基类(Object)的哈希码
            return base.GetHashCode();
        }
    }

    /// <summary>
    /// 重载等于运算符 (==),使其行为与 Equals 方法保持一致。
    /// </summary>
    /// <param name="left">左侧实体</param>
    /// <param name="right">右侧实体</param>
    /// <returns>比较结果</returns>
    public static bool operator ==(Entity left, Entity right)
    {
        // 如果左侧为 null,右侧也为 null 则返回 true,否则返回 false
        if (Object.Equals(left, null))
            return (Object.Equals(right, null));
        else
            // 否则调用左侧对象的 Equals 方法
            return left.Equals(right);
    }

    /// <summary>
    /// 重载不等于运算符 (!=)。
    /// </summary>
    /// <param name="left">左侧实体</param>
    /// <param name="right">右侧实体</param>
    /// <returns>比较结果</returns>
    public static bool operator !=(Entity left, Entity right)
    {
        // 直接取反等于运算符的结果
        return !(left == right);
    }
}

The previous code using a domain event list per entity will be explained in the next sections when focusing on domain events.

上一段代码中使用了每个实体包含一个领域事件列表的做法,这将在后续专注于领域事件的章节中进行详细解释。

Repository contracts (interfaces) in the domain model layer    领域模型层中的仓储契约(接口)

Repository contracts are simply .NET interfaces that express the contract requirements of the repositories to be used for each aggregate.

仓储契约(Repository contracts)仅仅是 .NET 接口,它们表达了每个聚合所需要使用的仓储的契约要求。

The repositories themselves, with EF Core code or any other infrastructure dependencies and code (Linq, SQL, etc.), must not be implemented within the domain model; the repositories should only implement the interfaces you define in the domain model.

仓储本身的具体实现,包括 EF Core 代码或任何其他基础设施的依赖项和代码(如 Linq、SQL 等),绝对不应该放在领域模型中实现;仓储只需要实现你在领域模型中定义的接口即可。

A pattern related to this practice (placing the repository interfaces in the domain model layer) is the Separated Interface pattern. As explained by Martin Fowler, "Use Separated Interface to define an interface in one package but implement it in another. This way a client that needs the dependency to the interface can be completely unaware of the implementation."

与这种实践(将仓储接口放在领域模型层)相关的模式叫做接口隔离模式(Separated Interface pattern)。正如 Martin Fowler 所解释的:“使用接口隔离模式,在一个程序包中定义接口,而在另一个程序包中实现它。这样一来,任何需要依赖该接口的客户端,就可以完全不知道具体的实现细节。”

Following the Separated Interface pattern enables the application layer (in this case, the Web API project for the microservice) to have a dependency on the requirements defined in the domain model, but not a direct dependency to the infrastructure/persistence layer. In addition, you can use Dependency Injection to isolate the implementation, which is implemented in the infrastructure/ persistence layer using repositories.

遵循接口隔离模式,可以让应用层(在本例中,即微服务的 Web API 项目)仅依赖于领域模型中定义的需求,而不会直接依赖基础设施/持久化层。此外,你还可以使用依赖注入(Dependency Injection)来隔离具体的实现,这些实现会使用仓储在基础设施/持久化层中完成。

For example, the following example with the IOrderRepository interface defines what operations the OrderRepository class will need to implement at the infrastructure layer. In the current implementation of the application, the code just needs to add or update orders to the database, since queries are split following the simplified CQRS approach.

例如,下面带有 IOrderRepository 接口的示例,定义了 OrderRepository 类在基础设施层需要实现哪些操作。在当前应用的实现中,代码只需要向数据库添加或更新订单即可,因为查询操作已经按照简化的 CQRS(命令查询职责分离)方法进行了拆分。

// 定义在 IOrderRepository.cs 文件中
/// <summary>
/// 订单仓储接口,继承自泛型仓储基接口,专门用于处理 Order 聚合根的数据访问操作。
/// </summary>
public interface IOrderRepository : IRepository<Order>
{
    /// <summary>
    /// 将一个新的订单添加到数据源中。
    /// </summary>
    /// <param name="order">要添加的订单聚合根实例。</param>
    /// <returns>添加成功后的订单实体。</returns>
    Order Add(Order order);

    /// <summary>
    /// 更新现有的订单信息。
    /// </summary>
    /// <param name="order">包含最新状态的订单聚合根实例。</param>
    void Update(Order order);

    /// <summary>
    /// 根据订单 ID 异步获取订单详情。
    /// </summary>
    /// <param name="orderId">订单的唯一标识符。</param>
    /// <returns>返回匹配指定 ID 的订单对象;若未找到则可能返回 null。</returns>
    Task<Order> GetAsync(int orderId);
}

// 定义在 IRepository.cs 文件中(属于 Domain Seedwork / 领域层种子代码)
/// <summary>
/// 泛型仓储基接口,为所有聚合根提供基础的数据访问契约和工作单元支持。
/// </summary>
/// <typeparam name="T">具体的聚合根类型,必须实现 IAggregateRoot 接口。</typeparam>
public interface IRepository<T> where T : IAggregateRoot
{
    /// <summary>
    /// 获取当前仓储关联的工作单元(Unit of Work),用于统一管控事务边界。
    /// </summary>
    IUnitOfWork UnitOfWork { get; }
}

 

posted @ 2026-05-17 18:30  菜鸟吊思  阅读(13)  评论(0)    收藏  举报