设计模式感悟
- 简介
- 简述6大设计原则
- 简述23种设计模式
- 总结
简介
本文章是设计模式的个人笔记和感悟,水平有限,敬请谅解。
简述6大设计原则
单一职责原则(Single Responsibility Principle)
定义
A class should have a single responsibility, where a responsibility is nothing but a reason to change.
利
- 类的复杂性降低,代码可读性高。
- 变更风险低,代码维护性高。
- 提高代码的扩展性。
- 本质是接口单一职责原则,软件项目里几乎不可能实施类单一职责原则。
解释4,某些语言(Java)不支持多继承,实施类单一职责原则意味着放弃继承,只能组合扩展。
弊
- 需要准确区分接口的责任,在时间仓促或者行业经验不足的情况下难以实现。
实施
- 一个接口里面只定义单一责任的方法,类实现多个接口完成业务开发。
里氏替换原则(Liskov Substitution Principle)
定义
In a computer program, if S is a subtype of T, then objects of type T may be replaced with objects of type S (i.e. an object of type T may be substituted with any object of a subtype S) without altering any of the desirable properties of the program (correctness, task performed, etc.)
从定义可以看出,里氏替换原则是针对并限制继承,是弥补继承的弊。这里分别简述继承和里氏替换原则的利弊。
继承的利
- 代码复用性强。
- 子类通过重写父类方法,较好的扩展性。
- 代码维护性高。
- 多态的必要条件之一。
继承的弊
- 父与子类紧密耦合,子类依赖父类,缺乏独立性。
- 子类通过重写父类方法,破坏了父类方法行为,可能造成意想不到的后果。
解释2,如果代码中子类B继承父类A且重写了方法C,那么句柄类型A引用了B类或者A类的实例,调用C方法则可能会产生不一样的行为。
里氏替换原则的利
- 解决了继承的弊2,不重写或者重写而不改变父类方法行为,保证子类替换标准。
里氏替换原则的弊
- 限制继承的扩展性。
实施
- 子类重写父类方法,可以增加增强某种不影响父类方法的功能,例如改变子类自身字段。
- 不通过继承,而用组合方式依赖调用某个类的方法。
- 父类是抽象类或者接口,子类继承或实现。
- 父类通过模板设计模式,抽离公共部分,留下hook为子类重写。
重写hook方法虽然可能改变了父类方法的行为,但这是父类方法在设计之初就鼓励子类重写展现个性,并不违背里氏替换原则。
依赖倒置原则(Dependency Inversion Principle)
定义
High level modules should not depend upon low level modules.Both should depend upon abstractions.Abstractions should not depend upon details.Details should depend upon abstractions.
即面向接口编程。
利
- 通过接口编程,高层模块与底层模块间解耦。
- 配合注入(构造,setter,接口)底层类,并通过控制反转提高开发速度。
弊
- 然而并没有!
实施
- 模块或者类之间实现接口或者继承抽象类,方法依赖接口或者抽象类即可。
接口分离原则(Interface Segregation Principle)
定义
Many client specific interfaces are better than one general purpose interface.
算是接口单一职责原则细化版本。若每一个方法都实现每一个接口则会造成接口太多而难以维护。函数式编程就是绝对服从接口分离原则,每个接口只有一个方法。大部分情况和单一责任原则大同小异,不具体展开讨论了。
迪米特原则(Law of Demeter)
定义
You only ask for objects which you directly need.
即减少类与类间的耦合。
利
- 降低类与类之间的耦合。
- 代码维护性高。
弊
- 与类有依赖,需要一定的组合能力和封装的能力。
实施
- 在充分理解业务的前提下,适当组合和封装类。否则,应该先违反迪米特原则而业务优先,最终重构。
开闭原则(Open Close Principle)
定义
Software entities like classes,modules and functions should be open for extension but closed for modifications.
本质就是不要去修改,而通过继承或者组合扩展实体。
这里的大前提是业务发展,而不是针对漏洞。
利
- 单元测试可不变,代码可靠性高。
- 代码复用性强。
- 代码维护性高。
弊
- 若业务架构不适,随着业务发展,最终可能需要重构整个业务线,风险滞后而已。
实施
- 若是业务开发,严格遵循开闭原则。若是架构师,辩证看待开闭原则。
简述23种设计模式
- 为了方便,以下uml图不区分接口和抽象类,统一用抽象类表示。
- 默认情况下,认为设计模式符合6大设计原则,除非特别声明。
- 默认接口下不添加多余的方法,尽量认为满足接口分离原则。
- 因设计模式较多,利弊可以根据设计原则自行推导。
单例模式
违背设计原则
- 单例更像是面向过程而非面向对象的模式,没有接口,基本不符合6大设计原则。
使用场景
- 类是贫血对象。
- 可复用,如IO等资源。
工厂方法模式
使用场景
- 大部分框架均可使用,是较为简单使用的设计模式。
- 一般会和语言特性(Java的反射)或者代理设计模式配合。
抽象工厂模式
只是在工厂方法模式基础上,在工厂接口添加不同产品线的生产方法。
违背设计原则
- 典型工厂x产品(m x n)组合,可以直接扩展工厂,不可以直接扩展产品,需要添加抽象工厂的方法,违背开闭原则。
使用场景
- 若是m x n组合可以考虑使用,否则使用工厂方法模式。
建造者模式
导演类包含了一个或者多个建造者,通过construct返回具体的产品。
违背设计原则
- 类似抽象工厂,同样是m x n组合问题,建造者难以添加新部位,违背开闭原则。
使用场景
- 从设计原则分析可知和抽象工厂模式类似,应用差别在于建造者模式适合在产品由零散而又相似部件构成,细化产品的控制。
- 非常适合部件组成固定,种类繁多的场景,可以组合出m x n的扩展。
模板模式
非常经典的设计模式,只要语言有继承,就能运用此模式。
使用场景
- 大部分框架均可使用,父类建立模板留下hook为子类扩展。
代理模式
通过代理对象访问真正的对象,代理对象具有决定控制权。
使用场景
- 用作静态代理,如防火墙。
- 用作动态代理,如切面编程Aspect-oriented programming(AOP)。
原型模式
使用场景
- 和语言融合一体(Java实现Cloneable),暂时只想到大对象的拷贝场景。
中介者模式
同事类间的调用都通过中介类,以中介类为中心程星形拓扑。中介类代码量会非常大。
违背设计原则
- 中介类责任不单一,违背单一职责原则。
- 中介接口臃肿,违背接口分离原则。
- 所有同事类的业务都和中介类耦合,更改同事类的方法时候,中介类只能跟着修改。违背开闭原则。
使用场景
- 适用于复杂关系的单体应用。随着分布式的发展,不建议使用此设计模式,重构将是一个灾难。
命令模式
命令模式解耦调用方和接收方,接受方是否接收,何时接收到命令对于调用方是透明的。
使用场景
- Java中的回调方法,开辟新线程runnable。
- 复合多个命令。
- 和函数式编程完美结合。
责任链模式
每一个链查看是否有自身责任对应的事件,并且顺序可控。
使用场景
- 在大部分框架内都会使用责任链模式用作过滤器处理事件,有非常强的扩展能力。
装饰模式
有点类似代理模式,代理类具有控制权,如拒绝访问被代理对象,而装饰类只是增强被装饰类。
也有点类似责任链模式,责任类只是按照责任调用,而装饰类可多重装饰,并且递归调用方法形成多重增强。对调用者透明,顺序难以保证。
使用场景
- 通过uml得知,对被装饰对象同时继承并包含,弥补因继承扩展而导致链过长。
- 扩展能力极强,可以在业务需求变更上通过装饰快速完成迭代而不影响原有耦合的业务。
策略模式
从uml图看上去,和命令模式很相似,只是少了一个接收者。命令模式是两个对象的解耦,即调用者和接收者。调用者根本不知道命令会被接收者如何执行,到底是抛弃还是延期执行。 而策略模式是把责任交给了调用者,调用者必须知道这个策略的算法产生的作用。
使用场景
- 若if else过多,Java可以通过匿名内部类或者lambada表达式配合switch enum抽离出策略。
适配者模式
适配者模式有两种模式,一种是通过继承也就是类适配,一种是通过包含对象也就是对象适配。部分语言无法多继承,建议使用对象适配,可适配多对象。以上uml为后者。
使用场景
- 一般用于和第三方接口的兼容。
- 维护老项目。
迭代器模式
使用场景
基本所有面向对象语言都在语法糖实现了,现在可以无视这个模式。
组合模式
组合模式有两种模式,一种是安全模式,接口只定义公共方法,容器类实现并扩展自身类,叶类直接实现。一种是透明模式,接口定义容器类和叶类的并集方法,容器类和叶类分别实现,叶类存在空实现或者抛异常。以上uml为后者。
违背设计原则
- 安全模式下,接口要依赖具体的类才能作出具体的操作,违背了依赖导致原则。
- 无论安全模式还是透明模式,难以在接口添加新的方法做为扩展,违背了开闭原则。
使用场景
- 一般在UI种类繁多,并且具有UI嵌套的情况。
- 在集合框架内可以考虑使用。
观察者模式
目标依赖于一个或者多个观察者,当目标活动的时候,通知观察者,观察者作出行动。解耦目标与观察者的行动。
使用场景
- 大部分框架都使用观察者模式,特别适合使用在监听端口并通知观察者。
- 通过匿名内部类或者lambada表达式配合可减少具体的观察者实现类。
门面模式
门面模式使多个模块联合一个接口对外,减少模块与模块的依赖。和中介模式本质一样,一个对外,一个对内。
违背设计原则
- 门面类责任不单一,违背单一职责原则。
- 内部系统和门面类耦合,更改系统类的方法时候,门面类只能跟着修改。违背开闭原则。
- 门面模式没有接口这个概念,谈不上里氏替换和接口分离原则的设计原则
使用场景
- 可运用在分布式通过API暴露在外部供外部系统调用,随着分布式和微服务的流行,可以取代中介模式。
备忘录模式
发起者通过看护者和备忘录交互。带点主观意识,我觉得这算什么设计模式,简直就是垃圾,毫无接口,过度封装。
违背设计原则
- 连设计原则的前提都没有。
使用场景
- 毫无用处,缓存,持久化存储用起来,备忘录模式还是放在历史角落留个备忘吧。
访问者模式
uml图已经揭露出访问者接口依赖具体的元素类,意图很明显,只是为了把访问的行为从元素类抽离出来。
从逻辑实现看,是和模板模式类似的,但访问者针对的是访问元素的行为抽离,而模板模式是子类对父类的行为扩展。
违背设计原则
- 访问者依赖具体的元素类,违背依赖倒置原则。
- 访问接口的接口不单一,违背接口分离原则。
- 访问者和多个元素类耦合,违背迪米特原则。
- 添加新元素的时候访问接口难以扩展,违背开闭原则。
使用场景
- 元素类较多,且访问意图差异较大的时候,遍历起来是很愉悦的。
状态模式
从uml图可知和策略模式极其相似。区别在于状态模式的上下文是保存状态,而策略模式的上下文是单纯的一种策略。行为上可以看出他们本质的区别在于驱动意图不一致。状态模式是状态,而策略模式是事件。
使用场景
- 状态驱动的场景如TCP三次握手四次挥手,但现实开发的情况极少以状态作为驱动,而是事件,事件和状态可以转换,可能需要进一步封装,且状态上下文需要缓存,得不偿失。
解释器模式
上下文存储被解释对象,通过递归直到TerminalExpression解析完毕。
####使用场景
- 解析命令和包,业务开发很少用到,至少很多源码解析包都是直接面向过程解析,并没有用到此模式。
享元模式
享元模式目的是为了类里面区分可共享和不可共享的部分,从而节省内存。内存是节约了,但存在并发改变不可共享部分的风险。可通过以不可共享部分的代码作为享元工厂池的key避免并发修改的问题,但每次改变不可共享部分要更新共享池又会耦合到享元工厂。
使用场景
- 大量初始化后只读且有大部分可共享信息的对象,本质是缓存可共享部分。
桥接模式
抽象类包含了一个或者多个实现类,方法都是被包含的具体实现类实现的,从而实现桥接以及功能的组合。
违背设计原则
- 因为桥接的关系,有多重责任,因此违反单一职责原则。
使用场景
- 重用性要求较高的场景。
- 部分语言无多继承功能,可通过桥接实现多父的特性。
总结
辩证看待这23种设计模式,不要过度记忆,这些都是术,有些是糟粕的设计模式,6大设计原则才是道,从术悟道,相辅相成。
有用的设计模式
在23种设计模式中,以下是我认为比较有用,包括架构和业务都可能接触到的。
- 单例模式
- 工厂方法模式
- 模板模式
- 命令模式
- 责任链模式
- 装饰模式
- 策略模式
- 适配者模式
- 观察者模式
- 门面模式
- 桥接模式
当业务开发遇到可以套用设计模式的时候请留一个TODO,写完单元测试后有时间就优化他们吧!
如何编写可扩展的代码
从这么多设计模式和6大设计原则中,目前悟出以下习惯。
- 多思考实例的抽象,本质有多少个对象,找出抽象中的共同点,为实例对应的类建立接口。
- 除非要用到子类的特性,否则引用类型应该保持抽象类型。
- 对外要善于封装接口,为内部接口保留可变的能力。
- 辩证看待继承,是否需要多态,可考虑组合是否满足扩展。
- 适当使用内部类,实现高内聚低耦合。