.Net Core 教程 Part6 – (23)(24)(25)(26)(27)(28)(29)DDD落地之整洁架构

Part6 – (23) 洋葱架构(整洁架构)

分层架构和传统三层架构

1、分层架构:把各个组件按照“高内聚、低耦合”的原则组织到不同的项目中。

2、传统的经典三层架构

.Net Core 教程 Part6 – (23)(24)(25)(26)(27)(28)(29)DDD落地之整洁架构
三层架构

三层架构的缺点:尽管由DAL,但仍然是面向数据库的思维方式;对于一些简单的、不包含业务逻辑的增删改查类操作,仍然需要BLL进行转发;依赖关系是单向的,所以下一层中的代码不能使用上一层中的逻辑。

整洁架构(洋葱架构)

1、内层的部分比外层的部分更加的抽象→内层表达抽象,外层表达实现

2、外层的代码只能调用内层的代码内层的代码可以通过依赖注入的形式来间接调用外层的代码。举一个简单的例子:读取文件然后发送邮件。对比三层架构谈洋葱架构的优点。

.Net Core 教程 Part6 – (23)(24)(25)(26)(27)(28)(29)DDD落地之整洁架构
整洁架构
//a@b.com|你好|刘总好
//b@c.com|Hello|ajhsdhk
string[] lines = File.ReadAllLines("e:/temp/1.txt");
foreach(var line in lines)
{
  string[] segments = line.Split('|');
  string email = segments[0];
  string title = segments[1];
  string body = segments[2];
  Console.WriteLine($"发送邮件:{email}{title}{body}");//smtp/调用服务接口
}
//如何解耦?
.Net Core 教程 Part6 – (23)(24)(25)(26)(27)(28)(29)DDD落地之整洁架构
抽象和实现分离

防腐层(Anti Corruption Layer, ACL)

外部服务(短信服务、邮件服务、存储服务等)的变化会比较频繁。把这些服务定义为接口,在内层代码中我们只定义和使用接口,在外层代码中定义接口的实现。

体现的仍然是洋葱架构的理念。

Part6 – (24) 项目分层

需求

1、一个包含用户管理、用户登录功能的微服务,系统的后台允许添加用户解锁用户修改用户密码等;系统的前台允许用户使用手机号加密码进行登录,也允许用户使用手机号加短信验证码进行登录;如果多次尝试登录失败,则账户会被锁定一段时间;为了便于审计,无论是登录成功的操作还是登录失败的操作,我们都要记录操作日志

2、为了简化问题,这个案例中没有对于接口调用进行鉴权,也没有防暴力破解等安全设置。

.Net Core 教程 Part6 – (23)(24)(25)(26)(27)(28)(29)DDD落地之整洁架构
依赖关系

层级解读:

  • Domain: 实体类、事件、防腐层接口、仓储接口、领域服务;
  • Infrastructure: 实现类的配置、DbContext、防腐层接口实现、仓储接口实现;
  • WebAPI: Controller、事件(领域事件和集成事件)的响应类;
.Net Core 教程 Part6 – (23)(24)(25)(26)(27)(28)(29)DDD落地之整洁架构
洋葱架构的项目分层

技术选型

对于ASP.NET Core Web API项目来讲,是否需要拆分出应用服务和用户界面层?

1)有的人认为前端代码是用户界面,而Web API的控制器的代码就是应用服务

2)有的人认为控制器也是一种用户界面,因此需要再拆分出来一个应用服务层,由控制器再调用应用服务层。

Part6 – (25) 领域模型的实现

实体

1、“用户”(User)实体类。没有基于Identity框架,因为……

2、“用户登录失败次数过多则锁定”这个需求并不属于“用户”这个实体中一个常用的特征,因此我们应当把它拆分到一个单独的实体中,因此我们识别出来一个单独的“用户登录失败”(UserAccessFail)实体;

3、“用户登录记录”(UserLoginHistory)也应该识别为一个单独的实体。

4、把User和UserAccessFail设计为同一个聚合,并且把User设置为聚合根;

5、有单独查询一段时间内的登录记录等这样独立于某个用户的需求,因此我们把UserLoginHistory设计为一个单独的聚合。

6、DbContext要定义到基础设施层。

手机号值对象

考虑到我们的系统可能被海外用户访问,而海外用户的手机号还需要包含“国家/地区码”,因此我们设计了用来表示手机号的值对象PhoneNumber。

public record PhoneNumber(int RegionCode,string Number);

Part6 – (26) 领域服务的实现

仓储接口

1、仓储接口的定义放在领域层中。

public interface IUserDomainRepository
{
    Task<User?> FindOneAsync(PhoneNumber phoneNumber);
    Task<User?> FindOneAsync(Guid userId);
    Task AddNewLoginHistoryAsync(PhoneNumber phoneNumber, string msg);
    Task PublishEventAsync(UserAccessResultEvent eventData);
    Task SavePhoneCodeAsync(PhoneNumber phoneNumber, string code);
    Task<string?> RetrievePhoneCodeAsync(PhoneNumber phoneNumber);
}

3、不建议用通用CRUDRepository,避免陷入“伪DDD”。

防腐层接口

public interface ISmsCodeSender
{
	Task SendCodeAsync(PhoneNumber phoneNumber,string code);
}

Part6 – (27) 基础设施的实现

原则

1、领域模型、领域服务中只是定义了抽象的实体、防腐层和仓储,我们需要在基础设施中对它们进行落地和实现。

2、实体类、值对象的定义是和持久机制无关的,而它们需要通过EF Core的配置、上下文等建立和数据库的关系。

3、上下文等也是和持久层相关的,也放到基础设施。

Part6 – (28) 工作单元的实现

原则

1、工作单元是由应用服务层来确定,其他层不应该调用SaveChangesAsync方法保存对数据的修改。 2、可以开发一个在控制器的方法调用结束后自动调用。

SaveChangesAsync的Filter:UnitOfWorkAttribute、UnitOfWorkFilter。
public class UnitOfWorkAttribute:Attribute
{
	public Type[] DbContextTypes { get; init; }
	public UnitOfWorkAttribute(params Type[] dbContextTypes)
	{
		this.DbContextTypes = dbContextTypes;
	}
}

Part6 – (29) 应用层的实现

实现

1、应用层主要进行的是数据的校验、请求数据的获取、领域服务返回值的显示等处理,并没有复杂的业务逻辑,因为主要的业务逻辑都被封装在领域层。

2、应用层是非常薄的一层,应用层主要进行安全认证、权限校验、数据校验、事务控制、工作单元控制、领域服务的调用等。从理论上来讲,应用层中不应该有业务规则或者业务逻辑。

3、监听登录失败或者成功的领域事件UserAccessResultEvent,记录到LoginHistory:

repository.AddNewLoginHistoryAsync(phoneNum,msg);

对于增删改查等这种简单的业务场景,我们没必要拘泥于DDD的原则。这也是洋葱架构的优点。

本文版权归个人技术分享站点所有,发布者:chaoqiang,转转请注明出处:https://www.zhengchaoqiang.com/1693.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-04-04 20:58
下一篇 2022-06-09 07:35

相关推荐

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注

近期个人博客正在迁移中,原博客请移步此处,抱歉!