Design Patterns: Design Principles

SOLID原则

SOLID原则分别指:单一职责原则、开闭原则、里式替换原则、接口隔离原则和依赖反转原则。

SRP: 单一职责原则

单一职责原则(Single Responsibility Principle,缩写为SRP),一个类或者模块只完成一个职责或者功能,不要设计大而全的类,要设计粒度小、功能单一的类。 是为了实现代码高内聚、低耦合,提高代码的复用性、可读性、可维护性。

当出现下面的情况时可能说明类的设计不满足SRP:

  • 类中的低吗行数、行数或者属性过多
  • 类依赖的其他类过多,或者依赖类的其他类过多
  • 私有方法过多
  • 比较难给类起一个合适的名字
  • 类中大量的方法都是集中操作类中的几个属性

可以先写一个粗粒度的类,满足业务需求,随着业务的发展,如果粗粒度的类越来越大,代码越来越多,可以将粗粒度的类拆分成几个更细粒度的类,这叫代码重构

开闭原则

开闭原则(Open Closed Principle,简写OCP),添加一个新的功能,应该是通过在已有代码基础上扩展代码(新增模块、类、方法、属性等),而非修改已有代码(修改模块、类、方法、属性等)的方式来完成。

需要注意:

  • 开闭原则并不是说完全杜绝修改,而是以最小的修改代码的代价来完成新功能的开发
  • 同样的代码改动,在粗代码粒度下,可能被认定为“修改”;在细代码粒度下,可能又被认定为“扩展”

我们要时具备扩展意识、抽象意识、封装意识。

很多设计原则、设计思想、设计模式,都是以提供代码的扩展性为最终目的的。最常用来提高代码扩展性的方法有:多态、依赖注入、基于接口而非实现编程,以及大部分的设计模式。

// 这一部分体现了抽象意识
public interface MessageQueue { //... }
public class KafkaMessageQueue implements MessageQueue { //... }
public class RocketMQMessageQueue implements MessageQueue {//...}

public interface MessageFormatter { //... }
public class JsonMessageFormatter implements MessageFormatter {//...}
public class MessageFormatter implements MessageFormatter {//...}

public class Demo {
  private MessageQueue msgQueue; // 基于接口而非实现编程
  public Demo(MessageQueue msgQueue) { // 依赖注入
    this.msgQueue = msgQueue;
  }
  
  // msgFormatter:多态、依赖注入
  public void sendNotification(Notification notification, MessageFormatter msgFormatter) {
    //...
  }
}

对扩展开放是为了应对变化(需求),对修改关闭是为了保证已有代码的稳定性,最终结果是为了让系统更有弹性

里式替换原则

里式替换原则(Liskov SUbstitution Principle,简称LSP),核心是”design by contract“,按照协议来设计,指子类对象能够替换程序中父类对象出现的任何地方,并且保证原来程序的逻辑行为(behavior)不变及正确性不被破坏。

多态是面向对象编程的一大特性,也是面向对象编程语言的一种语法。它是一种代码实现的思路。而里式替换是一种设计原则,是用来指导继承关系中子类该如何设计的,子类的设计要保证在替换父类的时候,不改变原有程序的逻辑以及不破坏原有程序的正确性

违反里式替换原则的例子:

  • 之类违背父类声明要实现的功能
  • 之类违背父类对输入、输出、异常的约定
  • 子类违背父类注释中所罗列的任何特殊说明

判断违背里式替换原则的小窍门,用父类的单元测试去验证子类的代码,如果某些单元测试运行失败,就有可能说明,子类设计实现没有完全遵守父类的约定,子类有可能违背了里式替换原则。

接口隔离原则

接口隔离原则(Interface Segregation Principle,简称ISP),指接口的调用者或者使用者不应该强迫依赖它不需要的接口。

接口的三种理解:

如果把“接口”理解为一组接口集合,可以是某个微服务的接口,也可以是某个类库的接口等。如果部分接口只被部分调用者使用,我们就需要将这部分接口隔离出来,单独给这部分调用者使用,而不强迫其他调用者也依赖这部分不会被用到的接口。

如果把“接口”理解为单个 API 接口或函数,部分调用者只需要函数中的部分功能,那我们就需要把函数拆分成粒度更细的多个函数,让调用者只依赖它需要的那个细粒度函数。

如果把“接口”理解为 OOP 中的接口,也可以理解为面向对象编程语言中的接口语法。那接口的设计要尽量单一,不要让接口的实现类和调用者,依赖不需要的接口函数。

单一职责原则针对的是模块、类、接口的设计。接口隔离原则相对于单一职责原则,一方面更侧重于接口的设计,另一方面它的思考角度也是不同的。接口隔离原则提供了一种判断接口的职责是否单一的标准:通过调用者如何使用接口来间接地判定。如果调用者只使用部分接口或接口的部分功能,那接口的设计就不够职责单一。

依赖反转原则

控制反转

控制反转(Inversion of Control,简称IOC),是一个比较笼统的设计思想,并不是一种具体的实现方法,一般用来指导框架层面的设计。这里所说的“控制”指的是对程序执行流程的控制,而“反转”指的是在没有使用框架之前,程序员自己控制整个程序的执行。在使用框架之后,整个程序的执行流程通过框架来控制。流程的控制权从程序员“反转”给了框架。

依赖注入

依赖注入(Dependency Injection,简称为DI),和控制反转恰恰相反,它是一种具体的编码技巧。我们不通过 new 的方式在类内部创建依赖类的对象,而是将依赖的类对象在外部创建好之后,通过构造函数、函数参数等方式传递(或注入)给类来使用。


// 非依赖注入实现方式
public class Notification {
  private MessageSender messageSender;
  
