设计原则&设计模式

导论

  1. 什么是设计原则:判断程序设计质量好坏的准则。
  2. 什么是设计模式:软件设计过程中重复出现问题的解决方案
  3. 设计原则的作用:指导抽象、类、类关系设计,相当于指导设计程序基础框架(Rank-分层、Role-角色、Relation-类关系)
  4. 设计模式的作用:知道对象关系设计
  5. 如何运用设计原则和设计模式

设计原则

内聚

定义:模块内部元素彼此结合的紧密程度。(模块内部元素的关联性大小)

  • 模块:服务、模块、包、类/接口
  • 元素:业务模块(服务)、包/命名空间(模块)、类/接口/全局数据(包)、属性/方法(类/接口)

内聚性高低判断:模块内部的元素都忠于模块职责元素之间紧密联系(模块内部元素都为模块服务)。(若模块内元素都忠于模块职责,即使关联不紧密也不影响内聚性)

内聚分类

  1. 偶然内聚:模块之间的元素联系不大,因为“巧合”放在一起,实际没有什么内聚性,如:utils包。

  2. 逻辑内聚:元素逻辑上属于同一个比较宽泛的类,但元素的职责可能不同。如:鼠标和键盘为输入类,打印机和显示器为输出类。

  3. 时间内聚:元素在时间上很相近。

  4. 过程内聚:模块内元素必须按照固定的“过程顺序”进行处理。如:读文件、解析、存储、通知、响应结果等封装在一个函数模块,它们的顺序固定。

    *时间内聚和过程内聚区别是过程内聚的顺序是固定的,而时间内聚顺序可变。

  5. 信息内聚:模块内元素操作相同的数据,如:增删改查某个数据(某个Service内CRUD方法)。

  6. 顺序内聚:模块内某些元素的输出是另外元素的输入,如:规则引擎一个函数读取配置,将配置转换为指令,另一个函数执行指令。

  7. 功能内聚:元素都是为了完成同一个单一任务(内聚性最好的一种方式)。

耦合

定义:模块之间的依赖程度。(模块之间的关联性大小)

耦合分类

  1. 无耦合:模块之间没有任何关系或者交互

:无耦合是不是最好的?

答:不一定,如果一个模块是最底层模块,没什么问题(被依赖);

​ 如果该模块是完全自给自足,则会得不偿失:

  • 失去重用其他模块的机会
  • 什么都要自己做,重复造轮子,效率低
  1. 消息耦合:模块间的耦合关系表现在消息传递上。这里的“消息”会随着“模块”的不同而不同。

    消息耦合是一种耦合度很低的耦合,调用方仅仅依赖于被调用方的“消息”,不需要传递参数,无需了解或控制被调用方的内部逻辑

    例:两个系统交互的接口:HTTP接口、RPC接口;A类调用B的某个方法,该方法就是消息;

  2. 数据耦合:两个模块通过参数传递基本数据。

    被调用方依赖于调用方的参数数据

    • 数据通过参数传递,并非全局数据、配置文件、共享内存等方式
    • 依赖的是基本数据类型
  3. 数据结构耦合:两个模块通过传递数据结构的方式传递数据,又称标签耦合,这里的数据结构是可理解为自定义对象参数,如:VmModel、Emp(区别于数据耦合)。

  4. 控制耦合:一个模块可以通过某种方式来控制另一个模块的行为。

  5. 外部耦合:两个模块依赖于外部相同数据格式、通信协议、设备接口

  6. 全局耦合:两个模块共享全局数据,也叫普通耦合

  7. 内容耦合:一个模块内容依赖另一个模块内容

高内聚低耦合

问1:为什么要高内聚低耦合?

答:降低软件复杂性。提升软件的可复用、移植、扩展、修改能力。

问2:高内聚低耦合是否意味着内聚越高越好,耦合越低越好?

答:并非如此,高内聚和低耦合像天平两端,不可能同时上升,需要从中做个平衡。(如:关联很小的类全放一起反而增加了复杂性)

低内聚模块特性

  • 低内聚模块让人难以理解,增加了理解复杂度(关联紧密的元素都分开了,如:机器和货道独立到两个服务)
  • 低内聚模块容易变化,增加了修改的复杂度(低内聚表明拆分细,变化点多,元素变化可能引起设计、测试、部署等的改变)

低耦合特性

  • 模块本身显得庞大,功能集中在一起,这样反而会导致模块本身不稳定性增加(量变引起质变)
  • 模块无法被重用

