Part6 – (23) 洋葱架构(整洁架构)
分层架构和传统三层架构
1、分层架构:把各个组件按照“高内聚、低耦合”的原则组织到不同的项目中。
2、传统的经典三层架构
三层架构的缺点:尽管由DAL,但仍然是面向数据库的思维方式;对于一些简单的、不包含业务逻辑的增删改查类操作,仍然需要BLL进行转发;依赖关系是单向的,所以下一层中的代码不能使用上一层中的逻辑。
整洁架构(洋葱架构)
1、内层的部分比外层的部分更加的抽象→内层表达抽象,外层表达实现。
2、外层的代码只能调用内层的代码,内层的代码可以通过依赖注入的形式来间接调用外层的代码。举一个简单的例子:读取文件然后发送邮件。对比三层架构谈洋葱架构的优点。
//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/调用服务接口
}
//如何解耦?
防腐层(Anti Corruption Layer, ACL)
外部服务(短信服务、邮件服务、存储服务等)的变化会比较频繁。把这些服务定义为接口,在内层代码中我们只定义和使用接口,在外层代码中定义接口的实现。
体现的仍然是洋葱架构的理念。
Part6 – (24) 项目分层
需求
1、一个包含用户管理、用户登录功能的微服务,系统的后台允许添加用户、解锁用户、修改用户密码等;系统的前台允许用户使用手机号加密码进行登录,也允许用户使用手机号加短信验证码进行登录;如果多次尝试登录失败,则账户会被锁定一段时间;为了便于审计,无论是登录成功的操作还是登录失败的操作,我们都要记录操作日志。
2、为了简化问题,这个案例中没有对于接口调用进行鉴权,也没有防暴力破解等安全设置。
层级解读:
- Domain: 实体类、事件、防腐层接口、仓储接口、领域服务;
- Infrastructure: 实现类的配置、DbContext、防腐层接口实现、仓储接口实现;
- WebAPI: Controller、事件(领域事件和集成事件)的响应类;
技术选型
对于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