理论六:我为何说KISS、YAGNI原则看似简单,却经常被用错?
如何理解KISS原则
- Keep it simple and stupid.
- Keep it short and simple.
-
Keep it simple and straightforward.
-
尽量保持简单。是一个万金油的准则,不仅在软件设计中,在更广泛的产品设计也同样适用。
- 在代码设计中,这个原则就是保持我们代码可读和可维护的重要手段。但是什么样才是simple?
代码行数越少越简单?
- 看个例子:检查输入的字符串 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; }
-
第一种是正则表达式,虽然行数最少,但是使用了复杂的匹配表达式(不易读),所以不符合KISS;
-
第二种是使用了一些现成的工具类处理字符串,第三种one pass处理按字符处理。第二种更加易读和清晰,而且第三种更容易写出一些bug。尽管第三种写法效率高一些。
工具类比较通用,所以在处理上会考虑的更多,执行效率就差一点;而处理字符的底层操作,针对性更强,没有其他的逻辑,所以定制化的这种方式效率相对高点。
尽管如此,如果这个代码不是系统瓶颈,我们不需要过多纠结于细微的性能问题而让代码变得不易读。
代码逻辑复杂就违背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 算法以快速高效著称。当我们需要处理长文本字符串匹配问题(几百 MB 大小文本内容的匹配),或者字符串匹配是某个产品的核心功能(比如 Vim、Word 等文本编辑器),又或者字符串匹配算法是系统性能瓶颈的时候,我们就应该选择尽可能高效的 KMP 算法。而 KMP 算法本身具有逻辑复杂、实现难度大、可读性差的特点。本身就复杂的问题,用复杂的方法解决,并不违背 KISS 原则。
如何写出满足KISS原则的代码
- 不使用同事可能不懂的技术来实现代码,比如复杂的正则,过于高级的语法;
- 不重复造轮子,善于使用已有的工具类库。
- 不要过度优化,过度使用一些奇技淫巧(比如位运算代替算术运算,复杂的条件语句代替if-else,使用一些过于底层的函数)牺牲代码的可读性,来优化代码。
YAGNI跟KISS说的是一回事吗?
- You ain't gonna need it. 也相当于一个万金油法则。
- 在软件开发过程中,不要去设计当前用不到的功能;不要去编写当前用不到的代码。-> 不要做过度设计。
- 比如,我们的系统暂时只用 Redis 存储配置信息,以后可能会用到 ZooKeeper。根据 YAGNI 原则,在未用到 ZooKeeper 之前,我们没必要提前编写这部分代码。当然,这并不是说我们就不需要考虑代码的扩展性。我们还是要预留好扩展点,等到需要的时候,再去实现 ZooKeeper 存储配置信息这部分代码。
- 再比如不要在项目中引入不需要依赖的开发包。比如为了减少和避免包缺失而频繁的修改Mave等配置文件,提前在项目中引入大量的library。
- KISS指导怎么做,YAGNI指导要不要做。