  public Notification() {
    this.messageSender = new MessageSender(); //此处有点像hard code
  }
  
  public void sendMessage(String cellphone, String message) {
    //...省略校验逻辑等...
    this.messageSender.send(cellphone, message);
  }
}

public class MessageSender {
  public void send(String cellphone, String message) {
    //....
  }
}
// 使用Notification
Notification notification = new Notification();

// 依赖注入的实现方式
public class Notification {
  private MessageSender messageSender;
  
  // 通过构造函数将messageSender传递进来
  public Notification(MessageSender messageSender) {
    this.messageSender = messageSender;
  }
  
  public void sendMessage(String cellphone, String message) {
    //...省略校验逻辑等...
    this.messageSender.send(cellphone, message);
  }
}
//使用Notification
MessageSender messageSender = new MessageSender();
Notification notification = new Notification(messageSender);

改进版本,基于接口而非实现的方式:


public class Notification {
  private MessageSender messageSender;
  
  public Notification(MessageSender messageSender) {
    this.messageSender = messageSender;
  }
  
  public void sendMessage(String cellphone, String message) {
    this.messageSender.send(cellphone, message);
  }
}

public interface MessageSender {
  void send(String cellphone, String message);
}

// 短信发送类
public class SmsSender implements MessageSender {
  @Override
  public void send(String cellphone, String message) {
    //....
  }
}

// 站内信发送类
public class InboxSender implements MessageSender {
  @Override
  public void send(String cellphone, String message) {
    //....
  }
}

//使用Notification
MessageSender messageSender = new SmsSender();
Notification notification = new Notification(messageSender);

依赖注入框架

我们通过依赖注入框架提供的扩展点,简单配置一下所有需要的类及其类与类之间依赖关系,就可以实现由框架来自动创建对象、管理对象的生命周期、依赖注入等原本需要程序员来做的事情。

依赖反转原则

依赖反转原则(Dependency Insertion Principle,简称为DIP),也叫作依赖倒置原则。这条原则跟控制反转有点类似,主要用来指导框架层面的设计。高层模块不依赖低层模块,它们共同依赖同一个抽象。抽象不要依赖具体实现细节,具体实现细节依赖抽象。

KISS原则

KISS原则英文版本:

  • Keep It Simple and Stupid
  • Keep It Short and Simple
  • Keep It Simple and Straightforward

是保持代码可读和可维护的重要手段。KISS 原则中的“简单”并不是以代码行数来考量的。代码行数越少并不代表代码越简单,我们还要考虑逻辑复杂度、实现难度、代码的可读性等。

本身就复杂的问题,用复杂的方法解决,并不违背 KISS 原则。同样的代码,在某个业务场景下满足 KISS 原则,换一个应用场景可能就不满足了。

  • 不要使用同事可能不懂的技术来实现代码
  • 不要重复造轮子,要善于使用已经有的工具类库
  • 不要过度优化

YANGNI原则(You Ain’t Gonna Need It),指不需要去设计当前用不到的功能,不要去编写当前用不到的代码,即不要过度设计。

KISS原则讲的是“如何做”的问题,尽量保持简单,而YANGNI原则说的是“要不要做”的问题,当前不需要的就不要做。

DRY原则

不要重复造轮子(Don’t Repeat Yourself,简称为DRY),

实现逻辑重复、功能语义重复、代码执行重复。实现逻辑重复,但功能语义不重复的代码,并不违反 DRY 原则。实现逻辑不重复,但功能语义重复的代码,也算是违反 DRY 原则。除此之外,代码执行重复也算是违反 DRY 原则。

代码复用(Code Reuse)表示我们在开发新功能的时候,尽量复用已经存在的代码。

代码可复用性表示一段代码可被复用的特性或者能力

DRY原则是不要写重复的代码。

三者的区别:

  • 不重复不代表可复用
  • 复用和可复用性关注角度不同
    • 可复用性从代码开发者的角度来说
    • 复用从代码使用者的角度来说

提高代码复用性的方法:

  • 减少代码耦合
  • 满足单一职责原则
  • 模块化
  • 业务与非业务逻辑分离
  • 通用代码下沉
  • 继承、多态、抽象、封装
  • 应用模板等设计模式

我们可以不写可复用的代码,但一定不能写重复的代码。

迪米特法则

“高内聚、松耦合”是一个非常重要的设计思想,能够有效提高代码的可读性和可维护性,缩小功能改动导致的代码改动范围。

“高内聚”用来指导类本身的设计,“松耦合”用来指导类与类之间依赖关系的设计。

所谓高内聚,就是指相近的功能应该放到同一个类中,不相近的功能不要放到同一类中。相近的功能往往会被同时修改,放到同一个类中,修改会比较集中。所谓松耦合指的是,在代码中,类与类之间的依赖关系简单清晰。即使两个类有依赖关系,一个类的代码改动也不会或者很少导致依赖类的代码改动。

迪米特法则(Law of Demeter,简称为LOD),也叫最小知识原则:The Least Knowledge Principle。

不该有直接依赖关系的类之间,不要有依赖;有依赖关系的类之间,尽量只依赖必要的接口。迪米特法则是希望减少类之间的耦合,让类越独立越好。每个类都应该少了解系统的其他部分。一旦发生变化,需要了解这一变化的类就会比较少。

Reference

Note: Cover Picture