设计模式之美
2021-10-27 20:44:36 42 举报
AI智能生成
对极客时间的设计模式之美进行个人思考笔记
作者其他创作
大纲/内容
从哪些维度评判代码
1. 可维护性
2. 可读性
3. 可扩展性
4. 灵活性
复用性高
扩展性高
接口易用
5. 简洁性
6. 可复用性
减少重复代码的编写
继承、多态
7. 可测试性
面向对象
7个知识点
面向对象的四大特性:封装、抽象、继承、多态
面向对象变成与面向过程变成的区别和联系
面向对象分析、面向对象设计、面向对象变成
接口和抽象类的区别以及各自的应用场景
基于接口而非实现编程的设计思想
多用组合少用继承的设计思想
面向过程的贫血模式和面向对象的充血模式
设计原则
SOLID 原则
SRP 单一职责原则
OCP 开闭原则
LSP 里式替换原则
ISP 接口隔离原则
DIP 依赖导致原则
三大类
创建型
常用:单例模式、工厂模式、建造者模式
不常用:原型模式
结构型
常用:代理模式、桥接模式、装饰者模式、适配器模式
不常用:门面模式、组合模式、享元模式
行为型
常用:观察者模式、模板模式、策略模式、职责链模式、迭代器模式、状态模式
不常用:访问者模式、备忘录模式、命令模式、解释器模式、中介模式
代码重构
三个维度
重构的目的(WHY)、对象(WHAT)、时机(WHEN)、方法(HOW)
保证重构不出错的技术手段:单元测试和代码的可测试性
两种不同规模的重构:大重构(大规模高层次)和小重构(小规模低层次)
面向对象
理论
封装、抽象
封装(Encapsulation)
信息隐藏和数据访问保护
抽象(Abstractin)
隐藏方法的具体实现,让调用者只需要关心方法提供了那些功能,并不需要知道这些功能是如何是实现的
不实现接口,也能满足抽象的特性,通过函数包括具体的实现逻辑,也是一种抽象
继承(Inheritance)
标识类之间的is-a的关系,比如猫(A)是一种哺乳动物(B)
多态(Polymorphism)
子类可以替换父类
重载、重写
提高扩展性、复用性
面向对象比面向过程的优势
面向对象
编程范式和编程风格
以类或对象作为组织的基本单元
将封装、抽象、继承、多态作为代码设计和实现的基石
支持类或对象的语法机制
面向过程
编程范式和编程风格
以过程(方法、函数、操作)为组织代码的基本单元
以数据(成员变量、属性)与方法相分离为最主要特点
流程化的编码风格,通过拼接一组顺序执行的方法来操作数据完成一项功能
缺点
不支持类和对象的概念
不支持丰富的面向对象变成特性(封装、继承、多态)
面向对象的优势
1.OOP更加能够应对大规模复杂程序的开发
2. OOP风格的代码更易复用、易扩展、易维护
3. OOP 语言更加人性化、更加高级、更加智能
哪些代码看似面向对象,实际是面向过程
1. 滥用getter、setter方法
2. Constants类,Utils类的设计问题
尽量职责单一
3. 基于贫血模式的开发模式
接口vs抽象类的区别
抽象类
特性
抽象类不允许被实例化,只能被继承
抽象类可以包含属性和方法
子类继承抽象类,必须实现抽象类中的所有方法
接口
特性
接口不能包含属性
接口只能声明方法,方法不能包含代码实现
类实现接口,必须声明接口的所有方法
基于接口而非实现编程
注意点
1. 函数的命名不能暴露任何细节
2. 封装具体的实现细节
3. 为实现类定义抽象的接口
接口的定义只表名做什么,而不是怎么做
多用组合少用继承
继承
继承层次过深、继承关系过于复杂会影响到代码的可读性和可维护性
三个作用 is-a关系,支持多态特性,代码复用
组合
组合和接口 has-a关系,多态特性利用接口,代码复用通过组合和委托实现
如何选择
继承结构稳定,层次浅,关系不复杂,使用继承
结构不稳定,层次深,关系复杂,使用组合
固定的设计模式会使用组合或者继承
实战
贫血模型
Service 层的数据和业务逻辑,被分割为BO和Service两个类。而Bo 只包含数据,不包含业务逻辑。同理Entity、Vo都是基于贫血模型
将数据与操作分离,破坏了面向对象的封装特性,典型的面向过程的风格
重Service 轻Bo
简单的系统开发
充血模型
数据和对应的业务逻辑被封装到同一个类中
Service层包含Service类和Domain类
轻Service 重Domain
复杂的系统开发。比如各种利息计算模型、还款模型等复杂的金融系统
领域驱动设计
指导如何解耦业务系统,划分业务模块,定义业务领域模型与其交互
需求转化具体的类的设计
划分职责进而识别出有哪些类
定义类及其属性和方法
定义类与类之间的交互关系
将类组装起来并提供执行入口
设计原则
理论
单一职责
一个类或者模块只负责完成一个职责(或者功能)
判定单一职责的原则
类中的代码行数、函数或属性过多
类依赖的其他类过多,或者依赖类的其他类过多
私有方法过多,我们就要考虑能否将私有方法独立到新的类中
比较难给类起一个合适名字,很难用一个业务名词概括,或者只能用一些笼统的 Manager、Context 之类的词语来命名,这就说明类的职责定义得可能不够清晰
类中大量的方法都是集中操作类中的某几个属性
对扩展开放、修改关闭
定义
开闭原则并不是说完全杜绝修改,而是以最小的修改代码的代价来完成新功能的开发
同样的代码改动,在粗代码粒度下,可能被认定为“修改”;在细代码粒度下,可能又被认定为“扩展”
如何做到
最常用来提高代码扩展性的方法有:多态、依赖注入、基于接口而非实现编程,以及大部分的设计模式(比如,装饰、策略、模板、职责链、状态
里式替换(LSP)
定义
子类对象(object of subtype/derived class)能够替换程序(program)中父类对象(object of base/parent class)出现的任何地方,并且保证原来程序的逻辑行为(behavior)不变及正确性不被破坏。
父类定义了函数的“约定”(或者叫协议),那子类可以改变函数的内部实现逻辑,但不能改变函数原有的“约定”。这里的约定包括:函数声明要实现的功能;对输入、输出、异常的约定;甚至包括注释中所罗列的任何特殊说明。
违反原则
1. 子类违背父类声明要实现的功能
2. 子类违背父类对输入、输出、异常的约定
3. 子类违背父类注释中所罗列的任何特殊说明
接口隔离原则
理解
如果把“接口”理解为一组接口集合,可以是某个微服务的接口,也可以是某个类库的接口等。如果部分接口只被部分调用者使用,我们就需要将这部分接口隔离出来,单独给这部分调用者使用,而不强迫其他调用者也依赖这部分不会被用到的接口。
如果把“接口”理解为单个 API 接口或函数,部分调用者只需要函数中的部分功能,那我们就需要把函数拆分成粒度更细的多个函数,让调用者只依赖它需要的那个细粒度函数。
如果把“接口”理解为 OOP 中的接口,也可以理解为面向对象编程语言中的接口语法。那接口的设计要尽量单一,不要让接口的实现类和调用者,依赖不需要的接口函数。
控制反转、依赖反转、依赖注入
控制反转
“控制”指的是对程序执行流程的控制,而“反转”指的是在没有使用框架之前,程序员自己控制整个程序的执行
依赖注入
我们不通过 new 的方式在类内部创建依赖类的对象,而是将依赖的类对象在外部创建好之后,通过构造函数、函数参数等方式传递(或注入)给类来使用
依赖反转
依赖反转原则也叫作依赖倒置原则。程序要依赖于抽象接口,不要依赖于具体实现。简单的说就是要求对抽象进行编程,不要对实现进行编程。
KISS 原则和 YAGNI 原则
KISS 原则
不要使用同事可能不懂的技术来实现代码;
不要重复造轮子,要善于使用已经有的工具类库;
不要过度优化。
YAGNI 原则
不要做过度设计
DRY 原则
Don’t Repeat Yourself
提高复用性的手段
减少代码耦合
满足单一职责原则
模块化
业务与非业务逻辑分离
通用代码下沉
继承、多态、抽象、封装
应用模板等设计模式
迪米特法则(LOD)
定义
高内聚,就是指相近的功能应该放到同一个类中,不相近的功能不要放到同一个类中。
松耦合,在代码中,类与类之间的依赖关系简单清晰。即使两个类有依赖关系,一个类的代码改动不会或者很少导致依赖类的代码改动。
迪米特法则
Each unit should have only limited knowledge about other units: only units “closely” related to the current unit.
Or: Each unit should only talk to its friends; Don’t talk to strangers.
Or: Each unit should only talk to its friends; Don’t talk to strangers.
不该有直接依赖关系的类之间,不要有依赖;有依赖关系的类之间,尽量只依赖必要的接口(也就是定义中的“有限知识”)。
实战
系统设计
1. 合理地将功能划分到不同模块
2. 设计模块与模块之间的交互关系
3. 设计模块的接口、数据库、业务模型
为什么要分 MVC 三层开发?
1. 分层能起到代码复用的作用
2. 分层能起到隔离变化的作用
3. 分层能起到隔离关注点的作用
4. 分层能提高代码的可测试性
5. 分层能应对系统的复杂性
BO、VO、Entity
BO、VO、Entity 存在的意义是什么?
VO、BO、Entity 并非完全一样
VO、BO、Entity 三个类虽然代码重复,但功能语义不重复,从职责上讲是不一样
为了尽量减少每层之间的耦合,把职责边界划分明确,每层都会维护自己的数据对象,层与层之间通过接口交互。
VO、BO、Entity 不能合并,那如何解决代码重复的问题呢?
继承可以解决代码重复问题
通过组合关系来复用这个类的代码。
非业务框架需求分析
1. 功能性需求分析
接口统计信息:包括接口响应时间的统计信息,以及接口调用次数的统计信息等。
统计信息的类型:max、min、avg、percentile、count、tps 等。
统计信息显示格式:Json、Html、自定义显示格式。
统计信息显示终端:Console、Email、HTTP 网页、日志、自定义显示终端。
2. 非功能性需求分析
易用性
性能
扩展性
容错性
通用性
规范与重构
理论
重构
可以把重构理解为,在保持功能不变的前提下,利用设计思想、原则、模式、编程规范等理论来优化代码,修改设计上的不足,提高代码质量。
重构是避免过度设计的有效手段
规模
大型重构指的是对顶层代码设计的重构,包括:系统、模块、代码结构、类与类之间的关系等的重构,重构的手段有:分层、模块化、解耦、抽象可复用组件等等
这类重构的工具就是我们学习过的那些设计思想、原则和模式。这类重构涉及的代码改动会比较多,影响面会比较大,所以难度也较大,耗时会比较长,引入 bug 的风险也会相对比较大。
小型重构指的是对代码细节的重构,主要是针对类、函数、变量等代码级别的重构,比如规范命名、规范注释、消除超大类或函数、提取重复代码等等
小型重构更多的是利用我们能后面要讲到的编码规范。这类重构要修改的地方比较集中,比较简单,可操作性较强,耗时会比较短,引入 bug 的风险相对来说也会比较小。
建立持续重构意识
单元测试
最可落地执行、最有效的保证重构不出错的手段应该就是单元测试(Unit Testing)
区别
集成测试的测试对象是整个系统或者某个功能模块,比如测试用户注册、登录功能是否正常,是一种端到端(end to end)的测试
单元测试的测试对象是类或者函数,用来测试一个类和函数是否都按照预期的逻辑执行。这是代码层级的测试。
好处
1. 单元测试能有效地帮你发现代码中的 bug
2. 写单元测试能帮你发现代码设计上的问题
3. 单元测试是对集成测试的有力补充
4. 写单元测试的过程本身就是代码重构的过程
5. 阅读单元测试能帮助你快速熟悉代码
6. 单元测试是 TDD 可落地执行的改进方案
测试驱动开发(Test-Driven Development,简称 TDD)是一个经常被提及但很少被执行的开发模式
代码的可测试性
典型的、常见的测试性不好的代码
1. 未决行为
2. 全局变量
3. 静态方法
4. 复杂继承
5. 高耦合代码
解耦
目的
实现代码高内聚、松耦合
方法
1. 封装与抽象
2. 中间层
3. 模块化
4. 其他设计思想和原则
单一职责原则
基于接口而非实现编程
依赖注入
多用组合少用继承
迪米特法则
快速地改善代码质量
命名与注释(Naming and Comments)
命名
利用上下文简化命名
举例说当前类叫Pig,那属性就可以name ,而不是pigName
命名要可读、可搜索
注释
注释比代码承载的信息更多
注释起到总结性作用、文档的作用
一些总结性注释能让代码结构更清晰
代码风格(Code Style)
编程技巧(Coding Tips)
1. 把代码分割成更小的单元块
2. 避免函数参数过多
3. 勿用函数参数来控制逻辑
4. 函数设计要职责单一
5. 移除过深的嵌套层次
6. 学会使用解释性变量
实战
代码质量
常规检查项
目录设置是否合理、模块划分是否清晰、代码结构是否满足“高内聚、松耦合”?
是否遵循经典的设计原则和设计思想(SOLID、DRY、KISS、YAGNI、LOD 等)?
设计模式是否应用得当?是否有过度设计?
代码是否容易扩展?如果要添加新功能,是否容易实现?
代码是否可以复用?是否可以复用已有的项目代码或类库?是否有重复造轮子?
代码是否容易测试?单元测试是否全面覆盖了各种正常和异常的情况?
代码是否易读?是否符合编码规范(比如命名和注释是否恰当、代码风格是否一致等)?
功能和非功能需求
代码是否实现了预期的业务需求?
逻辑是否正确?是否处理了各种异常情况?
日志打印是否得当?是否方便 debug 排查问题?
接口是否易用?是否支持幂等、事务等?
代码是否存在并发问题?是否线程安全?
性能是否有优化空间,比如,SQL、算法是否可以优化?
是否有安全漏洞?比如输入输出校验是否全面?
函数出错
1. 返回错误码
2. 返回 NULL 值
3. 返回空对象
4. 抛出异常对象
0 条评论
下一页