DRY原则
DRY 原则(Don’t Repeat Yourself),中文直译为:不要重复自己。将它应用在编程中,可以理解为:不要写重复的代码。
或许你可能会想当然的认为,只要代码有重复就是违反 DRY 原则了,真的是这样吗?答案是否定的。实际上,重复的代码不一定违反 DRY 原则,而有些看似不重复的代码也有可能违反 DRY 原则。
DRY 原则的典型代码重复情况有三种:实现逻辑重复、功能语义重复和代码执行重复。
实现逻辑重复
先来看下这段代码是否违反了 DRY 原则,如果违反了,你觉得应该如何重构才能让它满足 DRY 原则。
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 原则。为了移除重复,我们对上面代码做下重构,将两个方法合并为更通用的 isValidUserNameOrPassword()
:
public class UserAuthenticatorV2 {
public void authenticate(String username, String password) {
if (!isValidUsernameOrPassword(username)) {
// ...throw InvalidUsernameException...
}
if (!isValidUsernameOrPassword(password)) {
// ...throw InvalidPasswordException...
}
}
private boolean isValidUsernameOrPassword(String usernameOrPassword) {
// 实现逻辑和上面的一样...
return true;
}
}
重构后没有了重复代码,是否就不违反 DRY 原则呢?答案是否定的,这个方法已经违反了 DRY 原则。
合并之后的方法 isValidUsernameOrPassword()
单从名字看,它负责两件事情:验证用户名和密码,违反了 “单一职责原则” 和 “接口隔离原则”。
isValidUserName()
和 isValidPassword()
两个函数,从代码实现逻辑上看是重复的,但是从语义上并不重复。所谓“语义不重复”指的是:从功能上看,这两个函数干的是完全不重复的两件事情,即功能不重复,一个是校验用户名,另一个是校验密码。如果合并为 isValidUsernameOrPassword()
,在未来的某一天我们修改了密码的校验逻辑,那个时候 isValidUserName()
和 isValidPassword()
实现逻辑就不相同,还是要把合并后的代码重新拆分出来。
尽管代码的实现逻辑是相同的,但语义不同,我们判定它并不违反 DRY 原则。对于包含重复代码的问题,我们可以通过抽象成更细粒度函数的方式来解决。比如将校验只包含a-z、0-9、dot的逻辑封装成函数。
功能语义重复
再来看一个例子。同一个项目代码中有两个函数:isValidIp()
和 checkIfIpValid()
,都是检查IP的合法性。它们是否违反 DRY 原则?
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;
}
这个例子跟上面的例子正好相反。上一个例子是代码实现逻辑重复,但语义不重复,我们并不认为它违反了 DRY 原则。这个例子虽然两段代码的实现逻辑不重复,但语义重复,也就是功能重复,我们认为它违反了 DRY 原则。
在实际项目开发中这很常见,因为两个函数分别由不同的同事开发,如果没有统一一种实现思路,对后续的项目开发相当于是在“埋坑”,可能未来检查IP合法性改了其中一个函数,另外一个函数没有修改。
代码执行重复
再来看第三个例子。UserService
中 login()
用来校验用户是否登陆成功,如果失败就返回异常,否则返回用户信息。
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 (!isEmailValidation.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()
调用 UserRepo 的 checkIfUserExisted()
和 getUserByEmail()
,这两个函数都校验了一次 email。我们可以将校验代码放到 login()
即可。
另外还有一个隐藏的执行重复问题,login()
并不需要调用 checkIfUserExisted()
,只需要调用一次 getUserByEmail()
,从数据库中获取用户的 email、password 等信息跟用户输入的 email、password 信息做对比,依次判断是否登陆成功。
实际上,这样的优化是很有必要的,因为两个函数都查询了数据库,数据库I/O操作是比较耗时的,在写代码的时候应当减少这类I/O操作。
按照刚刚的思路把代码重构一下:
public class UserService {
private UserRepo userRepo;
public User login(String email, String password) {
if (!EmailValidation.validate(email)) {
// ...throw InvalidEmailException...
}
if (!PasswordValidation.validate(password)) {
// ...throw InvalidPasswordException...
}
User user = userRepo.getUserByEmail(email);
if (user == null || !password.equals(user.getPassword()) {
// ...throw AuthenticationFailureException...
}
return user;
}
}
public class UserRepo {
public boolean checkIfUserExisted(String email, String password) {
// ...query db to check if email&password exists
}
public User getUserByEmail(String email) {
// ...query db to get user by email...
}
}
代码复用性(Code Reusability)
什么是代码的复用性?
我们首先来区分三个概念:代码复用性(Code Reusability)、代码复用(Code Reuse)和DRY原则。
-
代码复用表示一种行为:我们在开发新功能的时候,尽量复用已经存在的代码
-
代码的可复用性表示一段代码可被复用的特性或能力:我们在编写代码的时候,让代码尽量可复用
-
DRY 原则是一条原则:不要写重复的代码
它们有点类似,但深究起来三者的区别还是蛮大的。
首先,“不重复” 并不代表 “可复用”。在一个项目代码中,可能不存在任何重复的代码,但也并不表示里面有可复用的代码,不重复和可复用完全是两个概念。所以,从这个角度来说,DRY 原则跟代码的可复用性讲的是两回事。
其次,“复用” 和 “可复用性” 关注角度不同。代码 ”可复用性“ 是从代码开发者的角度来讲的,”复用“ 是从代码使用者的角度来讲的。比如,A同事编写了一个 UrlUtils 类,代码的 ”可复用性“ 很好;B同事在开发新功能的时候,直接 ”复用“ A同事编写的 UrlUtils 类。
尽管复用、可复用性、DRY 原则这三者从理解上有所区别,但实际上要达到的目的都是类似的,都是为了减少代码量,提高代码的可读性、可维护性。
怎么提高代码复用性?
1、减少代码耦合
高度耦合的代码往往会牵一发而动全身,修改或新增一个功能就要牵连到很多其他相关的代码。高度耦合的代码会影响到代码的复用性,我们要尽量减少代码耦合。
2、满足单一职责原则
模块、类设计得而全,那依赖它得代码或者它依赖得代码就会比较多,进而增加了代码的耦合,也影响代码的复用性。相反,越细粒度的代码,代码的通用性会越好,越容易被复用。
3、模块化
这里的 ”模块“,不单单指一组类构成的模块,还可以理解为单个类、函数。我们要善于讲功能独立的代码,封装成模块,独立的模块更容易复用。
4、业务与非业务逻辑分离
越是跟业务无关的代码越是容易复用,越是针对特定业务的代码越难复用。所以,为了复用跟业务无关的代码,我们将业务和非业务逻辑代码分离,抽取成一些通用的框架、类库、组件等。
5、通用代码下沉
越底层的代码越通用、会被越多的模块调用,越应该设计得足够可复用。只允许上层代码调用下层代码及同层代码之间得调用,杜绝下层代码调用上层代码。
6、继承、多态、抽象、封装
越抽象、越不依赖具体的hi先,越容易复用。代码封装成模块,隐藏可变的细节、暴露不变的接口,就越容易复用。
7、应用模板等设计模式
一些设计模式也能提高代码的复用性。比如,模板模式利用了多态来实现,可以灵活替换其中的部分代码,整个流程模板代码可复用。
辩证思考和灵活应用
实际上,除非有非常明确的复用需求,否则,为了暂时用不到的复用需求,花费太多的时间、精力,投入太多的开发成本,并不是一个值得推荐的做法。这也违反我们之前讲到的 YAGNI 原则。
除此之外,有一个著名的原则,叫做 ”Rule Of Three“,`Three" 并不是真的就指确切的 ”三“,只是一个原则名称。这条原则用在编程领域,那就是说,我们在第一次写代码的时候,如果当下没有复用的需求,而未来的复用需求也不是特别明确,并且开发可复用代码的成本比较高,那我们就不需要考虑代码的复用性。在之后我们开发新功能的时候,发现可以复用之前写的这段代码,那我们就重构这段代码,让其变得更加可复用。
总结
1、DRY 原则
DRY 原则典型的三种重复情况:实现逻辑重复、功能语义重复、代码执行重复。实现逻辑重复,但功能语义不重复(即功能不同)的代码,并不违反 DRY 原则。实现逻辑不重复,但功能语义重复的代码,也算是违反 DRY 原则。代码执行重复也算是违反 DRY 原则。
2、代码复用性
提高代码复用性有以下7点:
-
减少代码耦合
-
满足单一职责原则
-
模块化
-
业务与非业务逻辑分离
-
通用代码下沉
-
继承、多态、抽象、封装
-
应用模板等设计模式
我们可以不写可复用的代码,但一定不能写重复的代码。