高内聚低耦合实际作用

高内聚将与模块相关的变化封装在模块内,产生的变化对其他模块的影响较小;低耦合使得模块之间的关联减小,一个模块受其他模块的影响可能性减小。

高内聚低耦合本质在于变化,核心是降低变化的影响。

类设计原则

设计原则&设计模式_第1张图片

SRP(单一职责原则)

Martin(人名)定义:一个类只有一个职责,职责是指引起该类变化的原因。

引起类变化的原因,如下变化都是类的职责吗:

  • 给类新增一个方法
  • 给类新增属性
  • 给类方法新增一个参数

另一种定义:SRP就是每个模块只做一件事

例1:学生信息管理系统,以下是四件事还是一件事?

  • 新增学生信息
  • 查询学生信息
  • 修改学生信息
  • 删除学生信息

站在我的角度,学生管理系统的职责是管理学生信息,而这个职责包含了新增、查询、修改、删除学生四个功能。

例2: 我是快递员,我的工作是分包、收快递、送快递、通知收货人取快递等,在我们看来快递员的职责是快递管理。

职责的结论

  • 职责是站在他人角度定义的
  • 职责不是一件事,而是多件事

类的职责

  • 类的职责是站在其他类的角度定义的
  • 类的职责是一组多个相关功能的

SRP的使用范围:只适合基础类,不适用基于基础类的聚合类。

SRP总结:单一职责原则适用于模块(服务、类、模块、包等)职责定义,强调模块的的职责应该保证单一,且职责是一组相关功能的集合

OCP(开闭原则)

维基百科:对扩展开放,对修改关闭(Open-Closed Principe)。 对使用者修改关闭,对提供者扩展开放。

个人理解:一个软件模块(类、模块、系统、函数等)应该通过扩展来实现变化,而不是通过修改已有的代码来实现变化的。(如提供者有新的功能扩展,不能修改使用者模块)

基本介绍:

​ (1)一个软件实体如类,模块和函数应该对扩展开放(对于提供方来说),对修改关闭(对于使用方来说)。用抽象构建框架,用实现扩展细节。

(2)当软件需要变化时,尽量通过扩展软件实体的行为来实现变化,而不是通过修改已有的代码来实现变化。

(3)编程中遵循其它原则,以及使用设计模式的目的就是遵循开闭原则。

例:有个IBook接口,有name()、price()、author()三个抽象方法,然后有一个小说类NovelBook实现了IBook。

  • 需求1:需要给所有的书籍类新增出版时间publicationDate()功能。
  • 需求2:对小说类的书籍进行打折销售。
  1. 需求1必须放在IBook接口实现
  2. 需求2的可能实现方式如下:
    • 修改IBook接口
    • 修改NovelBook类的价格方法
    • 新增一个OffNovelBook小说打折类继承NovelBook,在OffNovelBook对价格进行处理。

问:需求1和需求2的实现方案满足OCP?

OCP的应用原则:

  1. 接口不变:接口里的函数名、参数、返回值等,可以应用OCP。

  2. 接口改变:已有的函数名称、参数、返回值或新增函数,OCP不再适用。

为什么使用开闭原则:

1、开闭原则是最基础的设计原则,其它五个设计原则都是开闭原则的具体形态,它们本身就遵循开闭原则。依照java语言的称谓,开闭原则是抽象类,而其它的五个原则是具体的实现类。

2、提高复用性
面向对象设计中,所有逻辑都是从原子逻辑组合而来,不是在一个类中独立实现一个业务逻辑。只有这样的代码才可以复用,粒度越小,被复用的可能性越大。那为什么要复用呢?减少代码的重复,避免相同的逻辑分散在多个角落。

3、提高维护性
软件上线后,需要对程序进行扩展,维护人员更愿意扩展一个类,而不是修改一个类。

让维护人员读懂原有代码,再修改,是一件痛苦的事情,不要让他在原有的代码海洋中游荡后再修改,那是对维护人员的折磨

4:面向对象开发的要求
万物皆对象,我们要把所有的事物抽象成对象,然后针对对象进行操作,但是万物皆发展变化,有变化就要有策略去应对,怎么快速应对呢?这就需要在设计之初考虑到所有可能变化的因素,然后留下接口,等待“可能”转变为“现实”。

