设计模式-设计原则以及大纲
参考文献
- Java设计模式精讲 Debug方式+内存分析
- https://java-design-patterns.com/
- https://geek-docs.com/design-pattern/design-principle/design-principle-index.html
前置知识
UML
UML (Unified Modeling Language) 统一建模语言
- 特点
- UML是一种开放的方法;
- 用于说明,可视化,构建和编写一个正在开发的面向对象的软件密级系统的制品的开放方法;
- UML2.2分类: 一共14中图示
- 结构式图形: 强调的是系统式的建模;
- 静态图
- 类图
- 对象图
- 包图
- 实现图
- 组件图
- 部署图
- 剖面图
- 复合结构图
- 静态图
- 行为式图形: 强调系统模型中触发的事件;
- 活动图
- 状态图
- 用例图
- 交互式图形: 属于行为式图形子集合,强调系统模型中资料流程.
- 通信图
- 时序图
- 交互概述图
- 时间图
- 结构式图形: 强调的是系统式的建模;
类图
Class Diagram: 用于表示类,接口,实例等之间相互间的静态关系
- UML箭头方向: 从子类指向父类
- 定义子类是需要通过extends关键字指定父类
- 子类一定知道父类定义,但父类并不知道子类的定义
- 只有知道对方信息是才能指向对方
关联/依赖,继承/实现,组合/聚合
-
实线: 关联 虚线: 依赖
-
实线–关联: 关系稳定,实打实的关系
-
表示一个类对象和另一个类对象有关联
-
通常是一个类中有另一个类对象作为属性
-
这种关系比依赖更强、不存在依赖关系的偶然性、关系也不是临时性的,一般是长期性的,而且双方的关系一般是平等的、关联可以是单向、双向的;
-
表现在代码层面,为被关联类B以类属性的形式出现在关联类A中,也可能是关联类A引用了一个类型为被关联类B的全局变量;
-
-
虚线–依赖: 临时用一下,若即若离,虚无缥缈,若有若无;
- 表示一种使用关系,一个类需要借助另一个类来实现功能.
- 一般是一个类使用另一个类作为参数使用,或作为返回值;
- 比如某人要过河,需要借用一条船,此时人与船之间的关系就是依赖;
- 表现在代码层面,为类B作为参数被类A在某个method方法中使用;
-
-
实线: 继承(extends) 虚线: 实现(implements)
- 空心三角箭头: 继承或实现
- 实线–继承:
is a
关系,扩展目的(不虚,很现实); - 虚线–实现: 虚线代表"虚",无实体
-
实心菱形:组合 空心菱形: 聚合
- 菱形就是一个盛对象的器皿
- 组合: 代表满器皿里,已经有实体结果存在,生死与共;
- 整体和局部的关系和聚合的关系相比,关系更加强烈,两者有着相同的生命周期,
contans-a
的关系; - 强关系
- 整体和局部的关系和聚合的关系相比,关系更加强烈,两者有着相同的生命周期,
- 聚合: 代表空器皿里面可以放很多相同的东西,聚在一起(箭头方向所指的类)
- 整体和局部的关系,两者有着独立的生命周期,是
has a
的关系 - 弱关系
- 整体和局部的关系,两者有着独立的生命周期,是
-
常见数字表达以及含义
- 0…1: 0或1个实例
- 0…*: 0或多个实例
- 1…1: 1个实例
- 1: 只能一个实例
- 1…*: 至少一个实例
- 斜体为抽象
- +: public
- -: private
- #: protected
- ~: default(包权限)
- 下划线: static(静态)
Program to an interface
, not an Impltemention
- 使用者不需要知道数据类型,结构,算法的细节;
- 使用者不需要知道实现细节,只需要知道提供的接口;
- 利用抽象,封装,动态绑定,多态,符合面对对象的特质和理论;
Favor object compostion
over class inheritance
- 继承需要给子类暴露一些父类的设计和实现细节;
- 父类实现的改变会造成子类也需要改变;
- 我们以为继承主要是为了代码重用,但实际上在子类需要重新实现很多父类的方法;
- 继承更多的应该是为了多态;
UML时序图
Sequence Diagram: 是显示对象之间交互的图,这些对象是时间顺序排列的.时序图中包括的建模元素主要有:
- 对象(Actor)
- 生命线(Lifeline)
- 控制焦点(Focus of control)
- 消息(Message)
设计原则
- 糟糕的设计有 3 个重要特征,应该避免:
- 刚性(
Rigidity
)-很难改变,因为每次改变都会影响系统的太多其他部分 - 脆弱性(
Fragility
) - 当您进行更改时,系统的意外部分会损坏. - 固定性(
Immobility
) - 很难在另一个应用程序中重用,因为它无法与当前应用程序分离
- 刚性(
设计目标
开闭原则(Open Close Principle,OCP
)
Software entities like classes, modules and functions should be open for extension but closed for modifications.
像类、模块和函数这样的软件实体应该对扩展开放,但对修改关闭.
-
优点:
-
用抽象构建框架,用实现扩展细节;
-
提高软件系统的可复用性以及可维护性;
-
-
模板模式和策略模式
里氏替换原则(Liskov Substitution Principle,LSP
)
- Derived types must be completely substitutable for their base types.
派生类型必须完全可替换其基类型.
-
定义: 如果对每个类型为T1的对象o1,都有类型为T2的对象o2,使得以T1定义的所有程序P在所有的对象o1都替换成o2时程序P的行为没有发生变化,那么类型T2类型是类型T1的子类型.
-
定义扩展: 一个软件实体如果使用一个适用一个父类的话,那一定适用于子类,所有引用父类的地方必须能透明的使用其子类的对象,子类对象能够替换父类对象,而程序的逻辑不变.
-
引申意义: 子类可以扩展父类的功能,但不能改变父类原有的功能
-
含义1: 子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法;
-
含义2: 子类可以增加自己特有的方法;
-
含义3: 当子类的方法重载父类的方法时,方法的前置条件(即方法的输入/入参)要比父类的方法的输入参数更宽松(更大);
-
含义4: 当子类的方法实现父类的方法时(重写/重载或实现抽象方法)方法的后置条件(即方法的输出/返回值)要比父类更严格或相等(相等或小于);
-
含义5: 子类中的方法不应抛出基础方法预期之外的异常类型
-
-
优点:
-
约束继承泛滥,开闭原则的一种体现;
-
加强程序的健壮性,同时变更时也可以做到非常好的兼容性,提高程序的维护性,扩展性.降低需求变更时引入的风险;
-
迪米特原则(Law of Demeter, LoD
)
- 最少知道原则(
Least Knowledge Principle, LKP
)
- Each unit should have only limited knowledge about other units: only units “closely” related to the current unit.
每个单元应该只对其他单元有有限的了解:只有与当前单元“密切”相关的单元.- Each unit should only talk to its friends; don’t talk to strangers.
每个单位应该只与它的朋友交谈;不要和陌生人说话.- Only talk to your immediate friends.
只与您最亲密的朋友交谈.
-
定义: 一个对象应该对其他对象保持最少的了解,又叫最少知道原则;
- 尽量降低类与类之间的耦合
-
优点:
-
降低类之间的耦合
-
强调只和朋友交流,不和陌生人交流
-
朋友: 出现在成员变量,方法的输入,输出参数中的类称为成员朋友类,而出现在方法体内部的类不属于朋友类
-
设计方法
单一责任原则(Single Responsibility Principle,SRP
)
- A class should have only one reason to change.
一个类应该只有一个改变的理由.
-
定义: 不要存在多余一个导致类变更的原因;
- 一个类/接口/方法只负责一项职责
-
优点:
-
降低累的复杂度;
-
提高类的可读性;
-
提高系统的可维护性;
-
降低变更引起的风险;
-
接口隔离原则(Interface Segregation Principle,ISP
)
- Clients should not be forced to depend upon interfaces that they don’t use.
不应强迫客户端依赖他们不使用的接口.
-
定义: 用多个专门的接口,而不适用单一的总接口,客户端不应该被强迫依赖它不需要的接口;
-
一个类对一个类的依赖应该建立在最小的接口上
-
建立单一接口,不要建立庞大臃肿的接口
-
尽量细化接口,接口中方法尽量少
-
-
优点:
- 符合高内聚低耦合的设计思想,从而使类具有很好的可读性,可扩展性,可维护性.
依赖倒置原则(Dependency Inversion Principle,DIP
)
- High-level modules should not depend on low-level modules. Both should depend on abstractions.
高层模块不应该依赖于低层模块.两者都应该依赖于抽象.- Abstractions should not depend on details. Details should depend on abstractions.
抽象不应该依赖于细节.细节应该取决于抽象.
-
定义: 高层模块不应该依赖低层模块,二者都应该依赖抽象;
-
抽象不应该依赖细节,细节应该依赖抽象;
-
针对接口编程,不要针对实现编程;
-
-
定义中的高层模块和低层模块,主要对应的是调用关系上的层级.
-
高层模块和低层模块都应该依赖抽象,是为了消除模块间变化对对方造成的影响,换句话说,抽象是一种约束,让高层模块和低层模块不能太随意地变动.
-
抽象不应该依赖实现,实现应该依赖于抽象.
-
优点:
-
可能减少的耦合性,;
-
提高系统稳定性;
-
提高代码可读性和可维护性;
-
可降低修改程序所造成的风险;
-
-
目的:
-
控制代码变化带来的影响.
-
增强代码的可读性和可维护性
-
-
工厂和抽象工厂可以用作依赖框架,但有专门的框架,称为控制反转容器
IoC,DI,IoC容器
和DIP
的区别
IoC
容器是一种技术框架,它用来管理对象的创建及其生命周期,提供依赖注入实现,是DI
的具体实现;DI
是一种设计模式,将依赖通过“注入”的方式提供给需要的类,是DIP
和IoC
的具体实现;IoC
是一种设计原则(或设计模式),将代码本职之外的工作交由某个第三方(框架)完成,与DIP
相似;DIP
是一种设计原则,它认为高层组件的功能不应该依赖下层组件的实现,而应该提供抽象层让下层依赖,与IoC
有异曲同工之妙。
组合/聚合复用原则(Composite/Aggregate Reuse Principle CARP
)
-
定义: 尽量采用组合(
contains-a
)、聚合(has-a
)的方式而不是继承(is-a
)的关系来达到软件的复用目的 -
组合/聚合复用原则是通过将已有的对象纳入新对象中,作为新对象的成员对象来实现的,新对象可以调用已有对象的功能,从而达到复用. 原则是尽量首先使用合成 / 聚合的方式,而不是使用继承.
-
聚合
has-A
-
组合
contains-A
-
继承
is-A
-
-
优点: 可以使系统更加灵活,降低类与类之间的耦合度,一个类的变化,对其他造成的影响相对较少.
其他原则
DRY(Don't Repeat Yourself)
不要重复自己
- 宁可重复,也不要错误的抽象.不要为了抽象而创建抽象;
- 保持对原则的警醒比应用了多少原则更重要
KISS(Keep It Simple And Stupid)
保持简单,保持愚蠢
- 作用:
- 防止代码腐坏
- 减少时间成本的投入
- 快速迭代,拥抱变化
- 简单的含义
- 在软件开发中,"简单"其实是最终的一种状态
- 简单不是什么
- 简单≠简单设计或简单编程
- 保持简单并不是只能做简单设计或简单编程,而是做设计或编程时要努力以最终产出简单为目标,过程可能非常复杂也没关系.
- 简单≠数量少
- 简单≠过度简洁
- 简单≠简单设计或简单编程
- 简单是什么
- 简单应该是坚持实践
- 简单应该是尽量简单,但又不能太简单.换句话说,就是要管理合适的代码上下文,并且在边界范围内以"最少知识"的方式构建程序,满足要求即可,保持一定的克制.
- 简单应该是让别人理解代码逻辑时更简单.
- 四不要
- 不要长期进行打补丁式的编码
- 不要炫耀编程技巧
- 不要简单编程
- 不要过早优化
- 四要
- 要定期做
Code Review
- 要选择合适的编码规范
- 要适时重构
- 要有目标地逐渐优化
- 要定期做
YAGNI(You ain't Gonna Need It)
你并不会需要它
- 在软件开发中,它不希望你写出"将来可能需要,但现在却用不上"的代码
小结
- 开闭原则是核心,对扩展开放对修改关闭是软件设计、后期扩展的基石
- 单一职责原则要求我们设计接口,制定模块功能时保持模块或者接口功能单一,接口设计或功能设计尽量保持原子性,修改一处不能影响全局或其它模块
- 里氏替换原则和依赖倒置原则,按照作者的理解,这俩原则总的是要求我们要面向接口、面向抽象编程,设计程序的时候尽可能使用基类或者接口进行对象的定义或引用,而不是具体的实现,否则实现一旦有变更,上层调用者就必须做出对应变更,这样一来,整个模块可能都需要重新调整,非常不利于后期拓展
- 接口隔离原则具体应用到程序中,比如我们在传统 MVC 开发时,Service 层调用 DAO 层一般会使用接口进行调用,各层之间尽量面向接口通信,其实也是一种降低模块耦合的方法
- 迪米特法则的初衷也是为了降低模块耦合,代码示例中我们引入了类似 “中间人” 的概念,上层模块不直接调用下层模块,而是引入第三方进行代办,这也是为了降低模块的耦合度
- 组合/聚合复用原则,聚合是一种弱关联,而组合是一种强关联,表现在 UML 类图上的话聚合是使用空心四边形加箭头表示,而组合是使用实心四边形加箭头表示,合成复用原则总的就是要求我们尽量利用好已有对象,从而达到功能复用,具体是聚合还是组合,还是一般关联,就要看具体情况再定了
设计模式分类
创建型
-
是对对象创建过程的各种问题和解决方案的总结
-
简单工厂模式(不属于GOF23种设计模式)
定义: 由一个工厂对象决定创建出哪一种产品类的实例;
类型: 创建型 不属于GOF23中设计模式
适用场景:
- 工厂类负责创建的对象比较少;
- 客户端(应用层)只知道传入工厂类的参数,对于如何创建对象(逻辑)不关心;
优点: 只需要传入一个正确的参数,就可以获取所需要的对象,而无须知道其创建细节.
缺点: 工厂类的责任相对过重,增加新的产品需要修改工厂类的判断逻辑,违背开闭原则.
-
工厂方法模式(产品等级) 🔴
-
客户端需要创建的对象非常多;
-
客户端不关心对象的创建过程.
-
-
抽象工厂模式(产品族) 🔴
-
需要一组相关的产品对象;
-
系统独立于它的产品的创建、组合和表示时.
-
-
建造者模式(
Builder
) 🔴-
需要创建的产品对象有复杂的内部结构;
-
需要创建的产品对象的属性相互依赖;
-
要求生成的产品对象具有复杂的属性.
-
-
单例模式(
Singleton
,饿汉式和懒汉式,线程安全和非线程安全,单例模式的破坏方式) 🔴-
系统只需要一个实例对象;
-
系统需要控制某个类的实例数量.
-
-
原型模式(
ProtoType
)- 类初始化需要消化非常多的资源,这个资源包括数据、硬件资源等;
- 通过new产生一个对象需要非常繁琐的数据准备或访问权限等;
- 一个对象多个修改者的场景.
-
对象池(
Object Pool
) 🔴- 重用和共享创建成本高昂的对象
结构型
-
是针对软件设计结构的总结
-
外观模式(
Facade
)- 当需要为一个复杂子系统提供一个简单接口时;
- 客户端与复杂子系统之间存在很大的依赖性.
-
装饰者模式(
Decorator
)- 在不影响其他对象的情况下,以动态、透明的方式给单个对象添加职责;
- 需要动态地给一个对象添加功能,这些功能也可能会被动态的撤销.
-
适配器模式(
Adapter
) 🔴- 已经存在的类的接口不符合我们的需求;
- 创建一个可以复用的类,使得该类可以与其他不相关的类或者不可预见的类协同工作.
-
享元模式(
Flyweight
)- 需要缓存池的场景;
- 重复使用一个固定数量的对象.
-
组合模式(
Composite
) 🔴- 希望客户端能够一致地处理复杂与简单元素;
- 希望在不同层次进行对象的组合和处理.
-
桥接模式(
Bridge
)- 抽象和具体实现之间需要增加更多的灵活性;
- 一个类存在两个或多个独立变化的维度,且这两个或多个维度都需要进行扩展.
-
代理模式(
Proxy
) 🔴- 为其他对象提供一种代理以控制对这个对象的访问;
- 当无法或者不想直接访问某个对象或访问某个对象存在困难时,可以通过一个代理对象来间接访问.
行为型
- 是从类或对象之间交互、职责划分等角度总结的模式
- 模板方法模式(
Template Method
) 🔴- 当需要实现一些算法或者流程,但是其中的某些步骤需要不同的实现时,可以使用模板方法模式.这种情况下,算法或者流程的框架由父类定义,具体的实现由子类负责.
- 当需要多个类具有相似的行为,但是具体实现方式有所不同时,可以使用模板方法模式.这种情况下,可以将相同的行为抽象到父类中,具体实现则由子类来完成.
- 迭代器模式(
Iterator
)- 当需要遍历一个聚合对象时,可以使用迭代器模式.这种情况下,可以将遍历操作从聚合对象中分离出来,从而使得聚合对象和遍历算法可以独立地变化.
- 当需要对聚合对象进行不同的遍历方式时,可以使用迭代器模式.这种情况下,可以定义多个不同的迭代器,从而使得不同的遍历方式可以被实现
- 策略模式(
Strategy
) 🔴- 当需要在运行时动态地改变一个对象的行为时,可以使用策略模式.这种情况下,可以将不同的行为封装成不同的策略类,并且在运行时动态地选择使用哪一个策略类.
- 当需要封装一系列的算法,使得它们可以互换时,可以使用策略模式.这种情况下,每个算法都可以被封装成一个策略类,从而使得算法可以互换.
- 解释器模式(
Interpreter
)- 当需要定义一种语言文法,并且需要解释该语言中的语句时,可以使用解释器模式.
- 当需要对一个语言中的表达式进行解释时,可以使用解释器模式.
- 当需要动态地扩展语言规则时,可以使用解释器模式.
- 观察者模式(
Observer
) 🔴- 当一个对象的状态发生改变时,需要通知其他对象,并且不知道有多少个对象需要通知时,可以使用观察者模式.这种情况下,可以将状态的改变封装成一个事件,其他对象可以注册事件的监听器,从而在状态发生改变时得到通知.
- 当需要实现发布-订阅模型时,可以使用观察者模式.这种情况下,发布者发布事件,订阅者订阅事件,从而实现发布-订阅模型.
- 备忘录模式(
Memento
)- 当需要保存一个对象的内部状态,并且需要在后续将对象恢复到该状态时,可以使用备忘录模式.
- 当需要实现可撤销操作时,可以使用备忘录模式.
- 当需要保留对象的历史状态时,可以使用备忘录模式.
- 命令模式(
Command
)- 当需要将请求的发送者和接收者解耦时,可以使用命令模式.这种情况下,请求被封装成一个命令对象,发送者只需要知道如何发送命令,而不需要知道如何执行命令,接收者只需要知道如何执行命令,而不需要知道如何发送命令.
- 当需要在不同的时间指定请求、将请求排队和执行请求时,可以使用命令模式.这种情况下,命令对象可以被放入队列中,从而实现对请求的排队和调度.
- 中介者模式(
Mediator
)- 当多个对象之间存在复杂的关系时,可以使用中介者模式.
- 当一个对象需要引用很多其他对象时,可以使用中介者模式来简化对象之间的关系.
- 当需要动态地增加或删除对象时,可以使用中介者模式.
- 责任链模式(
Chain of Responsibility
) 🔴- 当需要以链式方式处理请求时,可以使用职责链模式.这种情况下,每个处理者都可以处理请求,但是只有在自己无法处理请求时才将请求传递给下一个处理者.
- 当需要将请求的发送者和接收者解耦时,可以使用职责链模式.这种情况下,发送者只需要将请求发送给第一个处理者,而不需要知道具体的处理过程,每个处理者只需要知道自己是否能够处理请求,如果不能处理则将请求传递给下一个处理者.
- 访问者模式(
Visitor
)- 当需要在不改变对象结构的前提下,定义新的操作和算法时,可以使用访问者模式.
- 当需要对对象结构中的元素进行复杂的操作时,可以使用访问者模式.
- 当对象结构中的元素类型较少,但需要对每个元素进行不同的操作时,可以使用访问者模式.
- 状态模式(
State
)- 对象的行为取决于其状态,并且需要在运行时根据状态改变其行为.
- 对象具有大量的状态,且这些状态之间经常发生转换,需要避免使用大量的条件语句或switch语句.
- 对象的状态可以被多个对象共享,并且需要在不同的上下文中使用.
- 对象的状态转换需要满足一定的约束条件,例如只能在特定的状态下进行转换.
- 对象的行为和状态需要独立地进行测试和修改,以便更好地维护和扩展.
- 对象的状态转换需要支持撤销和重做操作.