一些可以提升代码质量的设计原则

算是读书笔记吧

极客时间--设计模式之美


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

A class or module should have a single reponsibility
一个类或者模块只负责完成一个职责(或者功能)

  • 类和模块

其实类和模块都可以看做抽象集合
本质上都是一个领域的抽象,类作为方法的聚合抽象、模块作为类的聚合抽象。

  • 完成单一职责

不要设计大而全的类,要设计粒度小、功能单一的类。
简单来说,一个类包含了两个或者两个以上业务不相干的功能,那我们就说它职责不够单一,应该将它拆分成粒度更小的类。

  • 不要过度拆分

业务是否相干,职责是否单一。很多时候具有主观性,也就是程序员对模块使命的理解。
对于没办法完全说服自己进行拆分的两个功能:

我们可以先写一个粗粒度的类,满足业务需求。随着业务的发展,如果粗粒度的类越来越庞大,代码越来越多,这个时候,我们就可以将这个粗粒度的类,拆分成几个更细粒度的类。这就是所谓的持续重构。

  • 一些可以借鉴的量化指标

  1. 类中的代码行数、函数或属性过多
    会影响代码的可读性和可维护性,我们就需要考虑对类进行拆分;
  2. 类依赖的其他类过多,或者依赖类的其他类过多
    不符合高内聚、低耦合的设计思想,我们就需要考虑对类进行拆分;
  3. 私有方法过多
    我们就要考虑能否将私有方法独立到新的类中,设置为 public 方法,供更多的类使用,从而提高代码的复用性
  4. 比较难给类起一个合适名字
    很难用一个业务名词概括,或者只能用一些笼统的 Manager、Context 之类的词语来命名,这就说明类的职责定义得可能不够清晰
  5. 类中大量的方法都是集中操作类中的某几个属性
    比如,在 UserInfo 例子中,如果一半的方法都是在操作 address 信息,那就可以考虑将这几个属性和对应的方法拆分出来。也能提高代码复用性
  • 一个类多少行代码合适?

这个也很难明确的量化,就像问大厨“放盐少许”中的“少许”是多少一样。

不过当一个类的代码,读起来让你头大了,实现某个功能时不知道该用哪个函数了,想用哪个函数翻半天都找不到了,只用到一个小功能要引入整个类(类中包含很多无关此功能实现的函数)的时候,这就说明类的行数、函数、属性过多了。


接口隔离 -- ISP(Interface Segregation Principle)

Clients should not be forced to depend upon interfaces that they do not use
客户端(使用者)不应该强迫依赖它不需要的接口

  • 一组 API 接口集合

如果部分接口只被部分调用者使用,那我们就需要将这部分接口隔离出来,单独给对应的调用者使用,而不是强迫其他调用者也依赖这部分不会被用到的接口


public interface UserService {
  boolean register(String cellphone, String password);
  boolean login(String cellphone, String password);
  UserInfo getUserInfoById(long id);
  UserInfo getUserInfoByCellphone(String cellphone);
}

// 删除用户接口不应该暴露给客户端
public interface RestrictedUserService {
  boolean deleteUserByCellphone(String cellphone);
  boolean deleteUserById(long id);
}

public class UserServiceImpl implements UserService, RestrictedUserService {
  // ...省略实现代码...
}
  • 把“接口”理解为单个 API 接口或函数

函数的设计要功能单一,不要将多个不同的功能逻辑在一个函数中实现

以一个统计用类来举例:

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 dataSet) {
  Statistics statistics = new Statistics();
  //...省略计算逻辑...
  return statistics;
}

count方法内部会计算各种各样的结果,对单个结果的修改,都需要修改count方法。
如果大部分结果的使用频率不高,那么每次调用count,也会进行许多无用计算。
我们可以把接口更加细化,支持单个条件的获取

