前言

简单介绍一下实体模型的设计。

正文

前文提及了我们的应用分为:

  1. 共享层

  2. 基础设施层

  3. 领域层

  4. 应用层

今天来介绍领域模型层。

前文提及到领域模型在共享层有一个领域模型抽象类库。

里面有这些类:

先分别介绍一下这些类是做什么的。

IEntity 类,是我们实体的接口:

/// <summary>
/// 实体接口(包含多个主键的实体接口)
/// </summary>
public interface IEntity
{
object[] GetKeys();
} /// <summary>
/// 实体接口(包含唯一主键Id的实体接口)
/// </summary>
/// <typeparam name="TKey">主键ID类型</typeparam>
public interface IEntity<TKey> : IEntity
{
TKey Id { get; }
}

实现抽象类Entity:

/// <summary>
/// 实体抽象类(包含多个主键的实体接口)
/// </summary>
public abstract class Entity : IEntity
{
public abstract object[] GetKeys(); public override string ToString()
{
return $"[Entity:{GetType().Name}] Keys = {string.Join(",", GetKeys())}";
} #region 领域事件定义处理 DomainEvents /// <summary>
/// 领域事件集合
/// </summary>
private List<IDomainEvent> _domainEvents; /// <summary>
/// 获取当前实体对象领域事件集合(只读)
/// </summary>
public IReadOnlyCollection<IDomainEvent> DomainEvents => _domainEvents?.AsReadOnly(); /// <summary>
/// 添加领域事件至当前实体对象领域事件结合中
/// </summary>
/// <param name="eventItem"></param>
public void AddDomainEvent(IDomainEvent eventItem)
{
_domainEvents = _domainEvents ?? new List<IDomainEvent>();
_domainEvents.Add(eventItem);
} /// <summary>
/// 移除指定领域事件
/// </summary>
/// <param name="eventItem"></param>
public void RemoveDomainEvent(IDomainEvent eventItem)
{
_domainEvents?.Remove(eventItem);
} /// <summary>
/// 清空所有领域事件
/// </summary>
public void ClearDomainEvents()
{
_domainEvents?.Clear();
} #endregion
} /// <summary>
/// 实体抽象类(包含唯一主键Id的实体接口)
/// </summary>
/// <typeparam name="TKey">主键ID类型</typeparam>
public abstract class Entity<TKey> : Entity, IEntity<TKey>
{
int? _requestedHasCode;
public virtual TKey Id { get; protected set; }
public override object[] GetKeys()
{
return new object[] { Id };
} /// <summary>
/// 对象是否想等
/// </summary>
/// <param name="obj"></param>
/// <returns></returns>
public override bool Equals(object obj)
{
if (obj == null || !(obj is Entity<TKey>))
{
return false;
} if (Object.ReferenceEquals(this, obj))
{
return true;
} Entity<TKey> item = (Entity<TKey>)obj; if (item.IsTransient() || this.IsTransient())
{
return false;
}
else
{
return item.Id.Equals(this.Id);
}
} public override int GetHashCode()
{
if (!IsTransient())
{
if (!_requestedHasCode.HasValue)
{
_requestedHasCode = this.Id.GetHashCode() ^ 31; // TODO
}
return _requestedHasCode.Value;
}
else
{
return base.GetHashCode();
}
} /// <summary>
/// 对象是否为全新创建的,未持久化的
/// </summary>
/// <returns></returns>
public bool IsTransient()
{
return EqualityComparer<TKey>.Default.Equals(Id, default);
}
public override string ToString()
{
return $"[Entity:{GetType().Name}] Id = {Id}";
} /// <summary>
/// == 操作符重载
/// </summary>
/// <param name="left"></param>
/// <param name="right"></param>
/// <returns></returns>
public static bool operator ==(Entity<TKey> left,Entity<TKey> right)
{
if (Object.Equals(left,null))
{
return (Object.Equals(right, null)) ? true : false;
}
else
{
return left.Equals(right);
}
} /// <summary>
/// != 操作符重载
/// </summary>
/// <param name="left"></param>
/// <param name="right"></param>
/// <returns></returns>
public static bool operator !=(Entity<TKey> left, Entity<TKey> right)
{
return !(left == right);
}
}

