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