前言
设计原则,是指导我们如何设计出低耦合、高内聚的代码,让代码能够更好的应对变化,从而降本提效。
设计原则的关键,是从『使用方的角度』看『提供方』的设计,一句话概括就是:请不要要我知道太多,你可以改,但请不要影响我。
程序设计原则(SOLDI)
单一职责原则(SRP)
定义:一个函数/类只能因为一个理由被修改。
单一职责原则,是所有原则中看起来最容易理解的,但是真正做到并不简单。因为遵循这一原则最关键是职责的划分。
职责的划分至少要回答两个基本问题:
- 什么是你,什么是我?
- 什么事情归你管,什么事情归我管?
且不说写代码,工作中我们也会出现人人不管或相争的重叠地带,划分清楚职责看起容易,实际很难。
开闭原则(OCP)
定义:对扩展开放,对修改关闭(不修改代码就可以增加新功能)。
要理解开闭原则,关键是要理解定义中隐含着的两个主语,"使用方"和"提供方",即:
提供方可以修改 ,增加新的功能特性,但是使用方不需要被修改,即可享用新的功能特征。
开闭原则广泛的理解,可以指导类、模块、系统的设计,满足该原则的核心设计方法是:通过协议(接口)交互。
里氏替换原则(LSP)
定义:所有引用父类的地方,必须能透明的使用它的子类对象,指导类继承的设计。
面向对象的继承特性,一方面,子类可以拥有父类的属性和方法,提高了代码的复用性;另一方面,继承是有入侵性的,父类对子类有约束,子类必须拥有父类全部的属性和方法,修改父类会影响子类,增加了耦合性。
里氏替换原则是对继承进行了约束,体现在以下方面:
- 子类可以实现父类的抽象方法,但不能重写(覆盖)父类的非抽象方法;
- 子类可以增加父类所没有的属性和方法;
- 子类重写父类方法时,输入参数类型要和父类的一致,或更宽松(参数类型的父类);
- 子类重写父类方法时,返回值类型要和父类的一致,或更严谨(返回类型的子类)。
依赖倒置原则(DIP)
定义:高层模块不应该依赖低层模块,两者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象,目的是降低层与层之间的耦合。
从倒置来看,该原则可以有更泛化的理解:
- **依赖实体的倒置:**高层不依赖底层模块,抽象不依赖细节,例如模块分层规范中的domain不依赖infrastructure的实现;
- **依赖控制的倒置:**依赖具体对象的创建控制,从程序内部交给外部,例如Spring的Ioc容器。
举个购物车的例子:
- **商业能力基座:**主要包含购物车的业务流程实现、外域服务定义(非实现)、商业定制能力(扩展点),打包后需满足一套代码多处部署的要求。
- **域服务能力实例:**针对不同运行环境,提供适配环境的域服务实现,商业基座反向依赖域服务实例,使得基座与环境无关。
接口隔离原则(ISP)
定义:客户端不应该被强迫去依赖它并不需要的接口。
理解接口隔离原则,需要拿单一职责的原则做对比。细品一下,如果一个接口满足了
- 单一职责,是否就也就满足接口隔离原则?
- 单一职责原则,解决了接口内聚的问题。
接口隔离原则,认为某些场景下需要存在非内聚接口(多职责),但是又不希望客户端知道整个类,客户端只要知道具有内聚接口的抽象父类即可。
简单来讲,接口隔离原则解决的问题是,当某些类不满足职责单一原则时,客户端不应该直接使用它们,而是通过增加接口类,通过它隐藏客户端不需要感知到的部分。
项目实践
函数设计原则
有时候,优雅的实现仅仅是一个函数,不是一个类,不是一个框架,只是一个函数。------ John Carmack
(1)遵守的设计原则
单一职责。
- **短小:**一个函数不超过50行代码,大量的setXXX()除外。
- **专一:**一个函数只做一件事情,符合单一职责原则。
类设计原则
类是面向对象中最重要的概念,是一组关联数据的相关操作的封装 ,通常可以把类分为两种:
1)实体类: 承载业务的核心数据和业务逻辑,命名要充分体现业务语义,比如Order/Buyer/Item。
2)辅助类: 协调实体类完成业务逻辑,命名通常加后缀体现出其功能性,比如OrderQueryService/OrderRepository。
函数命名的关键点:
1)辅助类尽量避免用 Helper/Util 之类的后缀,因为其含义过于笼统,容易破坏单一职责原则。
2)针对某个实体的辅助操作过多,或单个操作很复杂,可通过 "实体 + 操作类型 + 功能后缀"来命名,同时符合职责单一和接口隔离的原则,比如OrderService:
- OrderCreateService:订单创建服务;
- OrderUpdateService:订单更新服务;
- OrderQueryService:订单查询服务。
接口设计原则
接口分为第三方接口定义和应用内部接口定义。
1.第三方接口定义
(1)遵守的设计原则
在项目开发中,经常需要给第三方提供接口,第三方接口定义需要满足单一职责原则 、开闭原则。
(2)接口定义建议
声明:
- 对于前端而言,后端web层接口属于第三方接口;
- 对于后端而言,后端调用其它服务的接口输液椅第三方接口。
此处,我主要是针对【后端】的第三方接口的讲解。
视角-接口类型(查询类与非查询类):
- 非查询接口:必须是幂等的,请求参数中应有requestId当做幂等id作为全局唯一标识。
-
- 幂等ID由上游提供,保证不同业务不同幂等ID。
- 通常由业务id充当幂等id,出问题时方便上下游通过幂等id找到确定是哪个业务出错。如订单编号、批次号...
- 查询类接口:大数据量查询必须提供分页操作,每页size不超过200条(dba强制:in语句在100条以内)。
视角-接口参数:
- 传参形式:推荐入参传递对象类型,而单个参数一个一个传递。反例:func(String param1, String param2)。原因:对象类型方便后面做扩展。
- 固定传参:如果是非查询类接口,参数中必须存在幂等ID。
- 参数类型:
-
- 浮点数(金额)必须使用BigDecimal,必须使用String构造方法。原因:防止精度丢失。
- 入参、出参不要使用枚举。推荐在这个字段上注释一个枚举类型。
2.内部接口定义
(1)遵守的设计原则
在项目开发中,经常需要给第三方提供接口,第三方接口定义需要满足单一职责原则 、开闭原则。
(2)接口定义建议
主要关注点是如何做 功能扩展。
模块分层原则
模块分层
- client:外部可见层(暴露服务声明);
- service:业务逻辑层,对client层的实现,协调domain和infrastructure一起完成业务逻辑;
- domain:领域层,对应DDD中的领域知识;
- infrastructure:基础设施层,数据库访问、消息、外部调用等;
- start:应用启动层,主要是项目启动时的静态配置。
模块内包分层
分包的建议:
- 如果有多个一级域,建议:一级按业务分包,二级按功能分包,三级可按子领域分包。
- 如果仅一个一级域,建议:一级按功能分包,二级按子领域分包。
例如:
|- xxx
|---- xxx-client // 只提供仅需的外部依赖(DO不能再这层定义)
|----biz1 // 子域module
|----event // 领域事件声明
|----constant // 常量(enum、final)
|----dto // 服务的出参对象,value object/view object
|----request // request or query
|----service // 本地/HSF服务接口声明
|----facade // 提供给Top/MTop的HSF接口
|---- xxx-domain // 提供领域能力(entity、domainservice等)
|----biz1 // 子域module
|----factory // 类工厂(构建器/转换器/工厂类)
|----entity // 充血模型的实体类
|----service // 领域服务
|----repository // api gateway 或 db repository接口,实现放在infrastructure
|--test // 领域层单元测试
|---- xxx-infrastructure // 基础设施层: mapper/config/repository impl
|--main
|----biz1 // 子域module
|----dataobject // do: 贫血模型
|----mapper // mybatis mapper
|----repository // repository impl
|--test // 基础设施层单元测试
|---- xxx-service // 调用外域服务,使用domain层的能力
|----biz1 // 子域module
|----factory // 类工厂(构建器/转换器/工厂类)
|----service // HSF服务接口实现
|----facade // 提供给Top/MTop的HSF接口实现,通常是调service的服务
|----dts
|----metaq
|--test // 服务实现层单元测试
|---- xxx-starter
|--test // 集成测试