本文章是我听B站杨中科的所做的笔记
杨中科B站视频链接:.NET 6教程,.Net Core 2022视频教程,杨中科主讲_哔哩哔哩_bilibili
软件架构设计的坑
架构设计之怪现状
1、“迷信大公司”
2、“迷信流行技术”。坑老板指南
3、应该怎么做
架构是进化而来的
1、淘宝的进化故事
2、很多项目第一天就是奔着淘宝去的,然后。。。
3、“最小的可行性产品”MVP;“演进式结构”
4、软件退化以及如何预防
什么是微服务
单体结构项目
缺点:耦合;技术栈统一,软件包版本锁定;一蹦全崩;升级周期长;无法局部扩容
微服务结构项目
微服务结构优缺点:
优点:耦合性低,易于开发和维护;可以用不同技术栈;可以单独扩容;相互隔离,影响小;部署周期短; 缺点:对运维能力要求高;运行效率会降低;技术要求高,需要处理事务最终一致性等问题
微服务的误区
微服务架构应该是进化而来的;微服务的拆分进化
微服务第一定律:避免使用微服务,除非用充足的理由。---杨中科
什么是DDD,应该怎么学
什么是DDD
1、DDD(Domain-driven design,领域驱动设计)是一个很好的应用于微服务架构的方法论
2、在项目的全生命周期内,所有岗位的人员都基于对业务的相同的理解来开展工作。所有人员站在用户的角度、业务的角度去思考问题,而不是站在技术的角度去思考问题
3、诞生于2004年,兴起于2014(微服务元年)
DDD之难
1、DDD晦涩难懂,难以落地,因为DD是方法论,不是行动指南
2、“盐少许,油少许”,每个人对DDD的理解和落地都不同,而且没有绝对的对错
3、如果只学习DDD概念而没有了解如何应用的话,会感觉没有落地;而如果过早关注落地的话,会导致理解片面
DDD学习之道
1、正确姿势:“从理论到实践,从实践再到理论。。。。”讲课顺序:把概念讲解和技术落地分开。why?
2、不要一下子学DDD的整体。不同岗位、不同阶段的人先从自己的角度学习DDD的一部分
DDD之领域与领域模型
领域
1、“领域"(Domain):一个组织做的事情。子领域
2、领域的划分(以手机公司为例): 核心域:解决项目的核心问题,组织业务紧密相关 支撑域:解决项目的非核心问题,则具有组织特性,但不具有通用性 通用域:解决通用问题,没有组织特性
3、领域的不同分类决定了公司的研发重点
领域模型
1、对于领域内的对象进行建模,从而抽象出来模型。以银行为例。
2、我们的项目应用开始于创建领域模型,而不是考虑如何设计数据库和编写代码。使用领域模型,我们可以一直用业务语言去描述和构建系统,而不是使用技术人员的语言
事务脚本(×)
使用技术人员的语言去描述和实现业务事务。没有太多设计,没有考虑可扩展性,可维护性,流水账地编写代码
事务脚本的问题:代码的可维护性、可扩展性非常差。比如如何增加”取款金额大于5万元需要主管审批“、”通知短信“等功能
DDD之通用语言、界限上下文
通用语言
1、”我想要商品被删除“=》”我想要把删除的还原回来“=》”Windows回收站都能“
2、此”用户“非彼“用户”
3、通用语言:一个拥有确切含义,没有二义性的语言
界限上下文
通用语言离不开特定的语义环境,只有确定了通用语言所在的边界,才能没有歧义的描述一个业务对象
DDD之实体、值对象
实体
1、“标识符”用来唯一定位一个对象,在数据库中我们一般用表的主键来实现“标识符”。主键和标识符的思考角度不同
2、实体:拥有唯一的标识符,标识符的值不会改变,而对象的其他状态则会经历各种变化。标识符用来跟踪对象状态变化,一个实体的对象无论怎样变化,我们都能通过都能通过标识符定位这个对象
3、实体一般的表现形式就是EF Core中的实体类
值对象
1、值对象:没有标识符的对象,也有很多的属性,依附于某个实体对象而存在。比如“商家”的地址位置、衣服的RGB颜色
2、定义为值对象和普通属性的区别:体现整体关系
DDD之聚合、聚合根
聚合
1、目的:高内聚、低耦合。有关系的实体紧密协作,而关系很弱的实体被隔离
2、把关系紧密的实体放到一个聚合中,每个聚合中由一个实体作为聚合根(Aggregate root),所有对于聚合内对象的访问都通过聚合根来进行,外部对象只能持有对聚合根的引用
3、聚合根不仅仅是实体,还是所在聚合的管理者
聚合的意义
1、为什么聚合可以实现“高内聚、低耦合”
2、聚合体现的是现实世界中整体和部分的关系,比如订单与订单明细。整体封装了对部分的操作,部分与整体有相同的生命周期。部分不会单独与外部系统单独交互,与外部系统的交互都由整体来负责
聚合的划分很难
1、系统中很多实体都存在着不同程度的关系,这些关系到底是设计为聚合之间的关系还是聚合之内的关系是很难的
2、聚合的判断标准:实体是否是整体和部分的关系,是否存在着相同的生命周期
3、订单与订单明细?用户与订单?
聚合的划分没有标准答案
1、不同的业务流程也就决定了不同的划分方式
2、新闻和新闻的评论?
聚合的划分的原则
1、尽量把聚合设计的小一点,一个聚合只包含一个聚合根实体和密不可分的实体,实体中包含最小数量的属性
2、小聚合有助于进行微服务的拆分
聚合宁愿设计的小一点也不要设计的太大
DDD之领域服务与应用服务
概念
1、聚合中的实体中没有业务逻辑代码,只有对象的创建、对象的初始化、状态管理等个体相关的代码
2、对于聚合内的业务逻辑,我们编写领域服务(Domain Service),而对于跨聚合协作以及聚合与外部系统协作的逻辑,我们编写应用服务(Application Service)
3、应用服务协调多个领域服务、外部系统来完成一个用例
DDD典型用例的处理流程
第一步:准备业务操作所需要的数据 第二步:执行由一个或者多个领域模型做出的业务操作,这些操作会修改实体的状态,或者生成一些操作结果 第三步:把实体的改变或者操作结果应用于外部系统
职责的划分
1、领域模型与外部系统不会发生直接交互,即领域模型不会涉及数据库操作
2、业务逻辑放入领域服务,而与外部系统的交互由应用服务负责
3、领域服务不是必须,在一些简单的业务处理中(比如增删改查)是没有领域知识(也就是业务逻辑)的,这种情况下应用服务可以完成所有操作,不需要引入领域服务。这样可以避免过度设计
“仓储”(Repository)和“工作单元”(Unit Of Work)
1、仓储负责按照要求从数据中读取数据以及把领域服务修改的数据保存回数据库
2、聚合内的数据操作是关系非常紧密的,我们要保证事务的强一致性,而聚合间的协作式关系不紧密的,因此我们只要保证事务的最终一致性即可
3、聚合内的若干相关联的操作组成一个“工作单元”,这些工作单元要么全部成功,要么全部失败
DDD之领域事件、集成事件
事务脚本处理“事件”
1、“当发生某事件的时候,执行某个动作”
2、当有人回复了用户的提问的时候,系统就向提问者的邮箱发送通知邮件。事务脚本的实现:
void 保存答案(long id,string answer)
{
保存到数据库(id,answer);
string email = 获取提问者邮箱(id);
发送邮件(email,"你的问题被回答了");
}
问题1
代码会随着需求的增加而持续膨胀。比如增加功能“如果用户回复的答案中有涉嫌违法的内容,则先把答案隐藏,并且通知审核人员进行审核”。怎么做?
问题2
代码可扩展低。比如把“发送邮件”改成“发送短信”,怎么办? “开闭原则”:对扩展开放,对修改关闭
问题3
容错性差,外部系统并不总是稳定的
采用事件机制的伪代码
void 保存答案(long id,string answer)
{
long aId = 保存到数据库(id,answer);
发布事件("答案已保存",aId,answer);
}
[绑定事件("答案已保存")]
void 审核答案(long aId,string answer)
{
if(检查是否疑似违规(answer))
{
隐藏答案(aId);
发布事件("内容待审核",aId);
}
}
[绑定事件("答案已保存")]
void 发邮件给提问者(long aId,string answer)
{
long qId = 获取问题Id(aId);
string email = 获取提问者邮箱(qId);
发送邮件(email,"你的问题被回答了");
}
优点:关注点分离;容易扩展;容错性好
两种事件
1、DDD中的事件分为两种各类型:领域事件(Domain Events)和集成事件(Integration Events)。 2、领域事件:在同一个微服务内的聚合之间的事件传递。使用进程内的通信机制完成
3、集成事件:跨微服务的事件传递。使用事件总线(EventBus)实现
.NET Core的贫血模型与充血模型
概念
1、贫血模型:一个类中只有属性或者成员变量,没有方法
2、充血模型:一个类中既有属性、成员变量,也有方法 需求:定义一个类保存用户的用户名、密码、积分;用户必须具有用户名;为了保证安全,密码采用密码的散列值保存;用户的初始积分为10分;每次登录成功奖励5个积分,每次登录失败扣3个积分。
贫血模型
class User
{
public string UserName { get; set; }//用户名
public string PasswordHash { get; set; }//密码的散列值
public int Credit { get; set; }//积分
}
User u1 = new User(); u1.UserName = "yzk"; u1.Credit = 10;
u1.PasswordHash = HashHelper.Hash("123456");//计算密码的散列值
string pwd = Console.ReadLine();