软件设计模式(Design pattern),又称设计模式,是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。使用设计模式是为了可重用代码、让代码更容易被他人理解、保证代码可靠性、程序的重用性。

设计模式的分类

总体来说设计模式分为三大类:
创建型模式,共五种:工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式。

结构型模式,共七种:适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式、享元模式。

行为型模式,共十一种:策略模式、模板方法模式、观察者模式、迭代子模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。

七大设计原则核心思想

1、单一职责原则 SRP ( Single Responsibility Principle )

概念

单一职责原则表示一个模块的组成元素之间的功能相关性。从软件变化的角度来看,就一个类而言,应该仅有一个让它变化的原因;通俗地说,即一个类只负责一项职责。

假设某个类 P 负责两个不同的职责,职责 P1 和 职责 P2,那么当职责 P1 需求发生改变而需要修改类 P,有可能会导致原来运行正常的职责 P2 功能发生故障。

小结

  1. 降低类的复杂度,一个类只负责一项职责;
  2. 提高类的可读性、可维护性;
  3. 降低变更引起的风险;
  4. 通常情况下,我们应当遵守单一职责原则,只有逻辑足够简单,才可以在代码级别上违反单一职责原则,只有类中方法足够少,才可以在方法级别保持单一职责原则

2、接口隔离原则 ISP ( Interface Segregation Principle )

概念

接口隔离原则,其 “隔离” 并不是准备的翻译,真正的意图是 “分离” 接口(的功能)。

接口隔离原则强调:客户端不应该依赖它不需要的接口;一个类对另一个类的依赖应该建立在最小的接口上。

我们先来看一张图:
概述 - 图1
从图中可以看出,类 A 依赖于 接口 I 中的方法 1,2,3 ,类 B 是对类 A 的具体实现。类 C 依赖接口 I 中的方法 1,4,5,类 D 是对类 C 的具体实现。对于类B和类D来说,虽然他们都存在着用不到的方法(也就是图中红色字体标记的方法),但由于实现了接口I,所以也必须要实现这些用不到的方法。

如果接口定义的过于臃肿,只要接口中出现的方法,不管依赖于它的类是否需要该方法,实现类都必须去实现这些方法,这就不符合接口隔离原则,如果想符合接口隔离原则,就必须对接口 I 如下图进行拆分:
概述 - 图2

小结

  1. 接口隔离原则的思想在于建立单一接口,尽可能地去细化接口,接口中的方法尽可能少
  2. 但是凡事都要有个度,如果接口设计过小,则会造成接口数量过多,使设计复杂化。所以一定要适度。

3、依赖倒转原则 DIP ( Dependence Inversion Principle )

概念

定义:高层模块不应该依赖低层模块,二者都应该于抽象。进一步说,抽象不应该依赖于细节,细节应该依赖于抽象。

举个例子, 某天产品经理需要添加新的功能,该功能需要操作数据库,一般负责封装数据库操作的和处理业务逻辑分别由不同的程序员编写。

封装数据库操作可认为低层模块,而处理业务逻辑可认为高层模块,那么如果处理业务逻辑需要等到封装数据库操作的代码写完的话才能添加的话讲会严重拖垮项目的进度。

正确的做法应该是处理业务逻辑的程序员提供一个封装好数据库操作的抽象接口,交给低层模块的程序员去编写,这样双方可以单独编写而互不影响。

依赖倒转原则的核心思想就是面向接口编程,思考下面这样一个场景。

样例

母亲给孩子讲故事,只要给她一本书,她就可照着书给孩子讲故事了。代码如下:

  1. class Book{
  2. public String getContent(){
  3. return "这是一个有趣的故事";
  4. }
  5. }
  6. class Mother{
  7. public void say(Book book){
  8. System.out.println("妈妈开始讲故事");
  9. System.out.println(book.getContent());
  10. }
  11. }
  12. public class Client{
  13. public static void main(String[] args){
  14. Mother mother = new Mother();
  15. mother.say(new Book());
  16. }
  17. }

