【程序设计】一文讲解程序设计原则SOLDI

前言

设计原则,是指导我们如何设计出低耦合、高内聚的代码,让代码能够更好的应对变化,从而降本提效。

设计原则的关键,是从『使用方的角度』看『提供方』的设计,一句话概括就是:请不要要我知道太多,你可以改,但请不要影响我。

程序设计原则(SOLDI)

单一职责原则(SRP)

定义:一个函数/类只能因为一个理由被修改。

单一职责原则,是所有原则中看起来最容易理解的,但是真正做到并不简单。因为遵循这一原则最关键是职责的划分

职责的划分至少要回答两个基本问题:

  • 什么是你,什么是我?
  • 什么事情归你管,什么事情归我管?

且不说写代码,工作中我们也会出现人人不管或相争的重叠地带,划分清楚职责看起容易,实际很难。

开闭原则(OCP)

定义:对扩展开放,对修改关闭(不修改代码就可以增加新功能)。

要理解开闭原则,关键是要理解定义中隐含着的两个主语,"使用方"和"提供方",即:

提供方可以修改 ,增加新的功能特性,但是使用方不需要被修改,即可享用新的功能特征。

开闭原则广泛的理解,可以指导类、模块、系统的设计,满足该原则的核心设计方法是:通过协议(接口)交互。

里氏替换原则(LSP)

定义:所有引用父类的地方,必须能透明的使用它的子类对象,指导类继承的设计。

面向对象的继承特性,一方面,子类可以拥有父类的属性和方法,提高了代码的复用性;另一方面,继承是有入侵性的,父类对子类有约束,子类必须拥有父类全部的属性和方法,修改父类会影响子类,增加了耦合性。

里氏替换原则是对继承进行了约束,体现在以下方面:

  • 子类可以实现父类的抽象方法,但不能重写(覆盖)父类的非抽象方法;
  • 子类可以增加父类所没有的属性和方法;
  • 子类重写父类方法时,输入参数类型要和父类的一致,或更宽松(参数类型的父类);
  • 子类重写父类方法时,返回值类型要和父类的一致,或更严谨(返回类型的子类)。

依赖倒置原则(DIP)

定义:高层模块不应该依赖低层模块,两者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象,目的是降低层与层之间的耦合。

从倒置来看,该原则可以有更泛化的理解:

  • **依赖实体的倒置:**高层不依赖底层模块,抽象不依赖细节,例如模块分层规范中的domain不依赖infrastructure的实现;
  • **依赖控制的倒置:**依赖具体对象的创建控制,从程序内部交给外部,例如Spring的Ioc容器。

举个购物车的例子:

  • **商业能力基座:**主要包含购物车的业务流程实现、外域服务定义(非实现)、商业定制能力(扩展点),打包后需满足一套代码多处部署的要求。
  • **域服务能力实例:**针对不同运行环境,提供适配环境的域服务实现,商业基座反向依赖域服务实例,使得基座与环境无关。

接口隔离原则(ISP)

定义:客户端不应该被强迫去依赖它并不需要的接口。

理解接口隔离原则,需要拿单一职责的原则做对比。细品一下,如果一个接口满足了

  • 单一职责,是否就也就满足接口隔离原则?
  • 单一职责原则,解决了接口内聚的问题。

接口隔离原则,认为某些场景下需要存在非内聚接口(多职责),但是又不希望客户端知道整个类,客户端只要知道具有内聚接口的抽象父类即可。

简单来讲,接口隔离原则解决的问题是,当某些类不满足职责单一原则时,客户端不应该直接使用它们,而是通过增加接口类,通过它隐藏客户端不需要感知到的部分。

项目实践

函数设计原则

有时候,优雅的实现仅仅是一个函数,不是一个类,不是一个框架,只是一个函数。------ John Carmack

(1)遵守的设计原则

单一职责。

  1. **短小:**一个函数不超过50行代码,大量的setXXX()除外。
  2. **专一:**一个函数只做一件事情,符合单一职责原则。

类设计原则

类是面向对象中最重要的概念,是一组关联数据的相关操作的封装 ,通常可以把类分为两种:

1)实体类: 承载业务的核心数据和业务逻辑,命名要充分体现业务语义,比如Order/Buyer/Item。

2)辅助类: 协调实体类完成业务逻辑,命名通常加后缀体现出其功能性,比如OrderQueryService/OrderRepository。

函数命名的关键点:

1)辅助类尽量避免用 Helper/Util 之类的后缀,因为其含义过于笼统,容易破坏单一职责原则。

2)针对某个实体的辅助操作过多,或单个操作很复杂,可通过 "实体 + 操作类型 + 功能后缀"来命名,同时符合职责单一和接口隔离的原则,比如OrderService:

  • OrderCreateService:订单创建服务;
  • OrderUpdateService:订单更新服务;
  • OrderQueryService:订单查询服务。

接口设计原则

接口分为第三方接口定义和应用内部接口定义。

1.第三方接口定义

(1)遵守的设计原则

在项目开发中,经常需要给第三方提供接口,第三方接口定义需要满足单一职责原则开闭原则

(2)接口定义建议

声明:

  1. 对于前端而言,后端web层接口属于第三方接口;
  2. 对于后端而言,后端调用其它服务的接口输液椅第三方接口。

此处,我主要是针对【后端】的第三方接口的讲解。

视角-接口类型(查询类与非查询类):

  1. 非查询接口:必须是幂等的,请求参数中应有requestId当做幂等id作为全局唯一标识。
    1. 幂等ID由上游提供,保证不同业务不同幂等ID。
    2. 通常由业务id充当幂等id,出问题时方便上下游通过幂等id找到确定是哪个业务出错。如订单编号、批次号...
  1. 查询类接口:大数据量查询必须提供分页操作,每页size不超过200条(dba强制:in语句在100条以内)。

视角-接口参数:

  1. 传参形式:推荐入参传递对象类型,而单个参数一个一个传递。反例:func(String param1, String param2)。原因:对象类型方便后面做扩展。
  2. 固定传参:如果是非查询类接口,参数中必须存在幂等ID。
  3. 参数类型:
    1. 浮点数(金额)必须使用BigDecimal,必须使用String构造方法。原因:防止精度丢失。
    2. 入参、出参不要使用枚举。推荐在这个字段上注释一个枚举类型

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            // 集成测试