第二部分:理论六

理论六

如何理解“KISS 原则”?

  • KISS 原则的英文描述有好几个版本,比如:Keep It Simple and Stupid,翻译成中文就是:尽量保持简单。
  • 代码的可读性和可维护性是衡量代码质量非常重要的两个标准。而KISS 原则就是保持代码可读和可维护的重要手段。

代码行数越少就越“简单”吗?

  • 比如,检查输入的字符串是否是合法的IP地址。
  • 第一种实现方式: 使用正则表达式。
    • 代码行数最少,只有三行代码
    • 但并不是最符合KISS原则的
    • 因为使用正则表达式本身就比较复杂,bug率高
    • 并不是每个程序员都精通正则表达式,会导致代码的可读性和可维护性变差
  • 第二种实现方式: 使用现成的工具类,实现方式更加“简单”,更加符合 KISS 原则。
  • 第三种实现方式: 不使用任何工具类
    • 代码行数和第二种差不多
    • 比第二种有难度,容易出bug
    • 可读性差
    • 完全是自己操作底层字符,没有多余函数调用和不必要的处理逻辑,性能更高些
    • 但除非第二种方法调用的工具类成为性能瓶颈代码,第三种实际上是一种过度优化,牺牲了代码的可读性和实现难度,投入产出比不高

检查输入的字符串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;
}

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

  • 比如,KMP 字符串匹配算法,是一个查找字符串匹配的算法,具体什么法没看懂
  • KMP 算法本身具有逻辑复杂、实现难度大、可读性差的特点。本身就复杂的问题,用复杂的方法解决,并不违背 KISS 原则。
  • 不过,平时的项目开发中涉及的字符串匹配问题,大部分都是针对比较小的文本,直接调用编程语言提供的现成的字符串匹配函数就足够了,如果非得用复杂的KMP算法,那真的违背KISS原则了

KMP 字符串匹配算法:

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

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

  • 不要使用同事可能不懂的技术来实现代码。比如前面例子中的正则表达式,还有一些编程语言中过于高级的语法等。
  • 不要重复造轮子,要善于使用已经有的工具类库。经验证明,自己去实现这些类库,出 bug 的概率会更高,维护的成本也比较高。
  • 不要过度优化。不要过度使用一些奇技淫巧(比如,位运算代替算术运算、复杂的条件语句代替 if-else、使用一些过于底层的函数等)来优化代码,牺牲代码的可读性。
  • 评判代码是否简单,还有一个很有效的间接方法,那就是 code review。
  • 一定不要过度设计,不要觉得简单的东西就没有技术含量。

YAGNI 跟 KISS 说的是一回事吗?

  • YAGNI 原则的英文全称是:You Ain’t Gonna Need It。直译就是:你不会需要它。
  • 这条原则的核心思想就是:不要做过度设计。
  • 比如,我们的系统暂时只用 Redis 存储配置信息,以后可能会用到 ZooKeeper。根据 YAGNI 原则,在未用到 ZooKeeper 之前,我们没必要提前编写这部分代码。当然,这并不是说我们就不需要考虑代码的扩展性。
  • 再比如,我们不要在项目中提前引入不需要依赖的开发包。
  • KISS 原则讲的是“如何做”的问题(尽量保持简单),而 YAGNI 原则说的是“要不要做”的问题(当前不需要的就不要做)。