聚合根接口IAggregateRoot.cs:

/// <summary>
/// 聚合根接口
/// 作用是我们在实现仓储层的时候,让我们的一个仓储对应一个聚合根
/// </summary>
public interface IAggregateRoot
{
}

领域事件IDomainEvent :

/// <summary>
/// 领域事件接口
/// 用来标记我们某一个对象是否是领域事件
/// </summary>
public interface IDomainEvent : INotification
{
}

领域事件的处理接口IDomainEventHandler:

/// <summary>
/// 领域事件处理器接口
/// </summary>
public interface IDomainEventHandler<TDomainEvent> : INotificationHandler<TDomainEvent> where TDomainEvent : IDomainEvent
{
//这里我们使用了INotificationHandler的Handle方法来作为处理方法的定义,所以无需重新定义
//Task Handle(TDomainEvent domainEvent, CancellationToken cancellationToken);
}

值对象 ValueObject:

/// <summary>
/// 值对象
/// TODO 领域驱动中比较关键的类
/// </summary>
public abstract class ValueObject
{
protected static bool EqualOperator(ValueObject left, ValueObject right)
{
if (ReferenceEquals(left, null) ^ ReferenceEquals(right, null))
{
return false;
}
return ReferenceEquals(left, null) || left.Equals(right);
} protected static bool NotEqualQperator(ValueObject left, ValueObject right)
{
return !(EqualOperator(left, right));
} /// <summary>
/// 获取值对象原子值(字段值)
/// 将我们值对象的字段输出出来,作为唯一标识来判断我们两个对象是否想等
/// </summary>
/// <returns></returns>
protected abstract IEnumerable<object> GetAtomicValues(); public override bool Equals(object obj)
{
if (obj == null || obj.GetType() != GetType())
{
return false;
}
ValueObject other = (ValueObject)obj;
IEnumerator<object> thisValues = GetAtomicValues().GetEnumerator();
IEnumerator<object> otherValues = other.GetAtomicValues().GetEnumerator();
while (thisValues.MoveNext() && otherValues.MoveNext())
{
if (ReferenceEquals(thisValues.Current, null) ^ ReferenceEquals(otherValues.Current, null))
{
return false;
}
if (thisValues.Current != null && !thisValues.Current.Equals(otherValues.Current))
{
return false;
}
}
return !thisValues.MoveNext() && !otherValues.MoveNext();
} public override int GetHashCode()
{
return GetAtomicValues()
.Select(x => x != null ? x.GetHashCode() : 0)
.Aggregate((x, y) => x ^ y);
}
}

那么来看一下领域模型的具体实现:

先来看一下Aggregate 的具体实现:

/// <summary>
/// 订单实体
/// </summary>
public class Order : Entity<long>, IAggregateRoot
{
// 实体内字段的 set 方法都是 private 的
// 实体类型相关的数据操作,都应该是由我们实体来负责,而不是被外部的对象去操作
// 这样的好处是让我们的领域模型符合封闭开放的原则 public string UserId { get; private set; }
public string UserName { get; private set; }
public Address Address { get; private set; }
public int ItemCount { get; set; } protected Order()
{
} public Order(string userId, string userName, int itemCount, Address address)
{
this.UserId = userId;
this.UserName = userName;
this.ItemCount = itemCount;
this.Address = address; // 构造新的Order对象的时候,添加一个创建Order领域事件
this.AddDomainEvent(new OrderCreatedDomainEvent(this));
} /// <summary>
/// 修改收货地址
/// </summary>
/// <param name="address"></param>
public void ChangeAddress(Address address)
{
this.Address = address; // 同样的,在修改地址操作时,也该定义一个类似的修改地址领域事件
//this.AddDomainEvent(new OrderAddressChangedDomainEvent(this));
}
}