如何运用开闭原则?

  1. 抽象约束

    抽象是将一组关联共性事物独立出来,形成一组功能的定义,是面向对象编程的一个基础骨架抽象类。抽象没有具体的实现,因此变化的可能结果较多,变化由具体扩展类实现,而抽象可以约束变化。

  2. 封装变化

    满足接口隔离原则

    (1). 相同变化封装到一个接口或抽象类中
    (2). 不同变化封装到不同的接口或抽象类中,不应该有两个不同的变化出现在同一个接口或抽象类中。

  3. 制定项目章程
    约定优于配置,团队中,建立项目章程是非常重要的,因为章程是所有人员都必须遵守的约定,对项目来说,约定优于配置。这比通过接口或抽象类进行约束效率更高,而扩展性一点也没有减少。

    如:不同功能独立到不同函数;编程之前先设计抽象骨架;

OCP可广泛应用在函数、类、系统、子系统、模块等角色之间关系的设计。类之间应用OCP,使用interface进行交互,系统或模块之间使用规定好的协议,如:HTTP、GRPC等。

*提供者如果随意改动会引起使用者一起修改,这些扩展的灵活性就大大降低,使用者较多则修改影响较大。一般情况,提供者会通过接口定义一组功能定义。

错误例

public class DriverCar {
  
  	public static void main(String[] args){
      Player player = new Player();
      player.play(new Bmw());
      player.play(new Benz());
      player.play(new Dazh());
    }
}

/**
 * 使用者
 */
public class Player {
  /**
   * 使用者修改部分,提供方每次新增一个车型,使用者被迫都需要修改
   */
	public void play(Car car) {
    String type = car.type();
    if(type.equals("bmw")) {
      System.out.println(car.derverBmw());
    } else if(type.equals("benz")) {
      System.out.println(car.derverBenz());
    } else if(type.equals("dazh")) {
      System.out.println(car.derverDazh());
    }
  }
}

public class Car {
 	String type; 
}

/**
 * bmw提供者
 */
public class Bmw extends Car {
  public Bmw() {
    this.type = "bmw";
  }
  
  public String derverBmw {
    return "derver bmw.";
  }
}
/**
 * benz提供者
 */
public class Benz extends Car {
  public Benz() {
    this.type = "benz";
  }
  
  public String derverBenz {
    return "derver benz.";
  }
}
/**
 * dazh提供者
 */ 
public class Dazh extends Car {
  public Benz() {
    this.type = "dazh";
  }
  
  public String derverDazh {
    return "derver dazh.";
  }
}

OCP例

public class DriverCar {
  
  	public static void main(String[] args){
      Player player = new Player();
      player.play(new Bmw());
      player.play(new Benz());
      player.play(new Dazh());
    }
}

/**
 * 使用者
 */
public class Player {
  /**
   * 提供方每次新增一个车型,使用者无需修改
   */
	public void play(Car car) {
    System.out.println(car.derver());
  }
}

public class ICar {
 	String derver();
}

/**
 * bmw提供者
 */
public class Bmw implements ICar {
  @Overring
  public String derver {
    return "derver bmw.";
  }
}
/**
 * benz提供者
 */
public class Benz implements ICar {
  @Overring
  public String derver {
    return "derver benz.";
  }
}
/**
 * dazh提供者
 */ 
public class Dazh implements ICar {
  @Override
  public String derver {
    return "derver dazh.";
  }
}

LSP(里氏替换原则)

Liskov(发明者):

  • 子类的对象提供了父类的所有行为,且加上子类额外的一些东西(可能是方法或属性)
  • 当程序基于父类实现时, 如果将子类替换父类而程序不需要修改,则说明符合LSP

Martin:

  1. 函数使用指向父类的指针或者引用时,必须能否在不知道子类类型的情况下使用子类对象。
  2. 子类必须能替换成它们的父类

个人理解:对象引用尽量使用接口或抽象类,这样子类可以无感替换父类。

函数和父类交互

  • 调用父类的方法(方法输入)
  • 得到父类方法的输出(方法输出)

由上函数和父类交互体现可得出:子类应该和父类有同样的输入和输出

如何做到ISP:

  • 子类必须实现或继承父类所有的公有方法
  • 子类每个方法的输入参数必须和父类一样(也可以比父类更严格)
  • 子类每个方法输出必须不比子类少(子类返回值应该比父类宽松),即父类的返回值应该是子类返回值的子集

例:

/**
 * 长方形
 */
public class Rectangle {
  protected int _width;
  protected int _height;
  
