一、设计模式6大原则
软件编程的总原则:高内聚、低耦合。
(一)单一职责原则(Single Responsibility Principle,简称SRP )
There should never be more than one reason for a class to change.
定义:不要存在多于一个导致类变更的原因,即一个类只负责一项职责。
优点:
1、可以降低类的复杂度,一个类只负责一项职责,其逻辑肯定要比负责多项职责简单的多;
2、提高类的可读性,提高系统的可维护性;
3、变更引起的风险降低。变更是必然的,如果单一职责原则遵守的好,当修改一个功能时,可以显著降低对其他功能的影响。
特别说明,单一职责原则不只是面向对象编程思想所特有的,只要是模块化的程序设计,都适用单一职责原则。
(二)开放封闭原则(Open Close Principle,简称OCP)
Software entities like classes,modules and functions should be open for extension but closed for modifications.
定义:一个软件实体如类、模块和函数应该对扩展开放,对修改关闭。
开闭原则由Bertrand Meyer于1988年提出,是面向对象设计中最基础的设计原则,它指导我们如何建立稳定灵活的系统。开闭原则可能是设计模式六项原则中定义最模糊的一个了,它只告诉我们对扩展开放,对修改关闭,可是具体做法并没有明确的告诉我们。
其实,我们遵循设计模式的其他5大原则,以及使用23种设计模式的目的就是遵循开闭原则。也就是说,只要我们对其余5项原则遵守的好了,设计出的软件自然是符合开闭原则的。这个开闭原则更像是另外五项原则遵守程度的“平均得分”,这5项原则遵守的好,平均分自然就高,说明软件设计开闭原则遵守的好;如果这5项原则遵守的不好,则说明开闭原则遵守的不好。
其实笔者认为,开闭原则的意思是:
因为抽象灵活性好,适应性广,只要抽象的合理,可以基本保持软件架构的稳定。而软件中易变的细节,我们用从抽象派生的实现类来进行扩展,当软件需要发生变化时,我们只需要根据需求重新派生一个实现类来扩展就可以了。当然前提是我们的抽象要合理,要对需求的变更有前瞻性和预见性才行。
其实,剩下的那5项原则,恰恰是告诉我们用抽象构建框架,用实现扩展细节的注意事项而已:
1、单一职责原则告诉我们实现类要职责单一;
2、里氏替换原则告诉我们不要破坏继承体系;
3、依赖倒置原则告诉我们要面向接口编程;
4、接口隔离原则告诉我们在设计接口的时候要精简单一;
5、迪米特法则告诉我们要降低耦合。
而开闭原则是总纲,他告诉我们要对扩展开放,对修改关闭。
(三)里氏替换原则(Liskov Substitution Principle,简称LSP)
Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it.
这项原则最早是在1988年,由麻省理工学院的一位女科学家(Barbara Liskov)提出来的。
里氏代换原则由2008年图灵奖得主、美国第一位计算机科学女博士Barbara Liskov教授和卡内基·梅隆大学Jeannette Wing教授于1994年提出。
定义1:如果对每一个类型为 T1的对象 o1,都有类型为 T2 的对象o2,使得以 T1定义的所有程序 P 在所有的对象 o1 都代换成 o2 时,程序 P 的行为没有发生变化,那么类型 T2 是类型 T1 的子类型。
定义2:所有引用基类的地方必须能透明地使用其子类的对象。
通俗的来讲就是:子类可以扩展父类的功能,但不能改变父类原有的功能。它包含以下4层含义:
1、子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法。
2、子类中可以增加自己特有的方法。
3、当子类的方法重载父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松。
4、当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格。
(四)依赖倒置原则(Dependence Inversion Principle,简称DIP)
High level modules should not depends upon low level modules.Both should depend upon abstractions.Abstractions should not depend upon details.Details should depend upon abstractions.
定义:高层模块不应该依赖低层模块,二者都应该依赖其抽象;抽象不应该依赖细节;细节应该依赖抽象。
依赖倒置原则基于这样一个事实:相对于细节的多变性,抽象的东西要稳定的多。以抽象为基础搭建起来的架构比以细节为基础搭建起来的架构要稳定的多。在java中,抽象指的是接口或者抽象类,细节就是具体的实现类,使用接口或者抽象类的目的是制定好规范和契约,而不去涉及任何具体的操作,把展现细节的任务交给他们的实现类去完成。
依赖倒置原则的核心思想是面向接口编程。遵循依赖倒置原则可以降低类之间的耦合性,提高系统的稳定性,降低修改程序造成的风险,也给多人并行开发带来了极大的便利。
传递依赖关系有三种方式:接口传递、构造方法传递、setter方法传递。相信用过Spring框架的,对依赖的传递方式一定不会陌生。
在实际编程中,我们一般需要做到如下3点:
1、低层模块尽量都要有抽象类或接口,或者两者都有。
2、变量的声明类型尽量是抽象类或接口。
3、使用继承时遵循里氏替换原则。
依赖倒置原则的核心就是面向接口编程,理解了面向接口编程,也就理解了依赖倒置。
(五)接口隔离原则(Interface Segregation Principle,简称ISP)
The dependency of one class to another one should depend on the smallest possible interface.
定义:客户端不应该依赖它不需要的接口;一个类对另一个类的依赖应该建立在最小的接口上。
具体含义是:建立单一接口,不要建立庞大臃肿的接口,尽量细化接口,接口中的方法尽量少。也就是说,我们要为各个类建立专用的接口,而不要试图去建立一个很庞大的接口供所有依赖它的类去调用。
接口隔离原则与单一职责原则的区别:
1、单一职责原则注重的是职责;而接口隔离原则注重对接口依赖的隔离。
2、单一职责原则主要是约束类,其次才是接口和方法,它针对的是程序中的实现和细节;而接口隔离原则主要约束接口,主要针对抽象,针对程序整体框架的构建。
采用接口隔离原则对接口进行约束时,要注意以下几点:
1、接口尽量小,但是要有限度。
对接口进行细化可以提高程序设计灵活性是不争的事实,但是如果过小,则会造成接口数量过多,使设计复杂化,所以一定要适度。
2、为依赖接口的类定制服务,只暴露给调用的类它需要的方法,它不需要的方法则隐藏起来。
只有专注地为一个模块提供定制服务,才能建立最小的依赖关系。
3、提高内聚,减少对外交互。
使接口用最少的方法去完成最多的事情。
运用接口隔离原则,一定要适度,接口设计的过大或过小都不好。设计接口的时候,只有多花些时间去思考和筹划,才能准确地实践这一原则。
(六)迪米特原则(或最少知道原则,Law of Demeter,简称LoD)
Only talk to you immediate friends.
定义:一个对象应该对其他对象保持最少的了解。即一个类对自己依赖的类知道的越少越好。
更简单的定义:只与直接的朋友通信。
每个对象都会与其他对象有耦合关系,只要两个对象之间有耦合关系,我们就说这两个对象之间是朋友关系。耦合的方式很多,如依赖、关联、组合、聚合等。其中,我们称出现成员变量、方法参数、方法返回值中的类为直接的朋友,而出现在局部变量中的类则不是直接的朋友。也就是说,陌生的类最好不要作为局部变量的形式出现在类的内部。
迪米特法则的初衷是降低类之间的耦合,由于每个类都减少了不必要的依赖,因此的确可以降低耦合关系。但是凡事都有度,虽然可以避免与非直接的类通信,但是要通信,必然会通过一个“中介”来发生联系。
过分的使用迪米特原则,会产生大量这样的中介和传递类,导致系统复杂度变大。所以在采用迪米特法则时要反复权衡,既做到结构清晰,又要高内聚低耦合。
前五个原则(单一职责、开放封闭、里氏替换、接口隔离、依赖倒置)被Bob大叔在21世纪早期定义为SOLID(即稳定的,由首字母合成而来)原则。作为面向对象编程的五个基本原则,当这些原则被一起使用时,它们使得一个软件系统更清晰、简单,最大程度地拥抱变化。
最后说明一下如何去遵守这六个原则。对这六个原则的遵守并不是是和否的问题,而是多和少的问题,也就是说,我们一般不会说有没有遵守,而是说遵守程度的多少。任何事都是过犹不及,设计模式的六个设计原则也是一样,制定这六个原则的目的并不是要我们刻板的遵守他们,而需要根据实际情况灵活运用。对他们的遵守程度只要在一个合理的范围内,就算是良好的设计。
参考资料:设计模式六大原则
扩展:
(七)合成复用原则(Composite Reuse Principle,CRP)
又叫组合/聚合复用原则(Composition/Aggregate Reuse Principle,CARP)。
定义:它要求在软件复用时,要尽量先使用组合或者聚合等关联关系来实现,其次才考虑使用继承关系来实现。
如果要使用继承关系,则必须严格遵循里氏替换原则。合成复用原则同里氏替换原则相辅相成的,两者都是开闭原则的具体实现规范。
7种原则总结:
开闭原则是总纲,它告诉我们要对扩展开放,对修改关闭;
里氏替换原则告诉我们不要破坏继承体系;
依赖倒置原则告诉我们要面向接口编程;
单一职责原则告诉我们实现类要职责单一;
接口隔离原则告诉我们在设计接口的时候要精简单一;
迪米特法则告诉我们要降低耦合度;
合成复用原则告诉我们要优先使用组合或者聚合关系复用,少用继承关系复用。
二、GoF 的 23 种设计模式的分类和功能
(一)设计模式分类
1、根据目的来分
根据模式是用来完成什么工作来划分,这种方式可分为创建型模式、结构型模式和行为型模式 3 种。
创建型模式:用于描述“怎样创建对象”,它的主要特点是“将对象的创建与使用分离”。GoF 中提供了单例、原型、工厂方法、抽象工厂、建造者等 5 种创建型模式。
结构型模式:用于描述如何将类或对象按某种布局组成更大的结构,GoF 中提供了代理、适配器、桥接、装饰、外观、享元、组合等 7 种结构型模式。
行为型模式:用于描述类或对象之间怎样相互协作共同完成单个对象都无法单独完成的任务,以及怎样分配职责。GoF 中提供了模板方法、策略、命令、职责链、状态、观察者、中介者、迭代器、访问者、备忘录、解释器等 11 种行为型模式。
2、根据作用范围来分
根据模式是主要用于类上还是主要用于对象上来分,这种方式可分为类模式和对象模式两种。
类模式:用于处理类与子类之间的关系,这些关系通过继承来建立,是静态的,在编译时刻便确定下来了。GoF中的工厂方法、(类)适配器、模板方法、解释器属于该模式。
对象模式:用于处理对象之间的关系,这些关系可以通过组合或聚合来实现,在运行时刻是可以变化的,更具动态性。GoF 中除了以上 4 种,其他的都是对象模式。
创建型模式
创建范例全部是关于如何创建实例的。这组范例可以被划分为两组:类创建范例及对象创建范例。类创建实例在实例化过程中有效的使用类之间的继承关系,对象创建范例则使用代理来完成其任务。
1、工厂方法 (Factory Method pattern,包括简单工厂模式和工厂方法模式)
2、抽象工厂 (Abstract Factory)
3、构造器 (建造者,Builder Pattern)
4、原型 (Prototype pattern)
5、单例 (Singleton pattern)
结构型模式
这组范例都是关于类及对象复合关系的。
1、适配器 (Adapter pattern)
2、桥接 (Bridge pattern)
3、组合 (Composite pattern)
4、装饰 (Decorator pattern)
5、外观 (门面,Façade pattern)
6、享元 (Flyweight pattern)
7、代理 (Proxy pattern)
行为型模式
这组范例都是关于对象之间如何通讯的。
1、责任链 (职责链,Chain-of-responsibility pattern)
2、命令 (Command pattern)
3、解释器 (Interpreter pattern)
4、迭代器 (Iterator pattern)
5、中介者 (调停者,Mediator pattern)
6、备忘录 (Memento pattern)
7、观察者 (Observer pattern)
8、状态 (State pattern)
9、策略 (Strategy pattern)
10、模板方法 (Template method pattern)
11、访问者 (Visitor)
(二)设计模式的功能
1、单例(Singleton)模式:某个类只能生成一个实例,该类提供了一个全局访问点供外部获取该实例,其拓展是有限多例模式。
2、原型(Prototype)模式:将一个对象作为原型,通过对其进行复制而克隆出多个和原型类似的新实例。
3、工厂方法(Factory Method)模式:定义一个用于创建产品的接口,由子类决定生产什么产品。
4、抽象工厂(AbstractFactory)模式:提供一个创建产品族的接口,其每个子类可以生产一系列相关的产品。
5、建造者(Builder)模式:将一个复杂对象分解成多个相对简单的部分,然后根据不同需要分别创建它们,最后构建成该复杂对象。
6、代理(Proxy)模式:为某对象提供一种代理以控制对该对象的访问。即客户端通过代理间接地访问该对象,从而限制、增强或修改该对象的一些特性。
7、适配器(Adapter)模式:将一个类的接口转换成客户希望的另外一个接口,使得原本由于接口不兼容而不能一起工作的那些类能一起工作。
8、桥接(Bridge)模式:将抽象与实现分离,使它们可以独立变化。它是用组合关系代替继承关系来实现,从而降低了抽象和实现这两个可变维度的耦合度。
9、装饰(Decorator)模式:动态的给对象增加一些职责,即增加其额外的功能。
10、外观(Facade)模式:为多个复杂的子系统提供一个一致的接口,使这些子系统更加容易被访问。
11、享元(Flyweight)模式:运用共享技术来有效地支持大量细粒度对象的复用。
12、组合(Composite)模式:将对象组合成树状层次结构,使用户对单个对象和组合对象具有一致的访问性。
13、模板方法(TemplateMethod)模式:定义一个操作中的算法骨架,而将算法的一些步骤延迟到子类中,使得子类可以不改变该算法结构的情况下重定义该算法的某些特定步骤。
14、策略(Strategy)模式:定义了一系列算法,并将每个算法封装起来,使它们可以相互替换,且算法的改变不会影响使用算法的客户。
15、命令(Command)模式:将一个请求封装为一个对象,使发出请求的责任和执行请求的责任分割开。
16、职责链(Chain of Responsibility)模式:把请求从链中的一个对象传到下一个对象,直到请求被响应为止。通过这种方式去除对象之间的耦合。
17、状态(State)模式:允许一个对象在其内部状态发生改变时改变其行为能力。
18、观察者(Observer)模式:多个对象间存在一对多关系,当一个对象发生改变时,把这种改变通知给其他多个对象,从而影响其他对象的行为。
19、中介者(Mediator)模式:定义一个中介对象来简化原有对象之间的交互关系,降低系统中对象间的耦合度,使原有对象之间不必相互了解。
20、迭代器(Iterator)模式:提供一种方法来顺序访问聚合对象中的一系列数据,而不暴露聚合对象的内部表示。
21、访问者(Visitor)模式:在不改变集合元素的前提下,为一个集合中的每个元素提供多种访问方式,即每个元素有多个访问者对象访问。
22、备忘录(Memento)模式:在不破坏封装性的前提下,获取并保存一个对象的内部状态,以便以后恢复它。
23、解释器(Interpreter)模式:提供如何定义语言的文法,以及对语言句子的解释方法,即解释器。
必须指出,这 23 种设计模式不是孤立存在的,很多模式之间存在一定的关联关系,在大的系统开发中常常同时使用多种设计模式,希望读者认真学好它们。
参考资料:23种设计模式全面解析