这里面实现了Entity,同时实现了IAggregateRoot,这个接口里面没有任何东西,表示这是定义接口,表示将这个Order 定义为一个聚合根。

看一下Address:

/// <summary>
/// 地址实体
/// 定义为值对象
/// </summary>
public class Address : ValueObject
{
public string Street { get; private set; }
public string City { get; private set; }
public string ZipCode { get; private set; }
public Address()
{ }
public Address(string street, string city, string zipCode)
{
this.Street = street;
this.City = city;
this.ZipCode = zipCode;
} /// <summary>
/// 重载获取原子值的方法
/// 这里特殊的是,我们使用了 yield return 的方式
/// </summary>
/// <returns></returns>
protected override IEnumerable<object> GetAtomicValues()
{
yield return Street;
yield return City;
yield return ZipCode;
}
}

将这个Address 定义为值对象。

梳理

这里如果不了解领域设计,会有点蒙。

那么这里只需要知道这里Order 定义为了aggregateRoot,也就是聚合根。

把Address 定义为了值对象即可。随着后面系列的他们之间的调用会越来越清晰的。

总结

  1. 将领域模型字段的修改设置为私有的。

  2. 使用构造函数表示对象的创建

  3. 使用具有业务的动作来操作模型字段

  4. 领域模型复制堆自己数据的处理

  5. 领域模型负责对自己数据的处理

  6. 领域服务或命令处理者扶着调用领域模型业务动作

下一节 领域模型之工作单元模式

重新整理 .net core 实践篇—————Entity的定义[二十五]的更多相关文章

  1. 重新整理 .net core 实践篇—————日志系统之战地记者[十五]

    前言 本节开始整理日志相关的东西.先整理一下日志的基本原理. 正文 首先介绍一下包: Microsoft.Extengsion.Logging.Abstrations 这个是接口包. Microsof ...

  2. 重新整理 .net core 实践篇—————工作单元模式[二十六]

    前言 简单整理一下工作单元模式. 正文 工作单元模式有3个特性,也算是其功能: 使用同一上下文 跟踪实体的状态 保障事务一致性 工作单元模式 主要关注事务,所以重点在事务上. 在共享层的基础建设类库中 ...

  3. 重新整理 .net core 实践篇————依赖注入应用[二]

    前言 这里介绍一下.net core的依赖注入框架,其中其代码原理在我的另一个整理<<重新整理 1400篇>>中已经写了,故而专门整理应用这一块. 以下只是个人整理,如有问题, ...

  4. 重新整理 .net core 实践篇—————服务与配置之间[十一二]

    前言 前面基本介绍了,官方对于asp .net core 设计配置和设计服务的框架的一些思路.看下服务和配置之间是如何联系的吧. 正文 服务: public interface ISelfServic ...

  5. 重新整理 .net core 实践篇————polly失败重试[三十四]

    前言 简单整理一下polly 重试. 正文 在开发程序中一般都有一个重试帮助类,那么polly同样有这个功能. polly 组件包: polly 功能包 polly.Extensions.Http 专 ...

  6. 重新整理 .net core 实践篇—————3种配置验证[十四]

    前言 简单整理一些配置的验证. 正文 配置的验证大概分为3类: 直接注册验证函数 实现IValidteOptions 使用Microsoft.Extensions.Options.DataAnnota ...

  7. 重新整理 .net core 实践篇—————路由和终结点[二十三]

    前言 简单整理一下路由和终节点. 正文 路由方式主要有两种: 1.路由模板方式 2.RouteAttribute 方式 路由约束: 1.类型约束 2.范围约束 3.正则表达式 4.是否必选 5.自定义 ...

  8. 重新整理 .net core 实践篇————配置应用[一]

    前言 本来想整理到<<重新整理.net core 计1400篇>>里面去,但是后来一想,整理 .net core 实践篇 是偏于实践,故而分开. 因为是重新整理,那么就从配置开 ...

  9. .NET Core实战项目之CMS 第十五章 各层联动工作实现增删改查业务

    连着两天更新叙述性的文章大家可别以为我转行了!哈哈!今天就继续讲讲我们的.NET Core实战项目之CMS系统的教程吧!这个系列教程拖得太久了,所以今天我就以菜单部分的增删改查为例来讲述下我的项目分层 ...

  10. abp(net core)+easyui+efcore实现仓储管理系统——EasyUI之货物管理七(二十五)

    abp(net core)+easyui+efcore实现仓储管理系统目录 abp(net core)+easyui+efcore实现仓储管理系统——ABP总体介绍(一) abp(net core)+ ...

