【设计模式】设计原则之K.Y.D.L 四原则

K.Y.D.L 四原则

K:KISS(Keep it Simple and Stupid)简单原则
Y:YAGNI(You Ain't Gonna Need It)不编写不需要代码原则
D:DRY(Don't repeat yourself)不要重复代码原则
L:LOD(Law of Demter)迪米特原则(最少知识原则)

1. KISS(Keep it Simple and Stupid)原则

1.1 定义

尽量保持简单。

1.2 KISS 中简单的含义

1. 代码行数越少越简单?

判断 IP 地址是否合法的三种实现方式:

// 第一种实现方式: 使用正则表达式
public boolean isValidIpAddressV1(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 isValidIpAddressV2(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;
}

// 第三种实现方式: 不使用任何工具类
public boolean isValidIpAddressV3(String ipAddress) {
  char[] ipChars = ipAddress.toCharArray();
  int length = ipChars.length;
  int ipUnitIntValue = -1;
  boolean isFirstUnit = true;
  int unitsCount = 0;
  for (int i = 0; i < length; ++i) {
    char c = ipChars[i];
    if (c == '.') {
      if (ipUnitIntValue < 0 || ipUnitIntValue > 255) return false;
      if (isFirstUnit && ipUnitIntValue == 0) return false;
      if (isFirstUnit) isFirstUnit = false;
      ipUnitIntValue = -1;
      unitsCount++;
      continue;
    }
    if (c < '0' || c > '9') {
      return false;
    }
    if (ipUnitIntValue == -1) ipUnitIntValue = 0;
    ipUnitIntValue = ipUnitIntValue * 10 + (c - '0');
  }
  if (ipUnitIntValue < 0 || ipUnitIntValue > 255) return false;
  if (unitsCount != 3) return false;
  return true;
}

第一种使用正则表达式实现的方式,代码行数确定较少,但由于正则表达式本身较复杂,难以理解,所以,整个代码的实现并不简单。这种实现方式导致代码的可读性和可维护性变差,并不符合 KISS 原则。

第二种和第三种实现思路是差不多的,唯一的区别是第二种实现方式使用了工具类,而第三种完全是原生实现。第二种实现方式相比第三种,逻辑更加清晰,更容易让人理解,所以,相比较而言,第二种更“简单”,更加符合 KISS 原则。

2. 代码逻辑复杂就违背 KISS 原则

// KMP algorithm: a, b分别是主串和模式串;n, m分别是主串和模式串的长度。
public static int kmp(char[] a, int n, char[] b, int m) {
  int[] next = getNexts(b, m);
  int j = 0;
  for (int i = 0; i < n; ++i) {
    while (j > 0 && a[i] != b[j]) { // 一直找到a[i]和b[j]
      j = next[j - 1] + 1;
    }
    if (a[i] == b[j]) {
      ++j;
    }
    if (j == m) { // 找到匹配模式串的了
      return i - m + 1;
    }
  }
  return -1;
}

// b表示模式串,m表示模式串的长度
private static int[] getNexts(char[] b, int m) {
  int[] next = new int[m];
  next[0] = -1;
  int k = -1;
  for (int i = 1; i < m; ++i) {
    while (k != -1 && b[k + 1] != b[i]) {
      k = next[k];
    }
    if (b[k + 1] == b[i]) {
      ++k;
    }
    next[i] = k;
  }
  return next;
}

KMP 是一个高效的匹配单模式字符串的算法,其实现本来就比较复杂,但效率却非常高。如果对于处理长文本字符串匹配这类复杂问题,使用 KMP 算法也就是本身就复杂的问题,用复杂的方法解决,并不违反 KISS 原则

如果在平时的开发中,只是简单的字符串匹配,这种情况下,再使用 KMP 算法,那就算是违背 KISS 原则了。

从此可以看出,是否违反某个设计原则,主要还是取决于当前的应用场景。

1.3 如何写出满足 KISS 原则的代码

  1. 尽量不要使用同事不懂的技术来实现代码,如:正则表达式...
  2. 不要重复造轮子,要善于使用已有的工具类库
  3. 避免过度优化来牺牲代码的可读性

2. YAGNI(You Ain't Gonna Need It)

2.1 定义

不要去设计当前用不到的功能;不要去编写当前用来到的代码。核心思想就是:不要过度设计。

2.2 例子

配置文件

目前系统暂时使用 Redis 来存储配置信息,以后可能使用到 ZooKeeper。如果根据 YAGNI 原则,在未用到 ZooKeeper 之前,没有必要提前写好这部分代码。当然,我们还是要预留好扩展点,等到需要的时候,再去实现 ZooKeeper 这部分的代码。

依赖开发包

通常,项目中会依赖很多第三方的开发包,而有些开发者嫌每次添加依赖配置较麻烦,往往会添加一个大而全的依赖配置,而将一些项目中根本用不到的第三方类库也添加到项目中去。这样做是违反 YAGNI 设计原则的。

2.3 YAGNI 和 KISS 的区别

KISS 原则讲的是“如何做”的问题(尽可能保持简单)。

YAGNI 原则讲的是“要不要做”的问题(当前不需要的就不要做)。

3. DRY(Don't repeat youself)原则

3.1 定义

不要写重复的代码。

3.2 DRY 原则中关于重复的定义

1. 实现逻辑重复

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;
  }
}

上面的 isValidPassword()isValidUsername() 两个函数的实现是一样的,那这种算重复代码么?

实际上是不算的,虽然两者的代码实现是一样的,但两个函数干的其实是两件事情,一个是效验用户名,一个是效验密码。尽管目前两个函数的代码是完全一样的,这也只是说刚好一样而已。以后,随着需求的变更,两个函数的实现逻辑就可能是不一样的。尽管代码的实现逻辑是一样的,但语义不同,所以,其并不违反 DRY 原则。至于包含重复代码的问题,可以通过更小粒度的函数来达到代码复用的目的。

2. 功能语义重复

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;
}

上面的代码,虽然函数名和函数实现都是不一样的,但功能是一样的,都是用来判断 IP 地址是否合法。这种情况,往往是由于开发同学分开开发导致的。由于要实现的功能是完全一样的,即使具体的函数实现不同,也是违反 DRY 原则的,需要将删除其中一个,让整个项目统一使用一个实现。

功能语义重复可能导致的问题:

项目中使用了两个同样功能的不同函数,如果哪天判断的规则变了,只改了一个,而另一个没有被改变,这种情况下,就可以会引入 BUG。

3. 代码执行重复

public class UserService {
  private UserRepo userRepo;//通过依赖注入或者IOC框架注入

  public User login(String email, String password) {
    boolean existed = userRepo.checkIfUserExisted(email, password);
    if (!existed) {
      // ... throw AuthenticationFailureException...
    }
    User user = userRepo.getUserByEmail(email);
    return user;
  }
}

public class UserRepo {
  public boolean checkIfUserExisted(String email, String password) {
    if (!EmailValidation.validate(email)) {
      // ... throw InvalidEmailException...
    }

    if (!PasswordValidation.validate(password)) {
      // ... throw InvalidPasswordException...
    }

    //...query db to check if email&password exists...
  }

  public User getUserByEmail(String email) {
    if (!EmailValidation.validate(email)) {
      // ... throw InvalidEmailException...
    }
    //...query db to get user by email...
  }
}

所谓代码执行重复,就是同一段代码被执行了多次。上面的代码,在 login 函数中 email 的效验被调用了两次,所以,是代码执行重复,属于违反了 DRY 原则。

3.3 什么是代码的复用性

代码的复用性

指的是一段代码可被复用的特性或能力。代码的可复用性,是从代码开发者的角度来讲的。

代码复用

在开发过程中,尽量使用已经存在的代码。代码复用,是从代码使用者的角度来讲的。

DRY 原则

不要写重复的代码。

如何提高代码复用性

  1. 减少代码耦合
  2. 满足单一职责
  3. 模块化
  4. 业务与非业务逻辑分离
  5. 通用代码下沉
  6. 继承、多态、抽象和封装
  7. 应用模块方法等设计模式,复用通用的算骨架
  8. 运用泛型技术编程,提高代码的抽象程度

3.4 Rule of Three

也就是说,第一次编写代码的时候,我们不考虑其复用性;第二次遇到复用场景的时候,再进行重构使其复用。这里的 Three,指的是二,而不是三。

4. 迪米特原则(最少知识原则) LOD(Law of Demeter)

4.1 定义

不该有直接依赖关系的类之间,不要依赖;有依赖关系的类之间,尽量只依赖必要的接口。

4.2 什么是高内聚、松耦合

高内聚用来指导类本身的设计,松耦合用来指导类与类之间依赖关系的设计。高内聚有助于松耦合,松耦合又需要高内聚的支持。

所谓高内聚指的是:相近的功能应该放到同一个类中,不相近的功能不要放到同一个类中。相近的功能往往会被同时修改,放到同一个类中,修改比较集中,代码也容易维护。实际上,单一职责原则是实现代码高内聚的非常有效的设计原则。

所谓松耦合指的是:类与类之间的依赖关系简单清晰。即有依赖关系的两个类,一个类的代码改动不会或很少导致依赖类代码的改动。依赖注入、接口隔离原则、依赖接口而非实现以及迪米特原则都是为了实现代码的松耦合。

4.3 不应该有依赖关系的类之间,不要有依赖例子

public class NetworkTransporter {
    // 省略属性和其他方法...
    public Byte[] send(HtmlRequest htmlRequest) {
      //...
    }
}

public class HtmlDownloader {
  private NetworkTransporter transporter;//通过构造函数或IOC注入
  
  public Html downloadHtml(String url) {
    Byte[] rawHtml = transporter.send(new HtmlRequest(url));
    return new Html(rawHtml);
  }
}

public class Document {
  private Html html;
  private String url;
  
  public Document(String url) {
    this.url = url;
    HtmlDownloader downloader = new HtmlDownloader();
    this.html = downloader.downloadHtml(url);
  }
  //...
}

存在问题一:NetworkTransporter 类作用一个底层通信类,其功能应该尽可能通用。而目前的设计依赖了太具体的 HtmlRequest 类,从这一种来讲,违反了迪米特原则,依赖了不该有直接依赖关系的类。

重构后的 NetworkTransporter

public class NetworkTransporter {
    // 省略属性和其他方法...
    public Byte[] send(String address, Byte[] data) {
      //...
    }
}

存在问题二:Document 类存在三个主要问题。

  1. 构造函数中逻辑过于复杂,耗时长,不应该放在构造函数中,影响代码的可测试性
  2. 所依赖的 HtmlDownloader 对象直接使用 new 的方式来创建,违反了基于接口而非实现编程的设计思想,也会影响代码的可测试性
  3. Document 网页文档没必要依赖 HtmlDownloader 类,违反了迪米特原则

优化后的 Document 类

public class Document {
  private Html html;
  private String url;
  
  public Document(String url, Html html) {
    this.html = html;
    this.url = url;
  }
  //...
}

// 通过一个工厂方法来创建Document
public class DocumentFactory {
  private HtmlDownloader downloader;
  
  public DocumentFactory(HtmlDownloader downloader) {
    this.downloader = downloader;
  }
  
  public Document createDocument(String url) {
    Html html = downloader.downloadHtml(url);
    return new Document(url, html);
  }
}

4.4 有依赖关系的类之间,尽量只依赖必要的接口

public class Serialization {
  public String serialize(Object object) {
    String serializedResult = ...;
    //...
    return serializedResult;
  }
  
  public Object deserialize(String str) {
    Object deserializedResult = ...;
    //...
    return deserializedResult;
  }
}

上面代码没有什么问题,但如果把其放到具体的应用场景中:假设在我们的项目中,有些类只用到了序列化方法,另一些类只用到了反序列化方法,那根据迪米特原则的后半部分“有依赖关系的两个类,尽量依赖必要的接口”,只用到了序列化的类不应该依赖反序列化接口,反之亦然。

满足迪米特原则的优化

public class Serializer {
  public String serialize(Object object) {
    String serializedResult = ...;
    ...
    return serializedResult;
  }
}

public class Deserializer {
  public Object deserialize(String str) {
    Object deserializedResult = ...;
    ...
    return deserializedResult;
  }
}

但满足迪米特原则的优化版本,又违反了高内聚的设计思想,即相近的功能要放到同一个类中,方便统一修改。那如何优化让其即满足高内聚设计思想,又满足迪米特原则呢?

通过接口隔离原则,引入两个接口,再根据多态特性,在使用序列化类的时候,依赖具体的单个u接口,而非具体类。

引入接口隔离原则后的优化版本

public interface Serializable {
  String serialize(Object object);
}

public interface Deserializable {
  Object deserialize(String text);
}

public class Serialization implements Serializable, Deserializable {
  @Override
  public String serialize(Object object) {
    String serializedResult = ...;
    ...
    return serializedResult;
  }
  
  @Override
  public Object deserialize(String str) {
    Object deserializedResult = ...;
    ...
    return deserializedResult;
  }
}

public class DemoClass_1 {
  private Serializable serializer;
  
  public Demo(Serializable serializer) {
    this.serializer = serializer;
  }
  //...
}

public class DemoClass_2 {
  private Deserializable deserializer;
  
  public Demo(Deserializable deserializer) {
    this.deserializer = deserializer;
  }
  //...
}

说明

此文是根据王争设计模式之美相关专栏内容整理而来,非原创。

你可能感兴趣的:(【设计模式】设计原则之K.Y.D.L 四原则)