10k

设计模式之美-课程笔记12-设计原则6-KISS&YAGNI

理论六:我为何说KISS、YAGNI原则看似简单,却经常被用错?

如何理解KISS原则

  • Keep it simple and stupid.
  • Keep it short and simple.
  • Keep it simple and straightforward.

  • 尽量保持简单。是一个万金油的准则,不仅在软件设计中,在更广泛的产品设计也同样适用。

  • 在代码设计中,这个原则就是保持我们代码可读和可维护的重要手段。但是什么样才是simple?

代码行数越少越简单?

  1. 看个例子:检查输入的字符串 ipAddress 是否是合法的 IP 地址。一个合法的 IP 地址由四个数字组成,并且通过“.”来进行分割。每组数字的取值范围是 0~255。第一组数字比较特殊,不允许为 0。
    
    // 第一种实现方式: 使用正则表达式
    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;
    }
  1. 第一种是正则表达式,虽然行数最少,但是使用了复杂的匹配表达式(不易读),所以不符合KISS;

  2. 第二种是使用了一些现成的工具类处理字符串,第三种one pass处理按字符处理。第二种更加易读和清晰,而且第三种更容易写出一些bug。尽管第三种写法效率高一些。

    工具类比较通用,所以在处理上会考虑的更多,执行效率就差一点;而处理字符的底层操作,针对性更强,没有其他的逻辑,所以定制化的这种方式效率相对高点。

    尽管如此,如果这个代码不是系统瓶颈,我们不需要过多纠结于细微的性能问题而让代码变得不易读。

代码逻辑复杂就违背KISS原则吗?

  1. 看个例子:
    // 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;
    }
  1. KMP 算法以快速高效著称。当我们需要处理长文本字符串匹配问题(几百 MB 大小文本内容的匹配),或者字符串匹配是某个产品的核心功能(比如 Vim、Word 等文本编辑器),又或者字符串匹配算法是系统性能瓶颈的时候,我们就应该选择尽可能高效的 KMP 算法。而 KMP 算法本身具有逻辑复杂、实现难度大、可读性差的特点。本身就复杂的问题,用复杂的方法解决,并不违背 KISS 原则。

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

  1. 不使用同事可能不懂的技术来实现代码,比如复杂的正则,过于高级的语法;
  2. 不重复造轮子,善于使用已有的工具类库。
  3. 不要过度优化,过度使用一些奇技淫巧(比如位运算代替算术运算,复杂的条件语句代替if-else,使用一些过于底层的函数)牺牲代码的可读性,来优化代码。

YAGNI跟KISS说的是一回事吗?

  1. You ain't gonna need it. 也相当于一个万金油法则。
  2. 在软件开发过程中,不要去设计当前用不到的功能;不要去编写当前用不到的代码。-> 不要做过度设计
  3. 比如,我们的系统暂时只用 Redis 存储配置信息,以后可能会用到 ZooKeeper。根据 YAGNI 原则,在未用到 ZooKeeper 之前,我们没必要提前编写这部分代码。当然,这并不是说我们就不需要考虑代码的扩展性。我们还是要预留好扩展点,等到需要的时候,再去实现 ZooKeeper 存储配置信息这部分代码。
  4. 再比如不要在项目中引入不需要依赖的开发包。比如为了减少和避免包缺失而频繁的修改Mave等配置文件,提前在项目中引入大量的library。
  5. KISS指导怎么做,YAGNI指导要不要做。
Thoughts? Leave a comment