随机推荐

  1. .NET Conf China 2023济南站社区活动

     2024年3月3日,在这个春暖花开的日子里,由微软MVP项目.山东财经大学管理科学与工程学院.胶东开发者社区.济南.NET俱乐部联合举办了[.NET Conf China 2023 JiNan Wa ...

  2. vue-helper 点击跳转插件 在 methods里面互相调用函数,会产生两个函数definitions ,然后就回弹出框让你选择,解决方案是加配置

    vue-helper 点击跳转插件 在 methods里面互相调用函数,会产生两个函数definitions ,然后就回弹出框让你选择 原因:换了台电脑,又从新配置下vscode "edit ...

  3. 重新认识 tag 快照 git (项目临时添加需求,之前有分支合并,导致从节点拉分支不行了,因为没有tag快照)

    之前的tag认知 之前一直以为tag就是在git的提交commit上打一个标,然后可以拉出分支.之前没太重视. 因为我觉得 可以直接从某个commit直接拉出分支,这打不打tag无所谓 翻车现场 今天 ...

  4. 摆脱鼠标操作 vscode-vim-use-readme.md

    vscode-vim 学习笔记 梳理下自己定义的快捷键 Normal模式返回 ESC capsLock 双击shift ctrl+[ jj ctrl+c (这个键比较特殊 用习惯y的话,考虑这个) 一 ...

  5. reciterdoc 资料库 支持中文搜索了。 vuepress-plugin-fulltext-search(用一半)

    http://pengchenggang.gitee.io/reciterdoc/ 支持中文搜索了,可是不容易了 通过插件 vuepress-plugin-fulltext-search 实现全文搜索 ...

  6. base-table 加入动态slot 流程 vue2

    columns { title: '字段标题', slot: 'yourSlotName', minWidth: 50, align: 'center' }, 组件内 props: { columns ...

  7. Android自定义View使用系统自有属性

    原文链接: Android自定义View使用系统自有属性 - Stars-One的杂货小窝 本篇默认各位有自定义View的相关知识,本篇只作为一个小知识点补充 有这样一种情况,比如说我们的一个自定义V ...

  8. 几个常用的cmd命令

    compmgmt.msc 计算机管理  devmgmt.msc 设备管理器  diskmgmt.msc 磁盘管理工具  dfrg.msc 磁盘碎片整理  eventvwr.msc 事件查看器  fsm ...

  9. 记录--求你了,别再说不会JSONP了

    这里给大家分享我在网上总结出来的一些知识,希望对大家有所帮助 JSONP是一种很远古用来解决跨域问题的技术,当然现在实际工作当中很少用到该技术了,但是很多同学在找工作面试过程中还是经常被问到,本文将带 ...

  10. 记录--纯CSS实现骚气红丝带

    这里给大家分享我在网上总结出来的一些知识,希望对大家有所帮助 在本文中,我们将探讨如何使用 CSS 以最少的代码创造出精美的 CSS 丝带形状,并最终实现下面这个效果: 下面我们使用html和css来 ...