  /**
   * 设置宽
   */
  public void setWidth(int _width) {
    this._width = _width;
  }
  
  /**
   * 设置高
   */ 
  public void setHeight(int _height) {
    this._height = _height;
  }
  
  /**
   * 获取面积
   */ 
  public int getArea() {
    return this._height * this._width;
  }
}

/**
 * 正方形
 */
public class Square extends Rectangle {
  /**
   * 设置宽
   */
  public void setWidth(int _width) {
    this._width = _width;
    this._height = _height;
  }
  
  /**
   * 设置高
   */ 
  public void setHeight(int _height) {
    this._width = _width;
    this._height = _height;
  }
}


public class UnitTester {
  //main函数相当调用者
  public static void main(String[] args) {
    //父类的指针
    Rectangle rectangle = new Rectangle();
    rectangle.setWidth(4);
    rectangle.setHeight(5);
    System.out.println(rectangle.getArea() == 20);
    
    //子类替换了父类new Rectangle()
    rectangle = new Square();
    rectangle.setWidth(4);
    rectangle.setHeight(5);
    //可正常调用输出
    System.out.println(rectangle.getArea() == 20);
  }
}
//打印结果:
//true
//false

ISP(接口隔离原则)

Martin:

  1. 客户端不应该强迫去依赖它们并不需要的接口
  2. ISP承认对象需要需要非内聚接口,然而ISP建议客户端不需要知道整个类(包含所有功能),只需要知道具有内聚接口的抽象父类即可。

个人理解:不同功能的接口可以用不同的方式聚合到不同抽象类。(不同功能分开,需要哪些接口聚合到抽象类,做到要哪些功能聚合哪些)。

好处:

  • 功能隔离互不影响
  • 灵活扩展
  • 依赖清晰
  • 按需聚合

隔离的理解:隔离自己需要和不需要的部分

例:

/**
 * 复印机接口
 */
public interface Icopier {
  /**
   * 复印
   */
  void copy(Paper paper);
}

/**
 * 传真机接口
 */
public interface IFaxMachine {
  /**
   * 传真
   */
  void fax(String msg);
}

/**
 * 打印机接口
 */
public interface IPrinter {
  /**
   * 打印
   */
  void print(Document doc);
}

/**
 * 扫描仪接口
 */
public interface IScanner {
  /**
   * 扫描
   */
  void scan(Paper paper);
}

【MultiFuncPrinter】

/**
 * 多功能打印机(一体机)
 * 实现了Icopier、IFaxMachine、IPrinter、IScanner四个接,而不是提供一个IMultiFuncPrinter包含了所有复印、打印、传真、扫描功
 * 能
 */
public class MultiFuncPrinter implements Icopier, IFaxMachine, IPrinter, IScanner {
  /**
   * 复印
   */
  @Override
  public void copy(Paper paper) {
    //
  }
  
  /**
   * 扫描IFaxMachine
   */
  @Override
  public void scan(Paper paper){
    //
  }
  
  /**
   * 传真
   */
  @Override
  public void fax(String msg){
    //
  }
  /**
   * 打印
   */
  @Override
  public void print(Document doc){
    //
  }
}

DIP(依赖反转原则)

Martin:也称依赖倒置原则

DIP含义:

  • 高层模块不应该直接依赖底层模块,两者都应该依赖抽象层
  • 抽象不能依赖细节,细节必须依赖抽象

DIP里描述的模块指:系统、子系统、模块、类等,因此模块是广义概念,不是狭义的软件系统里各个子模块。

由DIP含义映射到面向对象领域如下内容:

  • 高层模块依赖于底层模块:高层模块(调用类)需要调用低层模块(被调用类)方法
  • 高层模块依赖抽象层:高层模块基于抽象层编程
  • 低层模块依赖抽象层:低层模块继承或实现抽象层
  • 细节依赖抽象:细节指低层模块(子类),和上面的依赖一样
  • 抽象不能依赖细节:低层模块(子类)的变化不会影响抽象层

例:Player代表玩家,Ford、Benz、Chery

【Player】

/**
 * 玩家,对应DIP中的高层模块
 */
public class Player {
  /**
   * 开福特
   * 不好的依赖,Player直接依赖了Ford(低层模块而不是抽象)
   */
	public void play(Ford car) {
    car.shift();
    car.brake();
  }
  
  /**
   * 开奔驰
   * 不好的依赖,Player直接依赖了Benz(低层模块而不是抽象)
   */
	public void play(Benz car) {
    car.shift();
    car.brake();
  }
  
