首页 设计原则
文章
取消

设计原则

SOLID

  1. 单一职责原则
  2. 开闭原则
  3. 里式替换原则
  4. 接口隔离原则
  5. 依赖反转原则

单一职责原则(Single Responsibility Principle)

A class or module should have a single responsibility,即保持类或者模块的职责单一。

何谓职责单一

依托现实场景为依据,当一个类包含了两个或两个以上业务不相关的功能,其职责就不是单一的。

如何以现实场景为依据,判断一个类是否职责单一

以下面的 UserInfo 为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class UserInfo {
private long userId;
private String username;
private String email;
private String telephone;
private long createTime;
private long lastLoginTime;
private String avatarUrl;
private String provinceOfAddress; // 省
private String cityOfAddress; // 市
private String regionOfAddress; // 区
private String detailedAddress; // 详细地址
// ...省略其他属性和方法...
}

它即包含用户的基本信息,亦包含详细的地址信息,比如省,市,区和详细地址,当地址信息不止用来展示而是需要用在其他功能模块中,比如物流模块,那么在物流模块中用户的其他基本信息其实是多余的,因此要把地址从其中独立出来成为 UserAddress

总结

不同的应用场景、不同阶段的需求背景下,对同一个类的职责是否单一的判定,可能都是不一样的,但归根结底类的设计要是破坏了代码的内聚性增加了代码的维护成本那它就是不好的设计

开闭原则

software entities (modules, classes, functions, etc.) should be open for extension , but closed for modification。即模块、类和方法的设计应该对扩展开发,对修改关闭。

为什么要开闭原则

代码的可扩展性低意味着但需求改变需要经常修改代码,这样会导致调用者跟着修改,但是调用者可能散落在类、模块的各个角落,因此修改的成本为大幅增加,

如何实现开闭原则

开闭原则是为了提高程序的可扩展性,这也是大部分设计模式的目的,也是面向对象四大特性的最终目的,因此灵活的应用设计模式和面向对象特性就是实现开闭原则

如何在项目中或用开闭原则

  1. 如果开发的是一个业务导向的系统,比如金融系统、电商系统、物流系统等,要想识别出尽可能多的扩展点,就要对业务有足够的了解,能够知道当下以及未来可能要支持的业务需求。
  2. 如果你开发的是跟业务无关的、通用的、偏底层的系统,比如,框架、组件、类库,你需要了解“它们会被如何使用?今后你打算添加哪些功能?使用者未来会有哪些更多的功能需求?”等问题。

注意:没必要为一些遥远的、不一定发生的需求去提前买单,做过度设计。

里氏替换

If S is a subtype of T, then objects of type T may be replaced with objects of type S, without breaking the program,即在不破坏程序原本的逻辑,基类对象引用应该能用派生类对象进行替换。另一个描述是Design By Contract,即按照协议来设计

里氏替换和多态的不同

即是否改变程序原本的逻辑,多态是基于不同的子类,而产生不同的代码逻辑。

违反里氏替换的例子

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

接口隔离原则

Clients should not be forced to depend upon interfaces that they do not use,即调用者不应该被强迫依赖它不需要的接口。

接口隔离原则和单一职责原则的区别

前者是在调用者的角度判断类的职责是否单一,如果调用者并需要要接口中的一些功能,那么该接口就不满足接口隔离,我们应该把其中的不需要的功能独立出来成为一个单独的接口。

关于接口的理解

一组 API 接口集合,如微服务提供的一组跟用户相关的 API:

1
2
3
4
5
6
7
8
9
public interface UserService {
  boolean register(String cellphone, String password);
  boolean login(String cellphone, String password);
  UserInfo getUserInfoById(long id);
  UserInfo getUserInfoByCellphone(String cellphone);
}
public class UserServiceImpl implements UserService {
  //...
}

单个的 API 接口或函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Statistics {
   private Long max;
   private Long min;
   private Long average;
   private Long sum;
   private Long percentile99;
   private Long percentile999;
   //...省略constructor/getter/setter等方法...
}

public Statistics count(Collection<Long> dataSet) {
   Statistics statistics = new Statistics();
   //...省略计算逻辑...
   return statistics;
}

面向对象中的接口,如 Java 中的 interface。

依赖反转原则

什么是控制反转

以 Spring IOC 为例:框架提供好一个代码骨架,用来组装对象管理整个执行流程,程序员只需要往预留的扩展点加入自己的业务代码,这里“控制”指的是对程序执行流程的控制,而“反转”指的是在没有使用框架之前,程序员自己控制整个程序的执行。在使用框架之后,整个程序的执行流程可以通过框架来控制。流程的控制权从程序员“反转”到了框架。

需要注意的是控制反转并不是一种具体的实现技巧而是比较笼统的设计思想,它的实现像依赖注入、模板设计模式等

何谓依赖注入

即不通过 new 创建依赖对象,而是在外部创建好之后,通过构造函数或者 Setter 传递给类使用

依赖注入框架

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

依赖反转原则

High-level modules shouldn’t depend on low-level modules. Both modules should depend on abstractions. In addition, abstractions shouldn’t depend on details. Details depend on abstractions,即调用链上层不应该依赖调用链下层,它们俩都应该依赖抽象,除此自外抽象不应该依赖具体实现,实现应该依赖抽象。

举个形象的例子:

Tomcat 是运行 Java Web 应用程序的容器。我们编写的 Web 应用程序代码只需要部署在 Tomcat 容器下,便可以被 Tomcat 容器调用执行。按照之前的划分原则,Tomcat 就是高层模块,我们编写的 Web 应用程序代码就是低层模块。Tomcat 和应用程序代码之间并没有直接的依赖关系,两者都依赖同一个“抽象”,也就是 Servlet 规范。Servlet 规范不依赖具体的 Tomcat 容器和应用程序的实现细节,而 Tomcat 容器和应用程序依赖 Servlet 规范。

YAGNI 和 KISS 原则

  1. YAGNI,You Ain’t Gonna Need It,即不要做过度设计。
  2. KISS,keep it stupid and simple,尽量保持代码简单。

DRY 原则

Don’t Repeat Yourself,即不要写重复的代码

重复的代码

  1. 实现逻辑重复
  2. 功能语义重复
  3. 代码执行重复

迪米特法则

实际上跟接口隔离原则差不多,本质上还是解耦,减少类和类之间不必要的依赖.

总结

实际上设计原则都是为了提高代码的可读性,可扩展性和可维护性,它们是不断打磨的经验,遵循它们我们更容易写出更加易扩展的代码,但也不必过分拘泥设计原则是什么,内容有多少,设计模式才是对设计原则的实现。

本文由作者按照 CC BY 4.0 进行授权

如何进行面向分析、设计和实现

针对业务系统的开发,如何做需求分析和设计(以积分兑换系统为例)