设计模式
2024-06-28 01:16:51 0 举报
AI智能生成
设计模式
作者其他创作
大纲/内容
概念:一个类只负责完成一个职责或者功能
提高类的内聚性
实现代码的高内聚、松耦合
意义
1.类中的代码行数、函数或者属性过多
2.类依赖的其他类过多或者依赖类的其他类过多
3.私有方法过多
4.比较难给类起一个合适的名字
5.类中大量的方法都是集中操作类中的某几个属性
不满足的五种情况
SOLID原则:SRP单一职责原则
开闭原则并不是说完全杜绝修改,而是以最小的修改代码的代价来完成新功能的开发
比如,添加属性和方法相当于修改类,在类这个层面,这个代码改动可以被认定为“修改”;但这个代码改动并没有修改已有的属性和方法,在方法(及其属性)这一层面,它又可以被认定为“扩展”。
同样的代码变动,在粗代码粒度下,可能被认定为“修改”;在细代码粒度下,可能又被认定为“扩展”
如何理解?
时刻具备扩展意识,抽象意识,封装意识
多态
依赖注入
基于接口而非实现编程
大部分设计模式:装饰、策略、模板、职责链、状态
常用来提高代码扩展性的方法
如何做到?
SOLID原则:OCP开闭原则
概念:子类对象能够替换程序中父类对象出现的任何地方,并且保证原来程序的逻辑行为不变及正确性不被破坏
核心:“design by contract,按照协议来设计”。父类定义了函数的“约定(或协议)”,子类可以改变函数的内部实现逻辑,单不能改变函数的原有的“约定”
多态是面向对象编程的一大特性,是面向对象编程语言的一种语法,是一个代码实现思路
里氏替换是一个设计原则,用来指导继承关系中子类该如何设计,子类的设计要保证在替换父类的时候,不改变原有程序的逻辑及不破坏原有程序的正确性
里式替换原则VS多态
SOLID原则:LSP里式替换原则
概念:客户端不应该强迫依赖它不需要的接口。其中的“客户端”,可以理解为接口的调用者或者使用者
1.把“接口”理解为一组接口集合
2.把“接口”理解为单个API接口或函数
3.把“接口”理解为OOP中的接口
核心:“接口”的三种不同的理解
单一职责原则针对的是模块、类、接口的设计
接口隔离原则提供了一种判断接口的职责是否单一的标准;通过调用者如何使用接口来间接地判定
单一职责原则VS接口隔离原则
SOLID原则:LSP接口隔离原则
控制反转:一个比较笼统的设计思想,并不是一种具体的实现方式
依赖注入:和控制反转恰恰相反,是一种具体的编码技巧
依赖注入框架:通过依赖注入框架提供的扩展点,简单配置一下所有需要的类及其类与类之间依赖关系,就可以实现由框架来自动创建对象、管理对象的生命周期、依赖注入等原本需要程序猿来做的事情
依赖反转原则:也叫做依赖倒置原则,跟控制反转有点类似,主要用来指导框架层面的设计
SOLID原则:DIP依赖倒置原则
概念:尽量保持简单
意义:保持代码可读和可维护的重要手段
不要使用同事可能不懂的技术来实现代码
不要重复造轮子,善于使用已经有的工具类库
不要过度优化
如何写出满足KISS原则的代码
KISS原则
概念:不要去设计当前用不到的功能,不要去编写当前用不到的代码
核心:不要做过度设计
YANGI原则
KISS原则:“如何做”,尽量保持简单
YAGNI原则:“要不要做”,当前不需要的就不要做
KISS VS YAGNI
KISS、YAGNI原则
概念:不要写重复的代码
实现逻辑重复
功能语义重复
代码执行重复
三种代码重复的情况
减少代码耦合
满足单一职责原则
模块化
业务与非业务逻辑分离
通用代码下沉
继承、多态、抽象、封装
应用模板等设计模式
有复用意识
提高代码复用性的一些手段
DRY原则
高内聚:指导类本身的设计。松耦合:指导雷玉磊之间依赖关系的设计
高内聚:相近的功能应该放到同一个类中,不相近的功能不要放到同一个类中
松耦合:即使两个类有依赖关系,一个类的代码改动也不会或者很少导致依赖类的代码改动
高内聚、松耦合
概念:不该有直接依赖关系的类之间,不要有依赖;有依赖关系的类之间,尽量只依赖必要的接口
意义:减少类之间的耦合,让类越独立越好
迪米特法则
LOD原则
三、设计原则
保持代码质量持续处于一个可控状态,不至于腐化到无可救药地步
为什么要重构?
内容:对代码分层、模块化、解耦、梳理类之间的交互关系、抽象复用组件
理论基础:设计思想、原则、模式
大规模高层次的重构
内容:规范命名、注释、修正函数参数过多、消除超大类、提取重复代码等编程细节问题,主要是针对类、函数级别的重构
理论基础:编码规范
小规模低层次的重构
重构什么?
建立持续重构意识,把重构作为开发必不可少的部分融入到开发中
什么时候重构?
难度比较大,需要组织、有计划地进行,分阶段地小步快跑
影响范围小,改动耗时短,随时随地都可以做
如何重构?
重构概述
概念:代码层面的测试,用于测试“自己”编写的代码的逻辑的正确性
“单元”:一般是类或函数,而不是模块或者系统
什么是单元测试?
能有效地发现代码中的Bug,代码设计上的问题
写单元测试的过程本身就是代码重构的过程
单元测试是对集成测试的有力补充,有利于快速熟悉代码,是TDD(测试驱动开发)可落地执行的折中方案
为什么要写单元测试?
概念:针对代码设计覆盖各种输入、异常、边界条件的测试用例,并将其翻译成代码的过程
方法:可以利用一些测试框架来简化测试代码的编写
1.编写单元测试尽管繁琐,但并不是太耗时
2.可以稍微放低单元测试的质量要求
3.覆盖率作为衡量单元测试好坏的唯一标准是不合理的
4.写单元测试一般不需要了解代码的实现逻辑
5.单元测试框架无法测试多半是代码的可测试性不好
五个正确的编写认知
如何编写单元测试?
写单元测试本身比较模糊,技术挑战不大,很多程序猿不愿意去写
国内研发比较偏向快糙猛,容易因为开发进度紧,导致单元测试的执行虎头蛇尾
没有建立对单元测试正确的认识,觉得可有可无,单靠督促很难执行得很好
单元测试为何难落地执行?
单元测试
真滴代码编写单元测试的难易程度
什么是代码的可测试性
依赖注入可以通过mock的方法将不可控的依赖变得可控
除了mock方式,还可以利用二次封装来解决某些代码行为不可控的情况
依赖注入是编写可测试性代码的最有效手段
1.代码中包含未决行为逻辑
2.滥用可变全局变量
3.滥用静态方法
4.使用复杂的继承关系
5.高度耦合的代码
常见的五种Anti-Patterns(反面模式指的是在实践中明显出现但又低效或是有待优化的设计模式)
代码的可测试性
保证代码松耦合、高内聚,是控制代码复杂度的有效手段
代码高内聚、松耦合,也就是代码结构清晰、分层、模块化合理、依赖关系简单、模块或类之间的耦合小,代码整体的质量就不会差
“解耦”为何如此重要?
改动一个模块或类的代码受影响的模块或类的是否有很多
改动一个模块或者类的代码,依赖的模块或者类是否需要改动
代码的可测试性是否好
间接的衡量标准
把模块与模块之间及其类与类之间的依赖关系画出来。根据依赖关系图的复杂性来判断
直接的衡量标准
代码是否需要“解耦”?
封装与抽象
中间层
单一职责原则
多用组合少用继承
其他设计思想与原则
设计模式
如何给代码“解耦”?
大型重构:解耦
1.命名的关键是能精准达意
2.借助类的信息来简化属性、函数的命名、利用函数的信息来简化函数参数的命名
3.命名要可读、可搜索,不要使用生僻的、不好读的英文单词
4.接口的两种命名方式:在接口中带前缀“I”;在接口的实现类中带后缀“impl”抽象类的两种命名方式:带上前缀“Abstract”;不带前缀
5.注释的内容:做什么,为什么,怎么做。复杂的类和接口,还要写明“如何用”
6.类和函数一定要写注释,而且要写得尽可能全面详细
命名与注释
7.函数的代码行数不要超过一屏幕的大小,比如50行
8.一行代码最好不要超过IDE的显示宽度
9.善用空行分割单元块
10.推荐两个缩进,节省空间。一定不要用tab建缩进
11.将大括号跟上一条语句同一行,可以节省代码行数。另起新的一行,结构清晰
12.在Google Java编程规范中,依赖类按照字母序从小到大排序。类中先写成员变量后写函数。成员变量之间或函数之间,先写静态成员变量或函数,后写普通变量或函数,并且按照作用域大小一次排序
代码风格
13.将复杂的逻辑提炼拆分成函数和块
14.通过拆分成多个函数或将参数封装为对象的方式,来处理参数过多
15.函数中不要使用参数来做代码执行逻辑的控制
16.函数设计要职责单一
17.移除过深的嵌套层次
18.用字面常亮取代魔法数
19.用解释性变量来解释复杂表达式
编程技巧
20,团队或项目要知行统一的编码规范,code review督促执行
小型重构:编码规范(20条)
四、规范与重构
总结
标准多,带有主观性,无法用单一标准来评判,需要综合各个维度
如何评价代码质量的高低?
可维护性
可读性
可扩展性
最常用
灵活性
简洁性
可复用性
可测试性
其它
最常用的评价标准有哪几个?
设计思想
设计原则
编码规范
重构技巧
如何才能写出高质量的代码?
一、代码质量评判标准
面向过程
面向对象
函数式编程
3中主流变成范式(风格)
最主流:流行的编程语言大部分是面向对象编程语言
特性丰富:可以实现很多复杂的设计思路,是很多设计原则、设计模式的基础
面向对象概述
概念:也叫做信息隐藏或者数据访问保护
特点:需要编程语言提供权限访问控制语法来支持,例如JAva中的private、protected、public关键字
保护数据不被随意修改,提供代码的可维护性
仅暴露有限的必要接口,提高类的易用性
封装
概念:隐藏方法的具体实现
修改实现,不需要改变定义
处理复杂系统的有效手段,能有效过滤不必要关注的信息
抽象
概念:表示类之间的is-a关系
特性:编程语言需要提供特殊的语法机制来支持
单继承:一个子类只继承一个父类
多继承:一个子类可以继承多个父类
两种模式
意义:解决胆码复用的问题
继承
概念:子类可以替换父类,在实际的代码运行过程中,调用子类的方法实现
特性:编程语言需要提供特殊的语法机制来支持,比如继承、接口类
意义:提高代码的扩展性和复用性,是很多设计模式、设计原则、编程技巧的代码实现基础
面向对象四大特性
更能应对这种复杂类型的程序开发
具有更加丰富的特性(封装,抽象,继承,多态)
更加人性化、更加高级、更加智能
面对对象VS面向过程
面向对象分析:搞清楚做什么,产出详细的需求描述
划分职责进而识别出有哪些类
定义类及其属性和方法
定义类与类之间的交互关系
将类组装起来并提供执行入口
面向对象设计:搞清楚怎么做,将需求描述转化为具体的类
面向对象编程:将分析和实际的结果翻译成代码
面向对象分析、设计与编程
is-a( 是 \"a\" 小明是人类)表示的是属于得关系。比如兔子属于一种动物(继承关系)
如果要表示一种is-a的关系,并且是为了解决代码复用问题,我们就用抽象类
has-a( 有 \"a\" 汽车有轮胎) 表示组合,包含关系。比如兔子包含有腿,头等组件;就不能说兔子腿是属于一种兔子(不能说是继承关系)
如果要表示一种has-a的关系,并且是未了解决抽象而非代码复用问题,按我们就用接口
接口VS抽象类
将接口和实现相分离,封装不稳定的实现,暴露稳定的接口
即“基于抽象而非实现编程”。抽象是提高代码扩展性、灵活性、可维护性最有效的手段之一
继承层次过深、过复杂,会影响到代码的可维护性
为什么不推荐使用继承
表示is-a关系
支持多态特性
代码复用
继承的三个作用
组合能解决层次过深、过复杂的继承关系影响代码可维护性的问题
组合相比继承有哪些优势?
类之间的继承结构稳定,层次比较浅,关系不复杂,我们使用继承。反之,用组合
鼓励多用组合少用继承,单组合也并不是完美的,继承也并非一无是处
如何判断该用组合还是继承
适合业务不复杂的系统开发
基于贫血模式的传统开发模式:面向过程
适合业务复杂的系统开发
基于充血模式的DDD开发模式:面向对象
两者主要区别在Serivce层,Controller层和Repository层的代码基本上相同
贫血模式VS充血模式
二、面向对象
单例模式用来创建全局唯一的对象。一个类只允许创建一个对象(或者叫实例),那这个类就是一个单例类,这种设计模式就叫作单例模式。
单例有几种经典的实现方式,它们分别是:饿汉式、懒汉式、双重检测、静态内部类、枚举。
单例对 OOP 特性的支持不友好
单例会隐藏类之间的依赖关系
单例对代码的扩展性不友好
单例对代码的可测试性不友好
单例不支持有参数的构造函数
有些人认为单例是一种反模式(anti-pattern),并不推荐使用,主要的理由有以下几点:
替代单例的解决方案,比如,通过工厂模式、IOC 容器来保证全局唯一性。
单例模式
工厂模式包括简单工厂、工厂方法、抽象工厂这 3 种细分模式
如果创建对象的逻辑并不复杂,那直接通过 new 来创建对象就可以了,不需要使用工厂模式
当创建逻辑比较复杂,是一个“大工程”的时候,就考虑使用工厂模式,封装对象的创建过程,将对象的创建和使用相分离。
工厂模式用来创建不同但是相关类型的对象(继承同一父类或者接口的一组子类),由给定的参数来决定创建哪种类型的对象。
当每个对象的创建逻辑都比较简单的时候,推荐使用简单工厂模式,将多个对象的创建逻辑放到一个工厂类中。
当每个对象的创建逻辑都比较复杂的时候,为了避免设计一个过于庞大的工厂类,推荐使用工厂方法模式,将创建逻辑拆分得更细,每个对象的创建逻辑独立到各自的工厂类中。
简单工厂模式和工厂方法模式
封装变化:创建逻辑有可能变化,封装成工厂类之后,创建逻辑的变更对调用者透明。
代码复用:创建代码抽离到独立的工厂类之后可以复用。
隔离复杂性:封装复杂的创建逻辑,调用者无需了解如何创建对象。
控制复杂度:将创建代码抽离出来,让原本的函数或类职责更单一,代码更简洁。
作用
工厂模式
用来创建复杂对象,可以通过设置不同的可选参数,“定制化”地创建不同的对象。
如果一个类中有很多属性,为了避免构造函数的参数列表过长,影响代码的可读性和易用性,我们可以通过构造函数配合 set() 方法来解决。
把类的必填属性放到构造函数中,强制创建对象的时候就设置。如果必填的属性有很多,把这些必填属性都放到构造函数中设置,那构造函数就又会出现参数列表很长的问题。如果把必填属性通过 set() 方法设置,那校验这些必填属性是否已经填写的逻辑就无处安放了。
如果类的属性之间有一定的依赖关系或者约束条件,继续使用构造函数配合 set() 方法的设计思路,那这些依赖关系或约束条件的校验逻辑就无处安放了。
如果希望创建不可变对象,也就是说,对象在创建好之后,就不能再修改内部的属性值,要实现这个功能,就不能在类中暴露 set() 方法。构造函数配合 set() 方法来设置属性值的方式就不适用了。
使用建造者模式的情况
建造者模式
如果对象的创建成本比较大,而同一个类的不同对象之间差别不大(大部分字段都相同),在这种情况下,利用对已有对象(原型)进行复制(或者叫拷贝)的方式,来创建新对象,以达到节省创建时间的目的。
原型模式
创建型设计模式主要解决“对象的创建”问题
代理模式在不改变原始类接口的条件下,为原始类定义一个代理类。一般情况下,让代理类和原始类实现同样的接口。但是,如果原始类并没有定义接口,并且原始类代码并不是我们开发维护的。在这种情况下,我们可以通过让代理类继承原始类的方法来实现代理模式。
主要目的是控制访问,而非加强功能,这是它跟装饰器模式最大的不同
静态代理需要针对每个类都创建一个代理类,并且每个代理类中的代码都有点像模板式的“重复”代码,增加了维护成本和开发成本。
对于静态代理存在的问题,我们可以通过动态代理来解决。我们不事先为每个原始类编写代理类,而是在运行的时候动态地创建原始类对应的代理类,然后在系统中用代理类替换掉原始类。
代理模式
等同于“组合优于继承”设计原则
桥接模式
主要解决继承关系过于复杂的问题,通过组合来替代继承,给原始类添加增强功能。
装饰器模式还有一个特点,那就是可以对原始类嵌套使用多个装饰器。为了满足这样的需求,在设计的时候,装饰器类需要跟原始类继承相同的抽象类或者接口。
装饰器模式
代理模式、装饰器模式提供的都是跟原始类相同的接口,而适配器提供跟原始类不同的接口。
适配器模式是用来做适配的,它将不兼容的接口转换为可兼容的接口,让原本由于接口不兼容而不能一起工作的类可以一起工作。
类适配器使用继承关系来实现
对象适配器使用组合关系来实现
适配器模式有两种实现方式:类适配器和对象适配器。
封装有缺陷的接口设计
统一多个类的接口设计
替换依赖的外部系统
兼容老版本接口
适配不同格式的数据
使用场景
适配器模式
通过封装细粒度的接口,提供组合各个细粒度接口的高层次接口,来提高接口的易用性,或者解决性能、分布式事务等问题。
门面模式
主要是用来处理树形结构数据
组合模式
意图是复用对象,节省内存
前提是享元对象是不可变对象
具当一个系统中存在大量重复对象的时候,就可以利用享元模式,将对象设计成享元,在内存中只保留一份实例,供多处代码引用,这样可以减少内存中对象的数量,以起到节省内存的目的。实际上,不仅仅相同对象可以设计成享元,对于相似对象,也可以将这些对象中相同的部分(字段),提取出来设计成享元,让这些大量相似对象引用这些享元。
享元模式
结构型设计模式主要解决“类或对象的组合”问题
将观察者和被观察者代码解耦
主要是为了代码解耦
同步阻塞的实现方式
除了能实现代码解耦之外,还能提高代码的执行效率
异步非阻塞的实现方式
进程间的观察者模式解耦更加彻底,一般是基于消息队列来实现,用来实现不同进程间的被观察者和观察者之间的交互
进程内的实现方式
跨进程的实现方式
实现方式
观察者模式
在一个方法中定义一个算法骨架,并将某些步骤推迟到子类中实现。模板方法模式可以让子类在不改变算法整体结构的情况下,重新定义算法中的某些步骤。这里的“算法”,我们可以理解为广义上的“业务逻辑”,并不特指数据结构和算法中的“算法”。这里的算法骨架就是“模板”,包含算法骨架的方法就是“模板方法”,这也是模板方法模式名字的由来。
所有的子类可以复用父类中提供的模板方法的代码
复用
框架通过模板模式提供功能扩展点,让框架用户可以在不修改框架源码的情况下,基于扩展点定制化框架的功能。
扩展
同步回调看起来更像模板模式
同步回调
异步回调看起来更像观察者模式
异步回调
回调
更多的是在代码实现上,而非应用场景上。
回调基于组合关系来实现,模板模式基于继承关系来实现。回调比模板模式更加灵活。
回调跟模板模式的区别
模板方法
策略模式定义一族算法类,将每个算法分别封装起来,让它们可以互相替换。策略模式可以使算法的变化独立于使用它们的客户端(这里的客户端代指使用算法的代码)。策略模式用来解耦策略的定义、创建、使用。实际上,一个完整的策略模式就是由这三个部分组成的。
策略模式
在职责链模式中,多个处理器依次处理同一个请求。一个请求先经过 A 处理器处理,然后再把请求传递给 B 处理器,B 处理器处理完后再传递给 C 处理器,以此类推,形成一个链条。链条上的每个处理器各自承担各自的处理职责,所以叫作职责链模式。
职责链模式
迭代器模式也叫游标模式,它用来遍历集合对象。
“集合对象”,也可以叫“容器”“聚合对象”,实际上就是包含一组对象的对象,比如,数组、链表、树、图、跳表。
解耦容器代码和遍历代码
迭代器模式
状态模式一般用来实现状态机,而状态机常用在游戏、工作流引擎等系统开发中。状态机又叫有限状态机,它由 3 个部分组成:状态、事件、动作。其中,事件也称为转移条件。事件触发状态的转移及动作的执行。不过,动作不是必须的,也可能只转移状态,不执行任何动作。
利用 if-else 或者 switch-case 分支逻辑,参照状态转移图,将每一个状态转移原模原样地直译成代码
分支逻辑法
对于状态很多、状态转移比较复杂的状态机来说,查表法比较合适。通过二维数组来表示状态转移图,能极大地提高代码的可读性和可维护性。
查表法
对于状态并不多、状态转移也比较简单,但事件触发执行的动作包含的业务逻辑可能比较复杂的状态机来说
利用状态模式
状态模式
访问者模式允许一个或者多个操作应用到一组对象上
解耦操作和对象本身,保持类职责单一、满足开闭原则以及应对代码的复杂性。
访问者模式
备忘录模式也叫快照模式,具体来说,就是在不违背封装原则的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,以便之后恢复对象为先前的状态。
存储副本以便后期恢复
要在不违背封装原则的前提下,进行对象的备份和恢复
这个模式的定义表达了两部分内容
备忘录模式
命令模式用到最核心的实现手段,就是将函数封装成对象。
在大部分编程语言中,函数是没法作为参数传递给其他函数的,也没法赋值给变量。借助命令模式,将函数封装成对象,这样就可以实现把函数像对象一样使用。
命令模式
解释器模式为某个语言定义它的语法(或者叫文法)表示,并定义一个解释器用来处理这个语法。
解释器模式
中介模式的设计思想跟中间层很像,通过引入中介这个中间层,将一组对象之间的交互关系(或者说依赖关系)从多对多(网状关系)转换为一对多(星状关系)。原来一个对象要跟 n 个对象交互,现在只需要跟一个中介对象交互,从而最小化对象之间的交互关系,降低了代码的复杂度,提高了代码的可读性和可维护性。
观察者模式和中介模式都是为了实现参与者之间的解耦,简化交互关系。
在观察者模式的应用场景中,参与者之间的交互比较有条理,一般都是单向的,一个参与者只有一个身份,要么是观察者,要么是被观察者。
在中介模式的应用场景中,参与者之间的交互关系错综复杂,既可以是消息的发送者、也可以同时是消息的接收者。
观察者模式 VS 中介模式
中介模式
行为型设计模式解决“类或对象之间的交互”问题
23种设计模式
0 条评论
回复 删除
下一页