  /**
   * 开奇瑞
   * 不好的依赖,Player直接依赖了Chery(低层模块而不是抽象)
   */
	public void play(Chery car) {
    car.shift();
    car.brake();
  }
  
  /**
   * 开车
   * 好的依赖,Player直接依赖低层模块的抽象层接口Icar,不需要知道具体车型,Ford、Benz、Chery修改不会影响Player,只有ICar修改
   * Player才需要改变
   */
	public void play(ICar car) {
    car.shift();
    car.brake();
  }
}

【ICar】

/**
 * 汽车接口,对应DIP的抽象层
 */
public interface ICar {
		car.shift();
    car.brake();
}

【Benz】

/**
 * 奔驰车,对应DIP的低层模块或细节
 * 底层模块依赖于抽象层
 */
public class Benz implements ICar {
  	/**
 		 * shift里面的改变不影响Player
 		 */
    @Override
		public void shift() {
      //
    }
  
  	/**
 		 * brake里面的改变不影响Player
 		 */
    @Override
		public void brake() {
      //
    }
}

【Ford】

/**
 * 奔驰车,对应DIP的低层模块或细节
 * 底层模块依赖于抽象层
 */
public class Benz implements ICar {
  	/**
 		 * shift里面的改变不影响Player
 		 */
    @Override
		public void shift() {
      //
    }
  
  	/**
 		 * brake里面的改变不影响Player
 		 */
    @Override
		public void brake() {
      //
    }
}

【Chery】

/**
 * 奔驰车,对应DIP的低层模块或细节
 * 底层模块依赖于抽象层
 */
public class Chery implements ICar {
  	/**
 		 * shift里面的改变不影响Player
 		 */
    @Override
		public void shift() {
      //
    }
  
  	/**
 		 * brake里面的改变不影响Player
 		 */
    @Override
		public void brake() {
      //
    } 
}

LOP(迪米特法则)

定义:只与你的直接朋友交谈,不跟“陌生人”说话

含义是:如果两个软件实体无须直接通信,那么就不应当发生直接的相互调用,可以通过第三方转发该调用。其目的是降低类之间的耦合度,提高模块的相对独立性。

LOP要求限制软件实体之间通信宽度和深度,正确使用LOP将有以下两个优点:

  • 降低类之间的耦合度,提高了模块(类、模块、子系统、系统等)的相对独立性。
  • 提高了模块可复用率和系统的扩展性。

缺点:过度使用LOP会使系统产生大量的中介类,增加系统了复杂性(如结构、代码),使模块之间的通信效率降低。

*釆用LOP时需要反复权衡,确保高内聚和低耦合的同时,保证系统的结构清晰。

从迪米特法则的定义和特点可知,它强调以下两点:

  • 从依赖者的角度来说,只依赖应该依赖的对象。(依赖中间对象)
  • 从被依赖者的角度说,只暴露应该暴露的方法。(接口提供方只提供调用方需要的方法)

总结:LOP目的在于减少模块之间的依赖,中介者可隐藏后方的复杂性。因此调用方只依赖了中介者而无需依赖后方多个模块,且只提供需要的接口

应用设计模式:

  • 中介者模式
  • 外观模式

例:

  1. 一个中介,客户只要找中介要满足的楼盘 ,而不必跟每个楼盘发生联系。
  2. 无服务中的网关,前端都请求到网关,而不是直接请求具体的微服务。

如何应用设计原则

SOLID是干什么用的(What),具体在什么时候用(When),什么场景用(Where)

SOLID应用场景

设计原则 应用场景 应用说明 描述
SRP(单一职责) 用于类的设计 对象的职责应该是单一的 当我们想出一个类或者设计出一个类的原型后,可通过SRP核对类的设计是否符合SRP原则
OCP(开闭原则) 总的指导思想 对扩展开放,对修改关闭 开闭原则是核心原则,是其他所有原则的基础,其他原则必须先遵守OCP
LSP(里氏替换) 用于指导类继承的设计 程序中的对象是可以在不改变程序正确性的前提下被他的子类替换 当设计类之间的继承关系时,使用LSP来判断你的继承关系设计是否符合LSP要求
ISP(接口隔离) 用于指导接口设计 多个特定功能接口好过于一个功能宽泛的接口 ISP可以看作是SRP的变种,思想是一致的,都强调职责的单一性,而ISP用于指导接口的设计,SRP用于指导类的设计
DIP(依赖反转) 用于指导如何抽象 依赖于抽象而不是实现(面向接口编程) 当设计类之间的依赖(调用)关系时,可以使用DIP来判断这种依赖设计是否符合DIP。
DIP和LSP相辅相成:
1. DIP可用于指导抽象出接口或者抽象类
2. LSP用于指导从接口或者抽象类(也可以是普通类)派生出子类
LOD(迪米特法则) 用于指导类依赖关系 当设计类之间依赖关系,可通过LOP判断它们之间依赖是否存在多余