public Long max(Collection dataSet) { //... }
public Long min(Collection dataSet) { //... } 
public Long average(Colletion dataSet) { //... }
// ...省略其他统计函数...

  • 把“接口”理解为 OOP 中的接口概念

主要体现在面向接口/协议编程中

不要设计大而全的接口,通过按类型、功能划分的方式细化接口粒度。
在复用性以及扩展性上都有好处。
大而全的接口,也会强迫接入者实现无用方法,不利于后期修改维护。


依赖反转原则

高层模块(high-level modules)不要依赖低层模块(low-level)。高层模块和低层模块应该通过抽象(abstractions)来互相依赖。除此之外,抽象(abstractions)不要依赖具体实现细节(details),具体实现细节(details)依赖抽象(abstractions)。

所谓高层模块和低层模块的划分,简单来说就是,在调用链上,调用者属于高层,被调用者属于低层。

简而言之,依赖反转希望调用者的执行,不依赖被具体的调用者。
这个具体的被调用者,指的是具体的类。
如何能够不依赖具体的类?答案是面向接口编程。二者共同依赖同一个抽象(接口)。

以发电厂为电器供应电力为例
发电厂并不依赖具体的电器,而是通过共同的抽象(电源插口),与具体的电器相连。
在新增电器时,发电厂并不需要对其进行单独的设置,只要把这个电器也接入电源插口即可即可。

这样设计好处有两点:

  1. 低层次模块更加通用,适用性更广
  2. 高层次模块没有依赖低层次模块的具体实现,方便低层次模块的替换

保持简单 -- KISS原则

KISS原则主要想说“如何做”的问题:尽量保持简单

他的描述有好几个
Keep It Simple and Stupid.
Keep It Short and Simple.
Keep It Simple and Straightforward.

  • 并不是代码行数越少肯定好
    比如正则表达式和一些奇技淫巧,他们行数很少,但是维护起来可能要付出大量的精力

  • 不要使用同事可能不懂的技术来实现代码
    如果同时维护你的代码,要花很多时间去学习一门技术,那会大大的降低开发效率

  • 不要重复造轮子,要善于使用已经有的工具类库
    在写一个新轮子之前,看一看项目文档,或者问问同事。
    使用已有的工具类库,或者对其进行扩展。
    不要让项目中同样的功能,出现两个工具类。
    自己去实现这些类库,出 bug 的概率会更高,维护的成本也比较高。

  • 不要过度优化
    不要过度使用一些奇技淫巧来优化代码,牺牲代码的可读性。
    比如,位运算代替算术运算、复杂的条件语句代替 if-else、使用一些过于底层的函数等


过度设计 -- YAGNI原则

YAGNI 原则说的是“要不要做”的问题:当前不需要的就不要做

比如模块的可扩展性、各种设计模式的使用。
如果在可预见的范围内,并不需要就不要那样设计。
当项目的发展超出了预期,再去重构


重复代码 -- DRY原则

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

当代码的某些地方必须更改时,你是否发现自己在多个位置以多种不同格式进行了更改?
你是否需要更改代码和文档,或更改包含其的数据库架构和结构,或者…?
如果是这样,则您的代码不是DRY。

  • 1.实现逻辑重复

比如登录时对于用户名密码的格式校验。

二者分别叫isValidUserName() 函数和 isValidPassword()。初期二者可能校验逻辑相同,所以被copy了两份。


public class UserAuthenticator {
  public void authenticate(String username, String password) {
    if (!isValidUsername(username)) {
      // ...throw InvalidUsernameException...
    }
    if (!isValidPassword(password)) {
      // ...throw InvalidPasswordException...
    }
    //...省略其他代码...
  }

  private boolean isValidUsername(String username) {
    // check not null, not empty
    if (StringUtils.isBlank(username)) {
      return false;
    }
    // check length: 4~64
    int length = username.length();
    if (length < 4 || length > 64) {
      return false;
    }
    // contains only lowcase characters
    if (!StringUtils.isAllLowerCase(username)) {
      return false;
    }
    // contains only a~z,0~9,dot
    for (int i = 0; i < length; ++i) {
      char c = username.charAt(i);
      if (!(c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '.') {
        return false;
      }
    }
    return true;
  }

  private boolean isValidPassword(String password) {
    // check not null, not empty
    if (StringUtils.isBlank(password)) {
      return false;
    }
    // check length: 4~64
    int length = password.length();
    if (length < 4 || length > 64) {
      return false;
    }
    // contains only lowcase characters
    if (!StringUtils.isAllLowerCase(password)) {
      return false;
    }
    // contains only a~z,0~9,dot
    for (int i = 0; i < length; ++i) {
      char c = password.charAt(i);
      if (!(c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '.') {
        return false;
      }
    }
    return true;
  }
}

在合并时,我们要注意不能将其简单的合并成isValidUserNameOrPassword(),这样会导致将来难以维护。

我们可以将其根据具体功能抽象成一个或几个单独的校验函数,分别组装进isValidUserName() 函数和 isValidPassword()中。
比如将校验只包含 a-z、0~9、dot 的逻辑封装成 boolean onlyContains(String str, String charlist); 函数。

  • 功能语义重复

比如两个判断IP地址的函数isValidIp() 和 checkIfIpValid()
尽管两个函数的命名不同,实现逻辑不同,但功能是相同的。


public boolean isValidIp(String ipAddress) {
  if (StringUtils.isBlank(ipAddress)) return false;
  String regex = "^(1\\d{2}|2[0-4]\\d|25[0-5]|[1-9]\\d|[1-9])\\."
          + "(1\\d{2}|2[0-4]\\d|25[0-5]|[1-9]\\d|\\d)\\."
          + "(1\\d{2}|2[0-4]\\d|25[0-5]|[1-9]\\d|\\d)\\."
          + "(1\\d{2}|2[0-4]\\d|25[0-5]|[1-9]\\d|\\d)$";
  return ipAddress.matches(regex);
}

public boolean checkIfIpValid(String ipAddress) {
  if (StringUtils.isBlank(ipAddress)) return false;
  String[] ipUnits = StringUtils.split(ipAddress, '.');
  if (ipUnits.length != 4) {
    return false;
  }
  for (int i = 0; i < 4; ++i) {
    int ipUnitIntValue;
    try {
      ipUnitIntValue = Integer.parseInt(ipUnits[i]);
    } catch (NumberFormatException e) {
      return false;
    }
    if (ipUnitIntValue < 0 || ipUnitIntValue > 255) {
      return false;
    }
    if (i == 0 && ipUnitIntValue == 0) {
      return false;
    }
  }
  return true;
}

尽管两段代码的实现逻辑不重复,但语义重复,也就是功能重复,我们认为它违反了 DRY 原则。
我们应该在项目中,统一一种实现思路,同样语义的代码,都统一调用同一个函数。

  • 3.代码执行重复

很简单,如果一个代码的调用链中。有些无用逻辑的调用或者重复调用。
就需要重构一下,将重复的逻辑抽离出来。

  • 4.过多的过程性注释

写了好多的注释解释代码的执行逻辑,后续修改的这个方法的时候可能,忘记修改注释,造成对代码理解的困难。
实际应用应该使用KISS原则,将方法写的见名知意,尽量容易阅读。注释不必过多。

  • 如何提升代码复用性

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

复用意识也非常重要。
在设计每个模块、类、函数的时候,要像设计一个外部 API 一样去思考它的复用性。

  • Rule of Three

第一次编写代码的时候,我们不考虑复用性;
第二次遇到复用场景的时候,再进行重构使其复用。
需要注意的是,“Rule of Three”中的“Three”并不是真的就指确切的“三”,这里就是指“二”。


高内聚、松耦合 -- 迪米特法则(LOD)

The Least Knowledge Principle:
Each unit should have only limited knowledge about other units: only units “closely” related to the current unit. Or: Each unit should only talk to its friends; Don’t talk to strangers.

最小知识原则
每个模块(unit)只应该了解那些与它关系密切的模块(units: only units “closely” related to the current unit)的有限知识(knowledge)。或者说,每个模块只和自己的朋友“说话”(talk),不和陌生人“说话”(talk)。

  • 不该有直接依赖关系的类之间,不要有依赖
    假如你现在要去商店买东西,你肯定不会直接把钱包给收银员,让收银员自己从里面拿钱,而是你从钱包里把钱拿出来交给收银员。
    具体一些,非必要情况下,不要为了一两个属性传入整个类。
    把类和类偶合起来。

  • 有依赖关系的类之间,尽量只依赖必要的接口(也就是定义中的“有限知识”)
    类似序列化与反序列化的类,二者必须在一个类中实现,当方法较少时没什么问题。
    一旦实现了许多序列化与反序列化的方式,大部分代码只需要用到序列化的功能。
    对于这部分使用者,没必要了解反序列化的“知识”。
    那么,我们可以通过接口隔离原则,用序列化和反序列化两个接口来对两个接口进行隔离。

  • 高内聚
    相近的功能应该放到同一个类中,不相近的功能不要放到同一个类中

  • 松耦合
    类与类之间的依赖关系简单清晰
    即使两个类有依赖关系,一个类的代码改动不会或者很少导致依赖类的代码改动

你可能感兴趣的:(一些可以提升代码质量的设计原则)