3.6 KISS原则
3.6.1 如何理解KISS原则?
-
KISS 原则的英文描述有好几个版本:
- Keep It Simple and Stupid.
- Keep It Short and Simple.
- Keep It Simple and Straightforward.
意思其实差不多,翻译成中文就是:尽量保持简单。
3.6.2 代码行数越少就越“简单”吗?
-
例子:三段代码可以实现同样一个功能:检查输入的字符串 ipAddress 是否是合法的 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; }
- 第一种实现方式利用的是正则表达式。虽然代码行数最少,看似最简单,实际上却很复杂。这正是因为它使用了正则表达式:
- 一方面正则表达式本身是比较复杂的,写出完全没有 bug 的正则表达本身就比较有挑战;
- 另一方面,并不是每个程序员都精通正则表达式。对于不怎么懂正则表达式的同事来说,看懂并且维护这段正则表达式是比较困难的。
- 这种实现方式会导致代码的可读性和可维护性变差,所以,从 KISS 原则的设计初衷上来讲,这种实现方式并不符合 KISS 原则。
- 第二种实现方式使用了 StringUtils 类、Integer 类提供的一些现成的工具函数,来处理 IP 地址字符串:
- 第三种实现方式,不使用任何工具函数,而是通过逐一处理 IP 地址中的字符,来判断是否合法。
- 从代码行数上来说,这两种方式差不多。但是,第三种要比第二种更加有难度,更容易写出 bug。从可读性上来说,第二种实现方式的代码逻辑更清晰、更好理解。所以,在这两种实现方式中,第二种实现方式更加“简单”,更加符合 KISS 原则。
- 第三方实现方式性能可能更好,但属于过度优化,除非这个功能成为性能瓶颈,否则这样的优化投入产出比不高。
- 第一种实现方式利用的是正则表达式。虽然代码行数最少,看似最简单,实际上却很复杂。这正是因为它使用了正则表达式:
3.6.3 代码逻辑复杂就违背 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算法实现代码,当字符串匹配是某个产品的核心功能,或这部分功能为性能瓶颈的时候,就应该选择高性能的复杂的算法。
- 本身就复杂的问题,用复杂的方法解决,并不违背 KISS 原则。
3.6.4 如何写出满足 KISS 原则的代码?
- 不要使用同事可能不懂的技术来实现代码。比如前面例子中的正则表达式,还有一些编程语言中过于高级的语法等。
- 不要重复造轮子,要善于使用已经有的工具类库。经验证明,自己去实现这些类库,出 bug 的概率会更高,维护的成本也比较高。不要过度优化。
- 不要过度使用一些奇技淫巧(比如,位运算代替算术运算、复杂的条件语句代替 if-else、使用一些过于底层的函数等)来优化代码,牺牲代码的可读性。
3.6.5 YAGNI 跟 KISS 说的是一回事吗?
- YAGNI 原则的英文全称是:You Ain’t Gonna Need It。直译就是:你不会需要它。
- 核心思想就是:不要做过度设计
- 比如,系统暂时只用 Redis 存储配置信息,以后可能会用到 ZooKeeper。根据 YAGNI 原则,在未用到 ZooKeeper 之前,没必要提前编写这部分代码。这并不是说就不需要考虑代码的扩展性。还是要预留好扩展点,等到需要的时候,再去实现 ZooKeeper 存储配置信息这部分代码。
- KISS 原则讲的是“如何做”的问题(尽量保持简单),而 YAGNI 原则说的是“要不要做”的问题(当前不需要的就不要做)。