NOP

NOP, No Overdesign Priciple,不要过渡设计原则

过渡设计案例

架构师眼光长远,预测外来5年的业务变化,最后设计出的结果极为复杂。影响点:开发量大且复杂、测试和运维麻烦、出现问题可能不容易排查。

过渡设计危害:

  • 预测越远,预测结果的准确性越低。
  • 过渡设计会引入不必要的复杂性(运用设计原则可能会引入新的复杂性),如:代码量庞大、代码可阅读行降低、开发周期长、投入产出不成正比。
  • 过渡设计有时候远比设计不足危害更大,设计不足我们还有重构利器,不会出现浪费大量人力、物力的情况;而如果过渡设计原有的投入赞成浪费,其次即使是重构,也需要花费更多的人力物力。

设计模式

Gang of four(Gof):模式描述了一个在我们周围不断重复发生的问题,以及该问题的解决方案的核心。书:《设计模式-可复用面向对象软件基础》

即:模式是不断重复发生问题的解决方案

一个模式包含如下几个部分:

【名称】:模式名称隐含了模式的问题、解决方案、效果等信息

【问题】:问题描述了模式的应用场景,准确的理解模式对应的问题是理解模式的关键,也是实践中应用模式的关键

【解决方案】:描述了问题是如何解决的。

*设计模式不会描述一个具体的设计方案或实现,而是提供设计问题的抽象描述和如何运用一般意义元素组合(类或者对象组合)来解决问题。即:解决方案更关注问题的抽象分析和解决方案,而不是具体的设计实现。

【效果】:包含好的效果的不好的效果,因此使用设计模式可能也会引入新的复杂度。

设计模式分类:创建型模式结构型模式行为型模式

从**《设计模式-可复用面向对象软件基础》**副标题得出:

  • 设计模式解决的是“可复用”的设计问题,如性能设计可用性设计安全设计可靠性设计都不适用
  • 设计模式应用的领域是“面向对象”。

设计模式应用

设计模式应用的问题

非常熟悉设计模式,也能写出设计模式的样例代码,但实际项目设计和开发时,往往就陷入迷茫,不知哪个地方需要运用设计模式。

《设计模式》中23种设计模式只是掌握了设计模式的“”。

如:木匠对工具锯、钻、锤、刨样样精通,但他先要知道在什么地方运用这些工具。

设计模式之道

设计模式之“”就是用于指导我们什么时候用设计模式,为什么要用设计模式,23个设计模式告诉了我们How,而设计模式之道可以告诉我们WhyWhere

Gof《设计模式》:Find what varies and encapsulate it. 翻译:找到变化,封装变化

  1. 找到变化”解决了“在哪里”使用设计模式的问题,即回答了“Where”的问题
  2. 封装变化”解决了“为什么”使用设计模式的问题,即回答了“Why”的问题

面向对象的核心就是拥抱变化、提高扩展性。利用设计模式的目的就是封装变化

变化带来的影响:

  • 变化需要开发,设计不好会导致大量编码和自测工作
  • 测试需要测试变化的部分,关联的不变的部分也需要测试
  • 变化可能引起系统改动,上线后可能会出现问题,导致可用性降低

封装变化的好处:封装变化提升代码的可复用性可扩展性可测试性等。

变化可以存在于模块系统内,封装变化的方式:

  • 类和设计模式封装变化
  • 模块封装变化
  • 系统封装变化

Gof 在《设计模式》中提出中心思想是找到变化、封装变化,两个设计原则,形成一个中心两个基本点

  • 基于接口编程,而不是基于实现编程
  • 优先使用对象组合而不是类继承

学习和应用设计模式

  • 学习设计模式:学习设计模式的时候,必须深入理解是为了解决什么变化引起的问题,然后看设计模式如何应用两个基本点来封装变化。
  • 应用设计模式:找到问题可能变化的地方,再去选择合适的设计模式。

