今天我们要研究的主题是DRY原则。
DRY原则,其英文原文是Don't Repeat Yourself.
,在编程中可以理解为不要写重复的代码。
看似非常简单的一个原则,但其实有很多误区。重复的代码就一定违背DRY原则么?如何提高代码的复用性呢?
我们从三种代码重复的实际具体情况来分析DRY原则的具体应用,这三种代码重复分别是实现逻辑重复、功能语义重复和代码执行重复。
假设现在有一个校验用户名和用户密码的功能,规则基本差不多,代码实现如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
|
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;
}
}
|
上面的isValidUserName()函数和isValidPassword()函数中的代码都是一样的,重复的代码被敲了两次,看起来明显违反DRY原则,完全可以合并到一个方法中。
事实上真的如此吗?
答案是否定的,如果我们将其合到一起,明显违反了单一职责原则和接口隔离原则。假如将来某一天,密码的校验增加了新的逻辑,那么刚才的两个函数实现逻辑就会不相同。
对于这种情况,我们可以将相同校验的部分封装成颗粒度更小的函数。
所谓的功能语义重复就是两段代码或者两个函数,虽然名称不同,实现逻辑不同,但是功能是相同的,比如上一节的校验IP地址是否合法的几个函数。
这种功能语义函数是明显违反DRY原则的,在项目中对于同一个功能我们应该统一实现思路,否则会给其他人带来困扰,同事在阅读相关代码的时候会觉得写代码的人是不是有更高深的考量才会有这样的写法。
前两个例子一个是实现逻辑重复,一个是语义重复,我们再来看第三个例子。其中,UserService 中 login() 函数用来校验用户登录是否成功。如果失败,就返回异常;如果成功,就返回用户信息。
具体代码如下所示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
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...
}
}
|
上面的代码既没有逻辑重复,也没有语义重复,但仍然违反了DRY原则,这是因为代码执行重复了。重复执行最明显的一个地方,就是在 login() 函数中,email 的校验逻辑被执行了两次。
除此之外,代码中还有一处比较隐蔽的执行重复,不知道你发现了没有?实际上,login() 函数并不需要调用 checkIfUserExisted() 函数,只需要调用一次 getUserByEmail() 函数,从数据库中获取到用户的 email、password 等信息,然后跟用户输入的 email、password 信息做对比,依次判断是否登录成功。
实际上,这样的优化是很有必要的。因为 checkIfUserExisted() 函数和 getUserByEmail() 函数都需要查询数据库,而数据库这类的 I/O 操作是比较耗时的。我们在写代码的时候,应当尽量减少这类 I/O 操作。
我们先来区分三个概念,代码复用性(Code Reusability)、代码复用(Code Reuse)和DRY原则。
- 减少代码耦合
- 满足单一职责原则
- 模块化(不仅仅指modules,单个类、函数都可以模块化,独立的模块就像积木,更易复用)
- 业务与非业务逻辑分离(越是与业务无关的代码越容易复用)
- 通用代码下沉
- 继承、多态、抽象、封装
- 应用模板模式等设计模式
实际上,除非有非常明确的复用需求,否则,为了暂时用不到的复用需求,花费太多的时间、精力,投入太多的开发成本,并不是一个值得推荐的做法。这也违反我们之前讲到的 YAGNI 原则。