DRY原则,其英文为 Don’t Repaeat Yourself,不要重复自己,将其应用到编程中,即为,不要编写重复的代码

那么,什么是重复的代码,是编码的重复,当然不是,一般来说,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);

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);

return false;

}

}

return true;

}

}

上面的isValidUserName()函数和isValidPassword()函数,其内部的实现逻辑是基本一致的,这样我们就可以说是违反DRY原则的代码吗?

那么如果我们将两个代码进行合二为一,合为了更加通用的代码 isValidUserNameOrPassword(),就更加符合DRY原则了吗?

private boolean isValidUsernameOrPassword(String usernameOrPassword) {

//省略实现逻辑    //跟原来的isValidUsername()或isValidPassword()的实现逻辑一样…

return true;

}

答案是否定的,虽然两个函数的实现细节类似,但是从语义的角度来说,两者不尽相同,

如果在接下里的迭代过程中,需要将密码设计为长度为8-16个字符,可以大小写的呢?那么两者的实现逻辑就不一致了,就需要将合并后函数拆为了两个合并前的函数

当然,对于上面实现重复的代码,可以将其抽象为更加细粒度的代码,维持其复用性

2.功能语义重复

假如一个项目里有两个函数,isValidIp()和checkIpisValid(),功能一致,但是实现的逻辑不一致,是否可行?

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原则,实现逻辑不重复,但是语义重复了,可能出现一个项目中,多处需要对IP进行校验的地方,分别使用了这两种校验方式,那么在修改判断逻辑的时候,可能只修改了一种校验函数,导致使用另一种函数的方法统统出错,降低了代码的可维护性,而且很多新入手的同时,也会因为发现有两个功能类似的函数,从而很苦恼,进而降低了代码的可读性

3.代码执行重复

那么对于代码执行重复,相对难以理解,可以参考下面的例子,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 (!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…

}

}

那么上面代码是否符合规范?

可以看出在checkIfUserExisted()函数中,已经query db了,那么在getUserByEmail函数中,再去查询一次数据库吗?

而且在其中判断了两次Email是否符合规范,这个代码也很冗余,可以提取到UserService中

按照,之前不符合DRY原则的设计,进行了重构,重构后的代码如下:

public class UserService {

private UserRepo userRepo;//通过依赖注入或者IOC框架注入

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…

}

}

4.什么是代码复用性

首先来区分三个概念,代码复用性,代码复用 DRY原则

代码复用表示一种行为,在开发新功能的时候,尽量复用已经存在的代码,代码的复用性表示一段代码可以被复用的特性或者功能,在编写代码的时候,让代码尽可能的可复用,DRY原则是一条原则,不要写重复的代码,三者区别相对来说还是蛮大的

首先,不重复并不代表可复用,即使不存在重复的代码,也不代表里面有可以重用的代码

然后,复用和可复用性,关注角度不同,代码的可重复性,是代码开发者角度来讲的,复用是代码使用者角度讲的.

虽然三者不尽相同,但是都是为了减少代码量,提高代码的可读性,可维护性而准备的

5.如何提高代码的复用性

(1)减少代码的耦合

对于出现耦合的代码,尽可能将其抽取出来,形成单独的模块,提高可复用性

(2)满足单一职责原则

职责不够单一,模块大而全,那么依赖它的代码就会很多,增加了代码的耦合,影响代码的复用性,相反,越细粒度的代码,代码通用性更好,越容易复用

(3)模块化

封装功能独立的代码成一类,或者某一个模块,更加适合提高复用性

(4)业务和非业务逻辑分离

对于和具体业务不相关的代码,很容易复用,对于针对某一业务开发的代码,很难以复用,需要针对这两点进行抽取

(5)通用代码下沉

代码分层后,需要避免交叉调用,只允许上层代码调用下层代码和同层代码,杜绝下层代码调用上层代码

(6)继承,多态,抽象,封装

多利用面向对象的特性,利用继承,抽取公共代码到父类,多态,利用多态来替换代码的实现逻辑,抽象,将代码抽象化的,更容易替换

封装,隐藏可变的细节

(7)应用模板等设计模式

灵活利用设计模式,提高代码的复用性,模板模式利用了多态,灵活替换其中的一部分代码

编写可复用的代码并不简单,如果有需要复用的需求场景,那么根据需求去开发可复用的代码,不算难,但是如果没有复用的需求,我们需要去预测代码如何去复用,就比较有挑战,如果在一开始设计编写之初

就进行投入大量开发成本为了维持可复用性,其实违反了我们说的YAGNI原则

而且,有一个原则 Rule of Three,就是对于没有复用需求的代码,而且未来复用需求并不明确的时候,就先不考虑代码的复用性,而是在之后开发新功能的时候,再去重构,让其变得可复用

也就是,第一次编写代码的时候,不考虑代码的复用性,第二次遇到复用场景的时候,再进行重构使用

除了实现逻辑重复,功能语义重复,代码执行重复外,还有哪些类型的代码重复,是否违反DRY原则

老师已经在上面将DRY原则说的很透彻了,至于还有什么其他类型的代码重复,那我尝试使用单一职责原则来说明,如果一个类的属性和其他类中的属性有所重复,那么建议就是将这两个类中的相同属性进行抽取出来,组成单独的类,进行使用,也降低了耦合性

发表评论

邮箱地址不会被公开。 必填项已用*标注