设计模式应用之道法器

设计模式应用之道法器帮助我们如何活用设计模式

找变化、封装变化

:面向接口编程而不是实现;优先使用对象组合而不是继承;

:GOF设计模式、其他解决方法

:Java、C++、UML

例:单体架构应对之道法术器

​ 道:拆分

​ 法:分布式、模块化

​ 术:SOA、微服务

​ 器:SpringBoot

原则 VS 模式

设计原则和设计模式是互补的,体现在:设计原则主要用于指导“类的定义”的设计;设计模式主要用于“类的行为(变化)”设计。

设计原则 设计模式
设计中使用先后顺序 先设计原则 后设计模式
作用 设计程序基础框架 设计程序运行规则
设计包含 类的定义(类、属性、方法)、类关系(封装、继承、多态)、抽象层设计 对象交互(交互)
设计类别 静态设计(此时程序还是死的,没有运行规则) 动态设计(让程序动起来)
4R架构包含关系 4R(Ralation、Role、Rank-类分层) 4R(Rule)
可扩展性 保证软件可扩展性 提高软件可扩展性

先设计原则和设计模式,即现设计好Ralation、Role、Rank等类的定义,再设计具体的交互规则,设计原则和设计模式都是为类做出更好的软件设计。

设计模式示例

观察者模式

【业务】用户发出一条微博后,可能需要完成如下相关的事情

  • 统计微博的数量
  • 将微博推送给粉丝
  • 微博小秘书要审核微博

由于业务变化,以上粗粝可能还会不断增加

【发现变化】加入发微博事一个独立模块完成的,则这个模块本身是稳定的,不会经常变化,但发出微博之后的操作是随时可能变化的。

【传统方法】

传统方法是将所有操作都封装在一个模块内部,发完微博后就开始继续完成后续的处理工作

public class Weibo {
  public static boolean publish(int userId, String content) {
    int weiboId = save(content);
    //统计处理
    Statistics.save(userId, content);
    //发给粉丝
    Message.push(userId, content);
    //微博小秘书审核
    Audit.audit(userId, content);
    return true;
  }
  
  private static int save(String content){
    //TODO 省略
    return 10000;
  }
}

public class Statistics {
	public static int add(int userId, int weiboId) {
    //TODO 统计相关数据,例如将微博总数+1
    return 10000;
  }
}

public class Message {
	public static void push(int userId, int weiboId) {
    //TODO 获取粉丝列表,推送微博消息
    return 10000;
  }
}

public class Audit {
	public static boolean audit(int userId, int weiboId) {
    //TODO 微博小秘书审核微博内容
    return false;
  }
}

传统方法存在如下问题:

  1. 新增变化业务时,Weibo的publish需要同步修改
  2. 当原油变化业务被重构,publish方法同样可能需要修改

【设计模式方法】

设计模式封装变化是Observer模式,中文“观察者模式”或者“发布订阅模式”。即:某个对象对某个“发布者”感兴趣,需要观察发布者状态变化。

/**
 * 发布者
 */
public class Subject{
  protected ArrayList<Observer> observers = new ArrayList<();
  
  public void attah(Observer o) {
    //添加观察者
    // 这里用到了里氏替换原则和依赖反转;面向接口编程
    observers.add(o);
  }
  
  public void detach(Observer o) {
    //删除观察者
    // 这里用到了里氏替换原则和依赖反转;面向接口编程
    observers.remove(o);
  }
  
  public void notifyObservers() {
    //通知所有观察者
    // 这里用到了里氏替换原则和依赖反转;面向接口编程
    for(Observer o: observers){
      o.update();
    }
  }
}

/**
 * 抽象观察者
 */
public class Observer {
  public abstract void update();
}

/**
 * 微博
 */
public class Weibo extends Subject {
  public static boolean publish(int userId, String content) {
    int weiboId = save(content);
    //通知所有观察者,无需像传统方法那样调用各个观察者函数
   notifyObservers();
    return true;
  }
  
  private static int save(String content){
    //TODO 省略
    return 10000;
  }
}

/**
 * 微博小秘书
 * Audit依赖于抽象Observer
 */
public class Audit extends Observer {
  
  private Weibo weibo;
  /**
   * 观察者聚合了一个具体的发布者对象Weibo而不是Subject,在发布者调用通知方法执行update方法时,观察者处理实际发布者对象Weibo数据
   */
  public Audit(Weibo weibo) {
    this.weibo = weibo;
  }
  