假如有一天,给的是一份报纸,而不是一本书,让这个母亲讲下报纸上的故事,报纸的代码如下:

  1. class Newspaper{
  2. public String getContent(){
  3. return "这个一则重要的新闻";
  4. }
  5. }

然而这个母亲却办不到,应该她只会读书,这太不可思议,只是将书换成报纸,居然需要修改 Mother 类才能读,假如以后需要换成了杂志呢?原因是 Mother 和 Book 之间的耦合度太高了,必须降低他们的耦合度才行。

我们可以引入一个抽象接口 IReader 读物,让书和报纸去实现这个接口,那么无论提供什么样的读物,该母亲都能读。代码如下:

  1. interface IReader{
  2. public String getContent();
  3. }
  4. class Newspaper implements IReader {
  5. public String getContent(){
  6. return "这个一则重要的新闻";
  7. }
  8. }
  9. class Book implements IReader{
  10. public String getContent(){
  11. return "这是一个有趣的故事";
  12. }
  13. }
  14. class Mother{
  15. public void say(IReader reader){
  16. System.out.println("妈妈开始讲故事");
  17. System.out.println(reader.getContent());
  18. }
  19. }
  20. public class Client{
  21. public static void main(String[] args){
  22. Mother mother = new Mother();
  23. mother.say(new Book());
  24. mother.say(new Newspaper());
  25. }
  26. }

这样修改之后,以后无论提供什么样的读物,只要去实现了 IReader 接口之后就可以被母亲读。实际情况中,代表高层模块的 Mother 类将负责完成主要的业务逻辑,一旦需要对它进行修改,引入错误的风险极大。所以遵循依赖倒转原则可以降低类之间的耦合性,提高系统的稳定性,降低修改程序造成的风险。

小结

依赖倒转原则的核心就是要我们面向接口编程,理解了面向接口编程,也就理解了依赖倒转。

注意事项和细节

  1. 底层模块尽量都要有抽象类或接口,或者两者都有,程序稳定性更好。
  2. 变量的生命类型尽量是抽象类或接口,这样我们的变量引用和实际对象间,就存在一个缓冲层,有利于程序扩展和优化
  3. 继承时遵循里氏替换原则。

4、里氏替换原则 LSP ( Liskov Substitution Principle )

概念

在编程中常常会遇到这样的问题:有一功能 P1, 由类 A 完成,现需要将功能 P1 进行扩展,扩展后的功能为 P,其中P由原有功能P1与新功能P2组成。新功能P由类A的子类B来完成,则子类B在完成新功能P2的同时,有可能会导致原有功能P1发生故障。

里氏替换原则告诉我们,当使用继承时候,类 B 继承类 A 时,除添加新的方法完成新增功能 P2,尽量不要修改父类方法预期的行为。

里氏替换原则的重点在不影响原功能,而不是不覆盖原方法。

继承包含这样一层含义:父类中凡是已经实现好的方法(相对于抽象方法而言),实际上是在设定一系列的规范和契约,虽然它不强制要求所有的子类必须遵从这些契约,但是如果子类对这些非抽象方法任意修改,就会对整个继承体系造成破坏。而里氏替换原则就是表达了这一层含义。

举个例子

样例

我们需要完成一个两数相减的功能:

  1. class A{
  2. public int func1(int a, int b){
  3. return a-b;
  4. }
  5. }

后来,我们需要增加一个新的功能:完成两数相加,然后再与100求和,由类B来负责。即类B需要完成两个功能:

  • 两数相减
  • 两数相加,然后再加100

由于类A已经实现了第一个功能,所以类B继承类A后,只需要再完成第二个功能就可以了,代码如下:

  1. class B extends A{
  2. public int func1(int a, int b){
  3. return a+b;
  4. }
  5. public int func2(int a, int b){
  6. return func1(a,b)+100;
  7. }
  8. }

我们发现原来原本运行正常的相减功能发生了错误,原因就是类 B 在给方法起名时无意中重写了父类的方法,造成了所有运行相减功能的代码全部调用了类 B 重写后的方法,造成原来运行正常的功能出现了错误。在实际编程中,我们常常会通过重写父类的方法来完成新的功能,这样写起来虽然简单,但是这样往往也增加了重写父类方法所带来的风险。

