设计模式与原则
2020-07-08 00:27:00 0 举报仅支持查看
AI智能生成
设计模式与原则、提升代码质量
设计模式
模版推荐
作者其他创作
大纲/内容
提升代码质量
发现代码质量问题<br>
评判标准
可维护性
在不破坏原有代码设计、不引入新的 bug 的情况下,能够快速地修改或者添加代码
如果修改一个 bug,修改、添加一个功能,需要花费很长的时间,可以认为代码不易维护
可读性
代码是否易读、易理解外,代码的可读性在非常大程度上会影响代码的可维护性
看代码是否符合编码规范、命名是否达意、注释是否详尽、函数是否长短合适、模块划分是否清晰、是否符合高内聚低耦合等等
可扩展性
代码的可扩展性表示,在不修改或少量修改原有代码的情况下,通过扩展的方式添加新的功能代码
表示代码应对未来需求变化的能力,代码是否易扩展也很大程度上决定代码是否易维护<br>
可测试性
代码的可测试性差,比较难写单元测试,那基本上就能说明代码设计得有问题
可复用性
尽量减少重复代码的编写,复用已有的代码
灵活性
如果一段代码易扩展、易复用或者易用,都可以称这段代码写得比较灵活
简洁性
尽量保持代码简单,满足KISS原则。代码简单、逻辑清晰,也就意味着易读、易维护
常规检查
目录设置是否合理、模块划分是否清晰、代码结构是否满足“高内聚、松耦合”?
是否遵循经典的设计原则和设计思想(SOLID、DRY、KISS、YAGNI、LOD 等)?
设计模式是否应用得当?是否有过度设计?
代码是否容易扩展?如果要添加新功能,是否容易实现?
代码是否可以复用?是否可以复用已有的项目代码或类库?是否有重复造轮子?
代码是否容易测试?单元测试是否全面覆盖了各种正常和异常的情况?
代码是否易读?是否符合编码规范(比如命名和注释是否恰当、代码风格是否一致等)?
业务检查
代码是否实现了预期的业务需求?
逻辑是否正确?是否处理了各种异常情况?
日志打印是否得当?是否方便 debug 排查问题?
接口是否易用?是否支持幂等、事务等?
代码是否存在并发问题?是否线程安全?
性能是否有优化空间,比如,SQL、算法是否可以优化?
是否有安全漏洞?比如输入输出校验是否全面?
非功能性检查
易用性
性能
扩展性
容错性
通用性
应用设计原则
SRP — 单一职责原则
OCP — 开闭原则
LSP — 里式替换原则
ISP — 接口隔离原则
DIP — 依赖倒置原则
KISS — 尽量保持简单,保持代码的可读性和可维护性
YAGNI — 不要过度设计,不要写当前用不到的代码
DRY — 不要重复
LOD — 迪米特法则(最小知识原则)
应用编程技巧
面向对象的特性 — 封装、抽象、继承、多态
高内聚、松耦合
依赖注入
基于接口编程而非实现
多用组合少用继承
复杂继承可使用组合、接口、委托的技术实现
面向对象编程,多用充血模型
代码可测试性
依赖注入是编写可测试性代码的最有效手段
常见的反模式
代码中包含未决行为逻辑
滥用可变全局变量
滥用静态方法
使用复杂的继承关系
高度耦合的代码
编程规范
命名与注释
命名能准确达意
命名要可读、可搜索
注释的内容:做什么、为什么、怎么做,如何用
类和函数一定要写注释
代码风格要尽量统一
编程技巧
将复杂的逻辑拆分成类或函数
参数过多,拆分成多个函数或将参数封装为对象
函数中不要使用参数来做代码执行逻辑的控制
函数设计要职责单一
移除过深的嵌套层次
用字面量取代魔法数
用解释性变量来解释复杂表达式
团队统一编码规范
单元测试和 Code Review 是保证代码质量的有效手段
代码重构
持续重构是保持代码质量不下降的有效手段,能有效避免代码腐化到无可救药的地步
重构的工具就是面向对象设计思想、设计原则、设计模式、编码规范
在开发初期,不要过度设计,应用复杂的设计模式。当代码出现问题的时候,再针对问题,应用原则和模式进行重构
区别与联系
面向对象编程:可以实现很多复杂的设计思路,是很多设计原则、设计模式等编码实现的基础
设计原则:是指导代码设计的一些经验总结,对于某些场景下,是否应该应用某种设计模式,具有指导意义
设计模式:是针对软件开发中经常遇到的一些设计问题,总结出来的一套解决方案或者设计思路。应用设计模式的主要目的是提高代码的可扩展性。设计模式更加具体、更加可执行
编程规范:主要解决的是代码的可读性问题。编码规范更加具体、更加偏重代码细节、更加能落地。持续的小重构依赖的理论基础主要就是编程规范<br>
重构:保持代码质量不下降的有效手段。利用的就是面向对象、设计原则、设计模式、编码规范这些理论
面向对象
面向对象分析和面向对象设计
OOA、OOD、OOP 三个连在一起就是面向对象分析、设计、编程(实现),正好是面向对象软件开发要经历的三个阶段
面向对象分析就是要搞清楚做什么,面向对象设计就是要搞清楚怎么做,面向对象编程就是将分析和设计的的结果翻译成代码的过程
面向对象分析
面向对象分析的产出是详细的需求描述
需求需要你自己去挖掘,做合理取舍、权衡、假设,把抽象的问题具象化,最终产生清晰的、可落地的需求定义
需求分析的过程实际上是一个不断迭代优化的过程,先给出一个粗糙的、基础的方案,有一个迭代的基础,然后再慢慢优化
面向对象设计<br>
面向对象设计的产出是类,将需求描述转化为具体的类的设计
划分职责进而识别出有哪些类
定义类及其属性和方法
定义类与类之间的交互关系
泛化(Generalization):可以简单理解为继承关系<br>
实现(Realization):一般是指接口和实现类之间的关系
聚合(Aggregation):是一种包含关系,A 类对象包含 B 类对象,B 类对象的生命周期可以不依赖 A 类对象的生命周期,也就是说可以单独销毁 A 类对象而不影响 B 对象,比如课程与学生之间的关系
组合(Composition):是一种包含关系,A 类对象包含 B 类对象,B 类对象的生命周期依赖 A 类对象的生命周期,B 类对象不可单独存在,比如鸟与翅膀之间的关系。
依赖(Dependency):不管是 B 类对象是 A 类对象的成员变量,还是 A 类的方法使用 B 类对象作为参数或者返回值、局部变量,只要 B 类对象和 A 类对象有任何使用关系,我们都称它们有依赖关系。包含聚合和组合关系。
将类组装起来并提供执行入口
面向对象编程
面向对象编程就是将分析和设计的的结果翻译成代码的过程
面向对象的四大特性
封装(Encapsulation)
封装关注的是如何隐藏信息、保护数据。类暴露有限的访问接口,外部仅能通过类提供的方式(函数)来访问内部信息或者数据<br>
封装的意义在于,类仅仅通过有限的方法暴露必要的操作,能提高类的易用性、可读性、可维护性
封装需要编程语言本身提供一定的语法机制来支持。这个语法机制就是访问权限控制,如 Java 的 private、public 等关键字
抽象(Abstraction)
抽象关注的是如何隐藏方法的具体实现,让调用者只需要关心方法提供了哪些功能,并不需要知道这些功能是如何实现的
抽象有时候会被排除在面向对象的四大特性之外,因为抽象这个概念是一个非常通用的设计思想,可以用在很多方面
继承(Inheritance)
继承是用来表示类之间的 is-a 关系,继承最大的一个好处就是代码复用,可以将相同的部分,抽取到父类中,实现复用。<br>
要避免过度使用继承,继承层次过深过复杂,就会导致代码可读性、可维护性变差
继承需要编程语言提供特殊的语法机制来支持,比如 Java 使用 extends 关键字来实现继承
多态(Polymorphism)
多态是指,子类可以替换父类,在实际的代码运行过程中,调用子类的方法实现
多态特性能提高代码的可扩展性和复用性。多态也是很多设计模式、设计原则、编程技巧的代码实现基础
多态需要编程语言提供特殊的语法机制来实现,要支持父类对象可以引用子类对象,要支持继承,要支持子类可以重写(override)父类中的方法
面向对象与面向过程
面向过程编程
主要特点是,它以过程(可以理解为方法、函数、操作)作为组织代码的基本单元,数据(可以理解为成员变量、属性)与方法相分离
面向过程风格是一种流程化的编程风格,通过拼接一组顺序执行的方法来操作数据完成一项功能。
面向过程编程语言
它最大的特点是不支持类和对象两个语法概念,不支持丰富的面向对象编程特性(比如继承、多态、封装),仅支持面向过程编程
面向对象编程
它以类或对象作为组织代码的基本单元,并将封装、抽象、继承、多态四个特性,作为代码设计和实现的基石
面向对象编程语言
支持类或对象的语法机制,并有现成的语法机制,能方便地实现面向对象编程四大特性(封装、抽象、继承、多态)的编程语言
面向对象编程相比面向过程编程的优势<br>
OOP 更加能够应对大规模复杂程序的开发
接口vs抽象类
抽象类
继承关系是一种 is-a 的关系
抽象类不允许被实例化,只能被继承。子类继承抽象类,必须实现抽象类中的所有抽象方法
抽象类可以包含属性和方法。方法既可以包含代码实现,也可以不包含代码实现,不包含代码实现的方法叫作抽象方法
抽象类是为代码复用而生的,多个子类可以继承抽象类中定义的属性和方法,避免在子类中,重复编写相同的代码
抽象类是一种自下而上的设计思路,先有子类的代码重复,然后再抽象成上层的父类(也就是抽象类)
如果我们要表示一种 is-a 的关系,并且是为了解决代码复用的问题,我们就用抽象类
接口
接口表示一种 has-a 关系,接口有一个更加形象的叫法,那就是协议(contract)
接口不能包含属性(成员变量),接口只能声明方法,方法不能包含代码实现(可以有默认实现)
接口更侧重于解耦,是对行为的一种抽象,相当于一组协议或者契约。接口实现了约定和实现相分离,可以降低代码间的耦合性,提高代码的可扩展性
接口是一种自上而下的设计思路。在编程的时候,一般都是先设计接口,再去考虑具体的实现
如果我们要表示一种 has-a 关系,并且是为了解决抽象而非代码复用的问题,那我们就可以使用接口
基于接口而非实现编程
“接口”就是一组“协议”或者“约定”,是功能提供者提供给使用者的一个“功能列表”
越抽象、越顶层、越脱离具体某一实现的设计,越能提高代码的灵活性,越能应对未来的需求变化
多用组合少用继承
继承最大的问题就在于:继承层次过深、继承关系过于复杂,耦合度就会很高,会影响到代码的可读性和可维护性
可以利用组合(composition)、接口、委托(delegation)三个技术手段,来解决继承存在的问题<br>
继承主要有三个作用:表示 is-a 关系,支持多态特性,代码复用。而这三个作用都可以通过其他技术手段来达成。比如 is-a 关系,可以通过组合和接口的 has-a 关系来替代;多态特性我们可以利用接口来实现;代码复用我们可以通过组合和委托来实现
针对一类特性,封装成接口,不同的子类实现不同的接口;每个接口定义一个实现类,实际子类通过组合和委托的技术实现接口的特性。
设计原则
单一职责原则(SRP)
一个类只负责完成一个职责或者功能。也就是说,不要设计大而全的类,要设计粒度小、功能单一的类
如何判断类的职责是否足够单一
类中的代码行数、函数或属性过多,一个类的代码行数最好不能超过 200 行,函数个数及属性个数都最好不要超过 10 个
类依赖的其他类过多,或者依赖类的其他类过多,不符合高内聚、低耦合的设计思想,就需要考虑对类进行拆分
私有方法过多,我们就要考虑能否将私有方法独立到新的类中,设置为 public 方法,供更多的类使用,从而提高代码的复用性
比较难给类起一个合适名字,很难用一个业务名词概括,这就说明类的职责定义得可能不够清晰
类中大量的方法都是集中操作类中的某几个属性,可以考虑将这几个属性和对应的方法拆分出来
开闭原则(OCP)
添加一个新的功能应该是,在已有代码基础上扩展代码(新增模块、类、方法等),而非修改已有代码(修改模块、类、方法等)。对拓展开放是为了应对变化(需求),对修改关闭是为了保证已有代码的稳定性;最终结果是为了让系统更有弹性!
添加一个新功能,不可能任何模块、类、方法的代码都不“修改”,这个是做不到的。要尽量让修改操作更集中、更少、更上层,尽量让最核心、最复杂的那部分逻辑代码满足开闭原则。<br>
要写出支持“对扩展开放、对修改关闭”的代码的关键是预留扩展点,要写出扩展性好的代码,我们要时刻具备扩展意识、抽象意识、封装意识<br>
里式替换原则(LSP)<br>
子类对象能够替换程序中父类对象出现的任何地方,并且保证原来程序的逻辑行为不变及正确性不被破坏。里式替换原则还有一种理解是“按照协议来设计”
哪些代码明显违背了 LSP
子类违背父类声明要实现的功能
子类违背父类对输入、输出、异常的约定
子类违背父类注释中所罗列的任何特殊说明
接口隔离原则(ISP)
客户端不应该被强迫依赖它不需要的接口。其中的“客户端”,可以理解为接口的调用者或者使用者
接口的设计要尽量单一,不要让接口的实现类和调用者,依赖不需要的接口函数
依赖反转原则(DIP)
高层模块不要依赖低层模块,高层模块和低层模块应该通过抽象来互相依赖。抽象不要依赖具体实现细节,具体实现细节依赖抽象。简单来说就是面向接口编程,不同层之间增加接口层来隔离调用方和实现方的依赖。
在调用链上,调用者属于高层,被调用者属于低层。在平时的业务代码开发中,高层模块依赖底层模块是没有任何问题的。这条原则主要是用来指导框架层面的设计,跟控制反转类似。
KISS 原则
KISS 原则是保持代码可读和可维护的重要手段
如何写出满足 KISS 原则的代码
不要使用同事可能不懂的技术来实现代码。比如一些编程语言中过于高级的语法等
不要重复造轮子,要善于使用已经有的工具类库
不要过度优化,不要过度使用一些奇技淫巧来优化代码,牺牲代码的可读性<br>
YAGNI 原则
不要去设计当前用不到的功能;不要去编写当前用不到的代码。这条原则的核心思想就是:不要做过度设计
DRY原则
不要写重复的代码
三种典型的代码重复
实现逻辑重复:两个方法中部分逻辑一样,如一些参数的校验,此时并不违反DRY原则。对于包含重复代码的问题,可以通过抽象成更细粒度函数的方式来解决
功能语义重复:不同的函数实现同样的功能,我们应该在项目中,统一一种实现思路,同一种功能都统一调用同一个函数
代码执行重复:例如多处重复查询
迪米特法则(LOD)
也叫作最小知识原则,每个模块只应该了解那些与它关系密切的模块的有限知识<br>
不该有直接依赖关系的类之间,不要有依赖;有依赖关系的类之间,尽量只依赖必要的接口,基于最小接口而非最大实现编程
迪米特法则是希望减少类之间的耦合,让类越独立越好。每个类都应该少了解系统的其他部分,一旦发生变化,需要了解这一变化的类就会比较少
高内聚、松耦合
高内聚、松耦合 可以用来指导不同粒度代码的设计与开发,比如系统、模块、类,函数,也可以应用到微服务、框架、组件、类库等开发中
高内聚,指相近的功能应该放到同一个类中,不相近的功能不要放到同一个类中。单一职责原则就是实现代码高内聚非常有效的设计原则<br>
松耦合,就是类与类之间的依赖关系简单清晰。依赖注入、接口隔离、基于接口而非实现编程、迪米特法则,都是为了实现代码的松耦合
重构
重构的目的:为什么要重构
重构是一种对软件内部结构的改善,目的是在不改变软件的可见行为的情况下,使其更易理解,修改成本更低
重构就是在保持功能不变的前提下,利用设计思想、原则、模式、编程规范等理论来优化代码,修改设计上的不足,提高代码质量
重构是时刻保证代码质量的一个极其有效的手段,不至于让代码腐化到无可救药的地步
重构是避免过度设计的有效手段
重构的对象:到底重构什么
根据重构的规模,我们可以笼统地分为大规模高层次重构(大型重构)和小规模低层次的重构(小型重构)
大型重构指的是对顶层代码设计的重构,包括:系统、模块、代码结构、类与类之间的关系等的重构,重构的手段有:分层、模块化、解耦、抽象可复用组件等等
小型重构指的是对代码细节的重构,主要是针对类、函数、变量等代码级别的重构,比如规范命名、规范注释、消除超大类或函数、提取重复代码等等。小型重构更多的是利用编码规范。
重构的时机:什么时候重构
提倡持续重构,我们一定要建立持续重构意识,把重构作为开发必不可少的部分
重构思路:小步快走
提高代码的可读性
提高代码的可测试性
编写完善的单元测试
所有重构完成之后添加注释
单元测试
单元测试相对于集成测试来说,测试的粒度更小一些。集成测试的测试对象是整个系统或者某个功能模块。单元测试的测试对象是类或者函数
为什么要写单元测试
单元测试能有效地帮你发现代码中的(低级)bug
写单元测试能帮你发现代码设计上的问题
单元测试是对集成测试的有力补充
写单元测试的过程本身就是代码重构的过程
常见的反模式
未决行为:代码的输出是随机或者说不确定的
全局变量:滥用全局变量让编写单元测试变得困难
静态方法:静态方法很难 mock
复杂继承:很难 mock 依赖的对象
高耦合代码
创建型设计模式
单例模式(Singleton)
意图:保证一个类仅有一个实例,并提供一个访问它的全局访问点
主要解决:一个全局使用的类频繁地创建与销毁
何时使用:当想控制实例数目,节省系统资源的时候
单例模式实现方式
饿汉模式
懒汉模式 + Synchronized同步锁 + double-check + volatile
内部类 + 饿汉模式
枚举类 + 饿汉模式:推荐方式
多例模式
多例模式,可以理解为同一类型的只能创建一个对象,不同类型的可以创建多个对象
多例模式有点类似工厂模式,不同之处在于多例模式创建的对象都是同一个类的对象,而工厂模式创建的是不同子类的对象
枚举类型也相当于多例模式,一个类型只能对应一个对象,一个类可以创建多个对象。
例子
java.lang.Runtime 类就是一个单例类
com.google.common.hash.BloomFilterStrategies 是一种多例模式
工厂模式(Factory)
一般情况下,工厂模式分为三种更加细分的类型:简单工厂、工厂方法和抽象工厂
简单工厂(Simple Factory)<br>
简单工厂模式的代码实现中,一般会有多处 if 分支判断逻辑。简单工厂模式可看作是工厂方法模式的一种特例
工厂方法(Factory Method)<br>
意图:定义一个创建对象的接口,让其子类自己决定实例化哪一个工厂类,工厂模式使其创建过程延迟到子类进行
主要解决:主要解决接口选择的问题
何时使用:我们明确地计划不同条件下创建不同实例时
关键代码:创建过程在其子类执行
当对象的创建逻辑比较复杂,不只是简单的 new 一下就可以,而是要组合其他类对象,做各种初始化操作的时候,推荐使用工厂方法模式,将复杂的创建逻辑拆分到多个工厂类中,让每个工厂类都不至于过于复杂。
抽象工厂(Abstract Factory)
意图:提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类
主要解决:主要解决接口选择的问题
何时使用:系统的产品有多于一个的产品族,而系统只消费其中某一族的产品
关键代码:在一个工厂里聚合多个同类产品。
让一个工厂负责创建多个不同类型的对象,而不是只创建一种对象
例子
java.util.Calendar#getInstance 方法是简单工厂的实现
建造者模式(Builder)
意图:将一个复杂的构建与其表示相分离,使得同样的构建过程可以创建不同的表示。
主要解决:主要解决在软件系统中,有时候面临着"一个复杂对象"的创建工作,其通常由各个部分的子对象用一定的算法构成;由于需求的变化,这个复杂对象的各个部分经常面临着剧烈的变化,但是将它们组合在一起的算法却相对稳定。
何时使用:一些基本部件不会变,而其组合经常变化的时候。
关键代码:建造者:创建和提供实例,导演:管理建造出来的实例的依赖关系。
例子
java.util.Calendar$Builder 内部类是建造者模式实现
原型模式(Prototype)
意图:用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。
主要解决:在运行期建立和删除原型。
何时使用:建立相应数目的原型并克隆它们可能比每次用合适的状态手工实例化该类更方便一些。
关键代码: 实现克隆操作,继承 Cloneable,重写 clone()。
原型模式原理:使用 clone 创建新的对象,就无需再通过 new 实例化来创建对象了。这是因为 Object 类的 clone 方法是一个本地方法,它可以直接操作内存中的二进制流,所以性能相对 new 实例化来说更佳
注意:Object 类的 clone() 方法执行的是浅拷贝。它只会拷贝对象中的基本数据类型的数据,以及引用对象的内存地址,不会递归地拷贝引用对象本身
结构型设计模式
代理模式(Proxy)<br>
意图:为其他对象提供一种代理以控制对这个对象的访问
主要解决:有些对象由于某些原因(比如对象创建开销很大,或者某些操作需要安全控制,或者需要进程外的访问),直接访问会给使用者或者系统结构带来很多麻烦,我们可以在访问此对象时加上一个对此对象的访问层。
关键代码:实现与被代理类组合
代理模式一般分为静态代理、动态代理,参考:https://juejin.im/post/5c1ca8df6fb9a049b347f55c
应用场景:在业务系统中开发一些非功能性需求,比如:监控、统计、鉴权、限流、事务、幂等、日志。
桥接模式(Bridge)
意图:将抽象部分与实现部分分离,使它们都可以独立的变化。通俗地解释:实现系统可能有多角度分类,每一种分类都有可能变化,那么就把这种多角度分离出来让他们独立变化,减少他们之间的耦合。桥接模式实际上是基于多用组合少用继承的设计原则。<br>
主要解决:在有多种可能会变化的情况下,用继承会造成类爆炸问题,扩展起来不灵活。
何时使用:实现系统可能有多个角度分类,每一种角度都可能变化。
关键代码:抽象类依赖实现类。
弄懂定义中“抽象”和“实现”两个概念,是理解它的关键。定义中的“抽象”,指的并非“抽象类”或“接口”,而是被抽象出来的一套“类库”,它只包含骨架代码,真正的业务逻辑需要委派给定义中的“实现”来完成。而定义中的“实现”,也并非“接口的实现类”,而是一套独立的“类库”。“抽象”和“实现”独立开发,通过对象之间的组合关系(桥接),组装在一起。
例子
以 JDBC 为例,JDBC 本身就相当于“抽象”。这种抽象是跟具体的数据库无关的、被抽象出来的一套“类库”。具体的 Driver(比如,com.mysql.jdbc.Driver)就相当于“实现”。这种实现是跟具体数据库相关的一套“类库”。JDBC 和 Driver 独立开发,通过对象之间的组合关系,组装在一起。JDBC 的所有逻辑操作,最终都委托给 Driver 来执行。<br>
装饰器模式(Decorator)
意图:动态地给一个对象添加一些额外的职责。就增加功能来说,装饰器模式相比生成子类更为灵活。
主要解决:一般的,我们为了扩展一个类经常使用继承方式实现,由于继承为类引入静态特征,并且随着扩展功能的增多,子类会很膨胀。
何时使用:在不想增加很多子类的情况下扩展类
关键代码: Component 类充当抽象角色,不应该具体实现。修饰类引用和继承 Component 类,具体扩展类重写父类方法
例子
InputStream 是一个抽象类,FileInputStream 是专门用来读取文件流的子类。BufferedInputStream 是一个支持带缓存功能的数据读取类,FileInputStream、BufferedInputStream 都间接继承 InputStream,BufferedInputStream 通过构造方法传入 InputStream,增强数据读取的功能,<br>
Collections 类是一个集合容器的工具类,提供了很多静态方法,用来创建各种集合容器,比如通过 unmodifiableColletion() 静态方法,来创建 UnmodifiableCollection 类对象。而这些容器类中的 UnmodifiableCollection 类、CheckedCollection、SynchronizedCollection 类等,就是针对 Collection 类的装饰器类。<br>以 UnmodifiableCollection 为例,UnmodifiableCollection 是 Collection 类的一个装饰器类。UnmodifiableCollection 的构造函数接收一个 Collection 类对象,然后对其所有的函数进行了包裹(Wrap):重新实现(比如 add() 函数)或者简单封装(比如 stream() 函数)。<br>
适配器模式(Adapter)<br>
意图:将一个类的接口转换成客户希望的另外一个接口。适配器模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。
主要解决:主要解决在软件系统中,常常要将一些"现存的对象"放到新的环境中,而新环境要求的接口是现对象不能满足的。
原理:这个模式就是用来做适配的,它将不兼容的接口转换为可兼容的接口,让原本由于接口不兼容而不能一起工作的类可以一起工作。比如 USB 转接头充当适配器,把两种不兼容的接口,通过转接变得可以一起工作。<br>
关键代码:适配器模式有两种实现方式:类适配器和对象适配器。其中,类适配器使用继承关系来实现,对象适配器使用组合关系来实现。适配器继承或依赖已有的对象,实现想要的目标接口。
应用场景
封装有缺陷的接口设计
统一多个类的接口设计
替换依赖的外部系统
兼容老版本接口
在做版本升级的时候,对于一些要废弃的接口,我们不直接将其删除,而是暂时保留,并且标注为 deprecated,并将内部实现逻辑委托为新的接口实现。
例子
Slf4j 提供了一套打印日志的统一接口规范,还提供了针对不同日志框架(log4j、logback、JUL)的适配器,对不同日志框架的接口进行二次封装,适配成统一的 Slf4j 接口定义。具体使用哪种日志框架实现,是通过 Java 的 SPI 技术动态地指定的。<br>
老版本的 JDK 提供了 Enumeration 类来遍历容器。新版本的 JDK 用 Iterator 类替代 Enumeration 类来遍历容器。Collections 类中,仍然保留了 enumaration() 静态方法,enumaration 方法内使用 Iterator 来遍历。
门面模式(Facade)
意图:也叫外观模式,为子系统中的一组接口提供一个一致的界面,门面模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。<br>
主要解决:降低访问复杂系统的内部子系统时的复杂度,简化客户端与之的接口。
关键代码:在客户端和复杂系统之间再加一层,这一层将调用顺序、依赖关系等处理好。
原理:从隐藏实现复杂性,提供更易用接口这个意图来看,门面模式有点类似于迪米特法则(最少知识原则)和接口隔离原则:两个有交互的系统,只暴露有限的必要的接口。除此之外,门面模式还有点类似 封装、抽象的设计思想,提供更抽象的接口,封装底层实现细节。
应用场景
解决易用性问题
门面模式可以用来封装系统的底层实现,隐藏系统的复杂性,提供一组更加简单易用、更高层的接口。
解决性能问题
通过将多个接口调用替换为一个门面接口调用,减少网络通信成本,提高 App 客户端的响应速度。<br>
享元模式(Flyweight)
意图:运用共享技术有效地支持大量细粒度的对象。
主要解决:在有大量对象时,有可能会造成内存溢出,我们把其中共同的部分抽象出来,如果有相同的业务请求,直接返回在内存中已有的对象,避免重新创建。
如何解决:用唯一标识码判断,如果在内存中有,则返回这个唯一标识码所标识的对象。
关键代码:用 HashMap 存储这些对象。
享元模式跟单例的区别
在单例模式中,一个类只能创建一个对象,而在享元模式中,一个类可以创建多个对象,每个对象被多处代码引用共享。
享元模式跟对象池的区别
对象池、连接池、线程池、享元模式都是为了复用,池化技术中的“复用”可以理解为“重复使用”,主要目的是节省时间(比如从数据库池中取一个连接,不需要重新创建)。在任意时刻,每一个对象、连接、线程,并不会被多处使用,而是被一个使用者独占,当使用完成之后,放回到池中,再由其他使用者重复利用。享元模式中的“复用”可以理解为“共享使用”,在整个生命周期中,都是被所有使用者共享的,主要目的是节省空间。
例子
Integer 的 IntegerCache 相当于生成享元对象的工厂类,IntegerCache 缓存了最常用的整数对象。除了 Integer 类型之外,其他包装器类型,比如 Long、Short、Byte 等,也都利用了享元模式来缓存 -128 到 127 之间的数据。
组合模式(Composite)
意图:将对象组合成树形结构以表示"部分-整体"的层次结构。组合模式使得用户对单个对象和组合对象的使用具有一致性。
主要解决:它在树型结构的问题中,模糊了简单元素和复杂元素的概念,客户程序可以像处理简单元素一样来处理复杂元素,从而使得客户程序与复杂元素的内部结构解耦。
如何解决:树枝和叶子实现统一接口,树枝内部组合该接口。
关键代码:树枝内部组合该接口,并且含有内部属性 List,里面放 Component。
原理:组合模式和面向对象设计中的“组合关系”,完全是两码事。组合模式 主要是用来处理树形结构数据。这里的“数据”,可以简单理解为一组对象集合。数据必须能表示成树形结构,比如文件夹文件关系,部门子部门关系。<br>
各模式间的区别联系
代理、桥接、装饰器、适配器 4 种设计模式的区别
代理、桥接、装饰器、适配器,它们的代码结构非常相似。它们都可以称为 Wrapper 模式,也就是通过 Wrapper 类二次封装原始类。
代理模式:代理模式在不改变原始类接口的条件下,为原始类定义一个代理类,主要目的是控制访问,而非加强功能,这是它跟装饰器模式最大的不同。
桥接模式:桥接模式的目的是将接口部分和实现部分分离,从而让它们可以较为容易、也相对独立地加以改变。
装饰器模式:装饰者模式在不改变原始类接口的情况下,对原始类功能进行增强,并且支持多个装饰器的嵌套使用。
适配器模式:适配器模式是一种事后的补救策略。适配器提供跟原始类不同的接口,而代理模式、装饰器模式提供的都是跟原始类相同的接口。
适配器模式与门面模式的区别<br>
适配器模式做接口转换,解决的是原接口和目标接口不匹配的问题。
门面模式做接口整合,解决的是多接口调用带来的问题,降低使用复杂度。
行为型设计模式
观察者模式(Observer)
意图:定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。
主要解决:一个对象状态改变给其他对象通知的问题,而且要考虑到易用和低耦合,保证高度的协作
关键代码:在抽象类里有一个 ArrayList 存放观察者们。
实现原理:观察者模式也称为发布订阅模式,在对象之间定义一个一对多的依赖,当一个对象状态改变的时候,所有依赖的对象都会自动收到通知。一般情况下,被依赖的对象叫作被观察者(Observable),依赖的对象叫作观察者(Observer)。根据应用场景的不同,观察者模式会对应不同的代码实现方式:有同步阻塞的实现方式,也有异步非阻塞的实现方式;有进程内的实现方式,也有跨进程的实现方式。
实现异步观察者模式<br>
Google Guava EventBus 是一个比较著名的 EventBus 框架,它不仅仅支持异步非阻塞模式,同时也支持同步阻塞模式。
Java JDK 也提供了观察者模式的简单框架实现。它比 EventBus 要简单,只包含两个类:java.util.Observable 和 java.util.Observer。前者是被观察者,后者是观察者。
模板模式(TemplateMethod)
意图:定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。
主要解决:一些方法通用,却在每一个子类都重新写了这一方法。
原理:模板方法模式在一个方法中定义一个算法骨架,并将某些步骤推迟到子类中实现。模板方法模式可以让子类在不改变算法整体结构的情况下,重新定义算法中的某些步骤。
Callback回调函数<br>
回调也能起到跟模板模式相同的作用,回调是一种双向调用关系。简单来说,A类某个方法是固定的代码结构,某个步骤会调用一个注册的函数,外部调用这个方法时,传入定制的回调函数即可。
回调可以分为同步回调和异步回调(延迟回调)。从应用场景上来看,同步回调看起来更像模板模式,异步回调看起来更像观察者模式。
同步回调例子:Spring 提供了很多 Template 类,比如,JdbcTemplate、RedisTemplate、RestTemplate。尽管都叫作 xxxTemplate,但它们并非基于模板模式来实现的,而是基于回调来实现的,确切地说应该是同步回调。
异步回调例子:JVM 提供了 Runtime.addShutdownHook(Thread hook) 方法,可以注册一个 JVM 关闭的 Hook。当应用程序关闭的时候,JVM 会自动调用 Hook 代码。在注册完 Hook 之后,并不需要等待 Hook 执行完成,这算是一种异步回调。
模板模式 VS 回调
从应用场景上来看,同步回调跟模板模式几乎一致。它们都是在一个大的算法骨架中,自由替换其中的某个步骤,起到代码复用和扩展的目的。而异步回调跟模板模式有较大差别,更像是观察者模式。
从代码实现上来看,回调和模板模式完全不同。回调基于组合关系来实现,把一个对象传递给另一个对象,是一种对象之间的关系;模板模式基于继承关系来实现,子类重写父类的抽象方法,是一种类之间的关系。
在代码实现上,回调相对于模板模式会更加灵活
策略模式(Strategy)
意图:定义一系列的算法,把它们一个个封装起来,并且使它们可相互替换。
主要解决:在有多种算法相似的情况下,使用 if...else 所带来的复杂和难以维护。
关键代码:实现同一个接口,将这些算法封装成一个一个的类,任意地替换。
原理:策略模式解耦的是策略的定义、创建、使用这三部分。策略类的定义包含一个策略接口和一组实现这个接口的策略类。策略类的创建一般会有一个工厂类封装创建逻辑,根据策略的类型返回具体的策略对象。策略类的使用一般是运行时动态确定使用哪种策略。
职责链模式(Chain of Responsibility)
意图:避免请求发送者与接收者耦合在一起,让多个对象都有可能接收请求,将这些对象连接成一条链,并且沿着这条链传递请求,直到有对象处理它为止。
主要解决:职责链上的处理者负责处理请求,客户只需要将请求发送到职责链上即可,无须关心请求的处理细节和请求的传递,所以职责链将请求的发送者和请求的处理者解耦了。
职责链模式实现方式
第一种实现方式,Handler 是所有处理器类的抽象父类,handle() 是抽象方法。每个具体的处理器类的 handle() 函数的代码结构类似,如果它能处理该请求,就不继续往下传递;如果不能处理,则交由后面的处理器来处理。HandlerChain 是处理器链,从数据结构的角度来看,它就是一个记录了链头、链尾的链表。其中,记录链尾是为了方便添加处理器。
第二种实现方式,HandlerChain 类用数组而非链表来保存所有的处理器,并且需要在 HandlerChain 的 handle() 函数中,for 循环依次调用每个处理器的 handle() 函数。
职责链模式还有一种变体,那就是请求会被所有的处理器都处理一遍,不存在中途终止的情况。
例子
javax.servlet.Filter 就是处理器接口,FilterChain 就是处理器链。
Spring Interceptor 也是基于职责链模式实现的。HandlerExecutionChain 类是职责链模式中的处理器链。
Spring scurity 通过 HttpSecurity 配置构建一个过滤器链来处理特定的请求。FilterChainProxy 包含了一组过滤器链,在 doFilter 内构建了 VirtualFilterChain 来处理请求,其 doFilter 实际上也是递归调用。
状态模式(State)<br>
意图:允许对象在内部状态发生改变时改变它的行为,对象看起来好像修改了它的类。
主要解决:对象的行为依赖于它的状态(属性),并且可以根据它的状态改变而改变它的相关行为。
关键代码:状态模式一般和对象的状态有关。实现类的方法有不同的功能,覆盖接口中的方法。状态模式和命令模式一样,也可以用于消除 if...else 等条件选择语句。
状态机的实现<br>
状态模式一般用来实现状态机,状态机常用在游戏、工作流引擎等系统开发中。状态机的实现方式有多种,除了状态模式,比较常用的还有分支逻辑法和查表法。
状态机有 3 个组成部分:状态(State)、事件(Event)、动作(Action)。事件也称为转移条件,事件触发状态的转移及动作的执行。动作不是必须的,也可能只转移状态,不执行任何动作。
状态机实现方式一:分支逻辑法
最简单直接的实现方式是,参照状态转移图,将每一个状态转移,原模原样地直译成代码。这样编写的代码会包含大量的 if-else 或 switch-case 分支判断逻辑,甚至是嵌套的分支判断逻辑。
状态机实现方式二:查表法
状态机还可以用二维表来表示,第一维表示当前状态,第二维表示事件,值表示当前状态经过事件之后,转移到的新状态及执行的动作。<br>
如果要执行的动作是一系列复杂的逻辑操作,就没法用简单的二维数组来表示了,查表法的实现方式有一定局限性。
状态机实现方式三:状态模式
状态模式通过将事件触发的状态转移和动作执行,拆分到不同的状态类中,来避免分支判断逻辑。<br>
应用场景
像游戏这种比较复杂的状态机,包含的状态比较多,优先推荐使用查表法
像电商下单、外卖下单这种类型的状态机,它们的状态并不多,状态转移也比较简单,但事件触发执行的动作包含的业务逻辑可能会比较复杂,更加推荐使用状态模式来实现
迭代器模式(Iterator)
意图:迭代器模式也叫作游标模式。提供一种方法顺序访问一个聚合对象中各个元素,而又无须暴露该对象的内部表示。
主要解决:不同的方式来遍历整个整合对象。
关键代码:把在元素之间游走的责任交给迭代器,而不是聚合对象,定义接口:hasNext, next。
原理:迭代器模式将集合对象的遍历操作从集合类中拆分出来,放到迭代器类中,让两者的职责更加单一。迭代器是用来遍历容器的,所以,一个完整的迭代器模式一般会涉及容器和容器迭代器两部分内容。
访问者模式(Visitor)
意图:主要将数据结构与数据操作分离。
主要解决:稳定的数据结构和易变的操作耦合问题。
关键代码:在被访问的类里面加一个对外提供接待访问者的接口,在数据基础类里面有一个方法接受访问者,将自身引用传入访问者。
原理:访问者者模式允许一个或者多个操作应用到一组对象上,解耦操作和对象本身。<br>
例子
在 jsqlparser 中有一个表达式解析引擎,同时应用了解释器模式和访问者模式,Expression 是对象主体,使用访问者模式将操作对象和操作指令分离。ExpressionVisitor 中根据不同的数据类型或表达式定义了众多的动作。各个不同的表达式都实现了 Expression 的 visit 接口。
备忘录模式(Memento)
意图:在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。
主要解决:所谓备忘录模式就是在不破坏封装的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,这样可以在以后将对象恢复到原先保存的状态。
关键代码:通过一个备忘录类专门存储对象状态,客户不与备忘录类耦合,与备忘录管理类耦合。
备忘录模式应用场景比较明确和有限,主要是用来防丢失、撤销、恢复等。
命令模式(Command)
意图:将一个请求封装成一个对象,从而使您可以用不同的请求对客户进行参数化。
主要解决:在软件系统中,行为请求者与行为实现者通常是一种紧耦合的关系,但某些场合,比如需要对行为进行记录、撤销或重做、事务等处理时,这种无法抵御变化的紧耦合的设计就不太合适。
关键代码:通过调用者调用接受者执行命令,顺序:调用者→接受者→命令。 定义三个角色:1、received 真正的命令执行对象 2、Command 3、invoker 使用命令对象的入口
原理:命令模式最核心的实现手段,是将函数封装成对象,从实现的角度来说,它类似于回调。当我们把函数封装成对象之后,对象就可以存储下来,方便控制执行。
命令模式的主要作用和应用场景,是用来控制命令的执行,比如,异步、延迟、排队执行命令、撤销重做命令、存储命令、给命令记录日志等等
解释器模式(Interpreter)
意图:给定一个语言,定义它的文法表示,并定义一个解释器,这个解释器使用该标识来解释语言中的句子。
主要解决:对于一些固定文法构建一个解释句子的解释器。
关键代码:构建语法树,定义终结符与非终结符。 构建环境类,包含解释器之外的一些全局信息,一般是 HashMap。
原理:它用来描述如何构建一个简单的“语言”解释器,解释器模式只在一些特定的领域会被用到,比如编译器、规则引擎、正则表达式。它的代码实现的核心思想,就是将语法解析的工作拆分到各个小类中,以此来避免大而全的解析类。一般的做法是,将语法规则拆分成一些小的独立的单元,然后对每个单元进行解析,最终合并为对整个语法规则的解析。
例子
常见的应用有 Ognl 的表达式解析、Spel 表达式解析、SQL 解析引擎等。
中介者模式(Mediator)
意图:用一个中介对象来封装一系列的对象交互,中介者使各对象不需要显式地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互。
主要解决:对象与对象之间存在大量的关联关系,这样势必会导致系统的结构变得很复杂,同时若一个对象发生改变,我们也需要跟踪与之相关联的对象,同时做出相应的处理。
何时使用:多个类相互耦合,形成了网状结构。
关键代码:将网状结构分离为星型结构,对象之间的通信封装到一个类中单独处理。
原理:中介模式定义了一个单独的(中介)对象,来封装一组对象之间的交互。将这组对象之间的交互委派给与中介对象交互,来避免对象之间的直接交互。<br>
开源框架中的设计模式
Google Guava 类库
Builder 模式
构建一个缓存,需要配置 n 多参数,比如过期时间、淘汰策略、最大缓存大小等等。CacheBuilder 基于建造者模式提供了很多构建个性化参数的方法,并在真正构造 Cache 对象的时候,做一些必要的参数校验。<br>
Wrapper 模式
在 Google Guava 的 collect 包路径下,有一组以 Forwarding 开头命名的类。Forwarding 类实际上是一个“默认 Wrapper 类”或者叫“缺省 Wrapper 类”。如果让代理类直接实现 Collection 接口,那 Collection 接口中的所有方法,都要在子类中实现一遍。为了简化 Wrapper 模式的代码实现,Guava 提供一系列缺省的 Forwarding 类。用户在实现自己的 Wrapper 类的时候,基于缺省的 Forwarding 类来扩展,就可以只实现自己关心的方法,其他不关心的方法使用缺省 Forwarding 类的实现。
Immutable 模式
Immutable 模式,叫不变模式,因为数据不变,所以不存在并发读写问题,因此不变模式常用在多线程环境下,来避免线程加锁。所以,不变模式也常被归类为多线程设计模式。
不变模式可以分为两类,一类是普通不变模式,另一类是深度不变模式。普通的不变模式指的是,对象中包含的引用对象是可以改变的。通常我们所说的不变模式,指的就是普通的不变模式。深度不变模式指的是,对象包含的引用对象也不可变。它们两个之间的关系,有点类似浅拷贝和深拷贝之间的关系。
Google Guava 针对集合类(Collection、List、Set、Map…)提供了对应的不变集合类(ImmutableCollection、ImmutableList、ImmutableSet、ImmutableMap…)。Google Guava 提供的不变集合类属于普通不变模式,也就是说,集合中的对象不会增删,但是对象的成员变量(或叫属性值)是可以改变的。
JDK 也提供了不变集合类,通过 Collections.unmodifiableXxx 可以包装一个不变集合。区别在于,原始集合继续添加元素后,会影响 Collections 包装的不变集合,而不会影响 Guava 的不变集合。这是因为 Collections 的不变集合只是包装了原始集合,而 Guava 的不变集合内部是包装的一个新的数组对象。
Spring 框架
观察者模式
Spring 提供了观察者模式的实现框架,Spring 中实现的观察者模式包含三部分:Event 事件(相当于消息)、Listener 监听者(相当于观察者)、Publisher 发送者(相当于被观察者)。
框架使用起来并不复杂,主要包含三部分工作:定义一个继承 ApplicationEvent 的事件;定义一个实现了 ApplicationListener 的监听器;定义一个发送者,发送者调用 ApplicationContext 来发送事件消息。
模板模式
利用模板模式,Spring 能让用户定制 Bean 的创建过程。对象的初始化可以在类中自定义一个 init-method 函数或者实现 Initializingbean 接口;对象的销毁可以定义 destroy-method 函数或实现 DisposableBean 接口;Bean 初始化前置操作和后置操作可以实现 BeanPostProcessor 接口。<br>
BeanFactory 创建 bean 的过程体现了模板方法的模式,createBean 方法定义了很多 protected 的方法,不同的子类可以定制化不同的逻辑。<br>
在 Spring 中,只要后缀带有 Template 的类,基本上都是模板类,而且大部分都是用 Callback 回调来实现的,比如 JdbcTemplate、RedisTemplate 等。
适配器模式
在 Spring MVC 中,定义一个 Controller 的方式有:通过@Controller、@RequestMapping来定义;实现Controller接口;实现Servlet接口。不同方式定义的 Controller,其函数的定义(函数名、入参、返回值等)是不统一的。<br>
在应用启动的时候,Spring 容器会加载这些 Controller 类,并且解析出 URL 对应的处理函数,封装成 Handler 对象,存储到 HandlerMapping 对象中。当有请求到来的时候,DispatcherServlet 从 HanderMapping 中,查找请求 URL 对应的 Handler,然后调用执行 Handler 对应的函数代码,最后将执行结果返回给客户端。
DispatcherServlet 则利用了适配器模式避免 if-else 分支判断逻辑,统一了多个类的接口设计,让其满足开闭原则。Spring 定义了统一的接口 HandlerAdapter,并且对每种 Controller 定义了对应的适配器类。<br>
策略模式
Spring 支持两种动态代理实现方式,一种是 JDK 提供的动态代理实现方式,另一种是 Cglib 提供的动态代理实现方式。针对不同的被代理类,Spring 会在运行时动态地选择不同的动态代理实现方式。这个应用场景实际上就是策略模式。<br>
策略模式包含三部分,策略的定义、创建和使用。策略的定义:AopProxy 是策略接口,JdkDynamicAopProxy、CglibAopProxy 是两个实现了 AopProxy 接口的策略类。策略的创建:AopProxyFactory 是一个工厂类接口,DefaultAopProxyFactory 是一个默认的工厂类,用来创建 AopProxy 对象。策略的使用就是利用工厂动态生成的动态代理策略类来创建代理对象。<br>
组合模式
Spring 提供的缓存管理功能,CacheManager 的实现用到了组合模式。EhCacheManager、SimpleCacheManager、RedisCacheManager 等表示叶子节点,CompositeCacheManager 表示中间节点。getCache()、getCacheNames() 两个函数的实现都用到了递归。<br>
类似的,Spring security 的 TokenGranter 也用到了组合模式,TokenGranter 定义了获取 OAuth2AccessToken 的接口,CompositeTokenGranter 表示中间节点,其它的 ClientCredentialsTokenGranter、RefreshTokenGranter 等表示叶子节点。<br>
装饰器模式
为了将缓存的写操作和数据库的写操作,放到同一个事务中,要么都成功,要么都失败,Spring 的 TransactionAwareCacheDecorator 增加了对事务的支持,在事务提交、回滚的时候分别对 Cache 的数据进行处理。TransactionAwareCacheDecorator 实现 Cache 接口,并且将所有的操作都委托给 targetCache 来实现,对其中的写操作添加了事务功能。
解释器模式
SpEL,全称叫 Spring Expression Language,是 Spring 中常用来编写配置的表达式语言。它定义了一系列的语法规则。我们只要按照这些语法规则来编写表达式,Spring 就能解析出表达式的含义。这就是解释器模式的典型应用场景。
职责链模式
拦截器 Interceptor
代理模式
AOP
MyBatis 框架
职责链+代理模式
MyBatis Plugin 的代码实现是借助动态代理来实现职责链的。职责链模式的实现一般包含处理器(Handler)和处理器链(HandlerChain)两部分。对应到 MyBatis Plugin 就是 Interceptor 和 InterceptorChain。除此之外,MyBatis Plugin 还包含另外一个非常重要的类:Plugin,它用来生成被拦截对象的动态代理。<br>
InterceptorChain 的 pluginAll 方法会调用每个 Interceptor 的 plugin() 方法,这里就利用了职责链模式。
plugin() 方法通过直接调用 Plugin 的 wrap() 方法来实现。Plugin 是借助 Java InvocationHandler 实现的动态代理类,用来代理给 target 对象添加 Interceptor 功能。
MyBatis 中的职责链模式的实现方式比较特殊,它对同一个目标对象嵌套多次代理。每个代理对象(Plugin 对象)代理一个拦截器(Interceptor 对象)功能。当执行 Executor、StatementHandler、ParameterHandler、ResultSetHandler 这四个类上的某个方法的时候,MyBatis 会嵌套执行每层代理对象(Plugin 对象)上的 invoke() 方法。而 invoke() 方法会先执行代理对象中的 interceptor 的 intecept() 函数,然后再执行被代理对象上的方法。就这样,一层一层地把代理对象上的 intercept() 函数执行完之后,MyBatis 才最终执行那 4 个原始类对象上的方法。通过职责链模式+代理模式形成了一个 intercepter 的递归调用。<br>
模板模式
SqlSession 执行 SQL 的业务逻辑,都是委托给了 Executor 来实现。其中,Executor 本身是一个接口;BaseExecutor 是一个抽象类,实现了 Executor 接口;而 BatchExecutor、SimpleExecutor、ReuseExecutor 三个类继承 BaseExecutor 抽象类。BaseExecutor 实现了 Executor 的接口,定义了执行SQL的算法骨架,然后定义了很多模板方法,由子类具体实现。<br>
解释器模式
MyBais 支持配置文件中编写动态 SQL,就是在 SQL 中可以包含在 trim、if、#{}等语法标签,在运行时根据条件来生成不同的 SQL。动态 SQL 的语法规则是 MyBatis 自定义的,因此mybatis还实现了对应的解释器。<br>
解释器模式在解释语法规则的时候,一般会把规则分割成小的单元。MyBatis 把每个语法小单元叫 SqlNode,对于不同的语法小单元,MyBatis 定义不同的 SqlNode 实现类。
MixedSqlNode 起着职责链的作用,通过一个职责链来解析SQL语法树,最终形成一个可执行的SQL。
线程唯一的单例模式
在 MyBatis 中,ErrorContext 这个类是标准单例的变形:线程唯一的单例。它基于 Java 中的 ThreadLocal 来实现。
装饰器模式
在 MyBatis 中,缓存功能由接口 Cache 定义。PerpetualCache 类是最基础的缓存类,是一个大小无限的缓存。除此之外,MyBatis 还设计了 9 个包裹 PerpetualCache 类的装饰器类,用来实现功能增强。
迭代器模式
迭代器模式常用来替代 for 循环遍历集合元素。Mybatis 的 PropertyTokenizer 类实现了 Java Iterator 接口,是一个迭代器,用来对配置属性进行解析。PropertyTokenizer 并非标准的迭代器类。它将配置的解析、解析之后的元素、迭代器,这三部分本该放到三个类中的代码,都耦合在一个类中。不过,这样做的好处是能够做到惰性解析。我们不需要事先将整个配置,解析成多个 PropertyTokenizer 对象。只有当我们在调用 next() 函数的时候,才会解析其中部分配置。
适配器模式
MyBatis 并没有直接使用 Slf4j 提供的统一日志规范,而是定义了一套自己的日志访问接口。针对 Log 接口,MyBatis 还提供了各种不同的实现类,分别使用不同的日志框架来实现 Log 接口,这里就用到了适配器模式。
项目实战
项目一般都会经历 分析、设计、实现 三个阶段
分析 跟面向对象分析很相似,都是做需求的梳理,产出需求描述
设计 指的是系统设计,主要是划分模块,对模块进行设计
实现 实际上等于面向对象设计加实现,主要聚焦在代码层面,产出的是类的设计和实现。
需求分析
项目背景
需求背景
需求分析
功能性需求分析
项目要实现的功能
如何使用
非空能行需求分析
易用性
方面集成使用,低侵入、松耦合
扩展性
性能
容错性
低侵入、松耦合
系统设计
分析功能点,划分功能模块
通过合理的设计,完成功能性需求的同时,满足非功能性需求
关注项目重点是功能性需求还是非功能性需求,并做重点分析设计
代码实现
先实现一个包含核心功能、基本功能的 V1 版本,小步快跑、逐步迭代
先实现最小原型代码,不用过于考虑类的设计、代码质量等,再做优化调整
CodeView,结合设计思想、原则、模式、编码规范等审查代码质量
优化代码,满足代码质量标准,利用一些设计模式、编程思想优化代码结构,使其易扩展
收藏
立即使用
Collect
Get Started
Collect
Get Started
Collect
Get Started
Collect
Get Started
评论
0 条评论
下一页