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 原则的代码
- 尽量不要使用同事不懂的技术来实现代码,如:正则表达式...
- 不要重复造轮子,要善于使用已有的工具类库
- 避免过度优化来牺牲代码的可读性
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 原则
不要写重复的代码。
如何提高代码复用性
- 减少代码耦合
- 满足单一职责
- 模块化
- 业务与非业务逻辑分离
- 通用代码下沉
- 继承、多态、抽象和封装
- 应用模块方法等设计模式,复用通用的算骨架
- 运用泛型技术编程,提高代码的抽象程度
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 类存在三个主要问题。
- 构造函数中逻辑过于复杂,耗时长,不应该放在构造函数中,影响代码的可测试性
- 所依赖的 HtmlDownloader 对象直接使用 new 的方式来创建,违反了基于接口而非实现编程的设计思想,也会影响代码的可测试性
- 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;
}
//...
}
说明
此文是根据王争设计模式之美相关专栏内容整理而来,非原创。