小结

里氏替换原则通俗的来讲就是:子类可以扩展父类的功能,但不能改变父类原有的功能。

5、开闭原则 OCP ( Open-Closed Principle )

概念

开放-关闭原则表示软件实体 (类、模块、函数等等) 应该是可以被扩展的,但是不可被修改。(Open for extension, close for modification)

如果一个软件能够满足 OCP 原则,那么它将有两项优点:

  1. 能够扩展已存在的系统,能够提供新的功能满足新的需求,因此该软件有着很强的适应性和灵活性。
  2. 已存在的模块,特别是那些重要的抽象模块,不需要被修改,那么该软件就有很强的稳定性和持久性。

举个简单例子

样例

这里有个生产电脑的公司,根据输入的类型,生产出不同的电脑,代码如下:

  1. interface Computer {
  2. }
  3. class Macbook implements Computer {
  4. }
  5. class Surface implements Computer {
  6. }
  7. class Factory {
  8. public Computer produceComputer(String type) {
  9. Computer c = null;
  10. if(type.equals("macbook")){
  11. c = new Macbook();
  12. }else if(type.equals("surface")){
  13. c = new Surface();
  14. }
  15. return c;
  16. }
  17. }

显然上面的代码违背了开放 - 关闭原则,如果需要添加新的电脑产品,那么修改 produceComputer 原本已有的方法,正确的方式如下:

  1. interface Computer {
  2. }
  3. class Macbook implements Computer {
  4. }
  5. class Surface implements Computer {
  6. }
  7. interface Factory {
  8. public Computer produceComputer();
  9. }
  10. class AppleFactory implements Factory {
  11. public Computer produceComputer() {
  12. return new Macbook();
  13. }
  14. }
  15. class MSFactory implements Factory {
  16. public Computer produceComputer() {
  17. return new Surface();
  18. }
  19. }

正确的方式应该是将 Factory 抽象成接口,让具体的工厂(如苹果工厂,微软工厂)去实现它,生产它们公司相应的产品,这样写有利于扩展,如果这是需要新增加戴尔工厂生产戴尔电脑,我们仅仅需要创建新的电脑类和新的工厂类,而不需要去修改已经写好的代码。

小结

  1. OCP 可以具有良好的可扩展性,可维护性。
  2. 不可能让一个系统的所有模块都满足 OCP 原则,我们能做到的是尽可能地不要修改已经写好的代码,已有的功能,而是去扩展它。

6、迪米特法则 LOD ( Law Of Demeter )

概念

迪米特法则又称为 最少知道原则,它表示一个对象应该对其它对象保持最少的了解。通俗来说就是,只与直接的朋友通信。

首先来解释一下什么是直接的朋友:每个对象都会与其他对象有耦合关系,只要两个对象之间有耦合关系,我们就说这两个对象之间是朋友关系。耦合的方式很多,依赖、关联、组合、聚合等。其中,我们称出现成员变量、方法参数、方法返回值中的类为直接的朋友,而出现在局部变量中的类则不是直接的朋友。也就是说,陌生的类最好不要作为局部变量的形式出现在类的内部。

对于被依赖的类来说,无论逻辑多么复杂,都尽量的将逻辑封装在类的内部,对外提供 public 方法,不对泄漏任何信息。

举个例子

样例

小结

7、合成复用原则 CRP ( Composite/Aggregate Reuse Principle ) 组合/聚合复用原则

概念

组合/聚合复用原则就是在一个新的对象里面使用一些已有的对象,使之成为新对象的一部分; 新的对象通过向这些对象的委派达到复用已有功能的目的。

在面向对象的设计中,如果直接继承基类,会破坏封装,因为继承将基类的实现细节暴露给子类;如果基类的实现发生了改变,则子类的实现也不得不改变;从基类继承而来的实现是静态的,不可能在运行时发生改变,没有足够的灵活性。于是就提出了组合/聚合复用原则,也就是在实际开发设计中,尽量使用组合/聚合,不要使用类继承。

样例

小结

Reference

设计模式之七大基本原则:https://zhuanlan.zhihu.com/p/24614363