  @Override
  private void update(){
    //TODO 审核内容,处理实际发布者对象Weibo数据
  }
}

/**
 * 消息推送
 * Message依赖于抽象Observer
 */
public class Message extends Observer {
  
  private Weibo weibo;
  /**
   * 观察者聚合了一个具体的发布者对象Weibo而不是Subject,在发布者调用通知方法执行update方法时,观察者处理实际发布者对象Weibo数据
   */
  public Message(Weibo weibo) {
    this.weibo = weibo;
  }
  
  @Override
  private void update(){
    //TODO 获取用户粉丝,推送微博信息,处理实际发布者对象Weibo数据
  }
}

/**
 * 统计
 * Statistics依赖于抽象Observer
 */
public class Statistics extends Observer {
  
  private Weibo weibo;
  /**
   * 观察者聚合了一个具体的发布者对象Weibo而不是Subject,在发布者调用通知方法执行update方法时,观察者处理实际发布者对象Weibo数据
   */
  public Statistics(Weibo weibo) {
    this.weibo = weibo;
  }
  
  @Override
  private void update(){
    //TODO 统计相关的数据,如微博总数+1,处理实际发布者对象Weibo数据
  }
}

/**
 * 统计
 */
public class Test  {
  
  public static void main(String[] args){
    Weibo weibo = new Weibo();
    Audit audit = new Audit(weibo);
    Message message = new Message(weibo);
    Statistics statistics = new Statistics(weibo);
    
    weibo.attach(audit);
    weibo.attach(message);
    weibo.attach(statistics);
    
    weibo.publish(10000, "第一条微博");
    weibo.publish(20000, "第二条微博");
    weibo.publish(30000, "第三条微博");
    
    //TODO 统计相关的数据,如微博总数+1,处理实际发布者对象Weibo数据
  }
}

观察者模式:

  • 观察者和被观察者都依赖于抽象,用到了依赖反转原则
  • 父类的引用可以被子类替换,用到了里氏替换

GOF设计模式

变化原因 变化描述 可用设计模式
通过显式地指定一个类来创建对象 在创建对象时指定类名将使你受特定实现的约束,而不是特定接口约束。这会使未来的变化更复杂,为避免这种情况,我们应间接的创建对象。 Abstract Factory、Factory Method、Prototype
对特殊操作的依赖 当你为请求指定一个特殊的操作时,完成该请求的方式就固定下来了。为避免把请求代码写死,你将可以在编译时刻或运行时刻很方便地改变响应请求的方法。 Chain of Resposibility,Command
对硬件和软件平台的依赖 外部的操作系统接口和应用编程接口(API)在不同硬件平台上是不同的。依赖于特定平台的软件很难移植到其他平台,甚至都很难跟上本地平台的更新。所以系统设计使限制其平台相关性就很重要了。 Abstract Factory、Bridge
对对象表示或实现的依赖 依赖于对象的客户在对象发生变化时也需要变化,对客户隐藏这些变化信息能阻止连锁变化。 Abstract Factory、Bridge、Memento、Proxy
算法依赖 算法在开发和复用时常常被扩展、优化和替代。依赖于某个特定算法的对象在算法发生变化时不得不变化。 因此发生变化的地方应该被封装起来。 Builder、Iterator、Strategy、Template Method、Visitor
紧耦合 紧耦合很难被复用,依赖密切,修改模块时需要了解改变其他类。
松耦合提高了类被复用的可能性,易扩展、学习、移植、修改。设计模式采用抽象耦合和分层技术来提高系统的松散耦合。
Abstract Factory、Command、Facade、Mediator、Observer、Chain of Reponsibility
通过生成子类来扩充功能 优先使用对象组合而不是继承(扩展子类),应该优先利用现有对象的能力扩展新功能,过多的对象组合也会导致设计难以理解。 Bride、Chain of Reponsibility、Composite、Decorator、Observer、Strategy
不能方便的对子类进行修改 有时你不得不改变一个难以修改的类,或者可能对类的修改会要求修改其他已存在的类,应避免这种修改变化。 Adapter、Decorator、Visitor

总结

设计原则&设计模式_第2张图片

【参考文献】

  • 李运华著《编程的逻辑-如何运用面向对象方法实现复杂业务需求》

你可能感兴趣的:(编程思想,面向对象编程,设计原则,设计模式,编程思想)