设计模式之美学习-DRY原则(十)
什么是DRY
Don’t Repeat Yourself 不要定义重复代码
实现逻辑重复
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; } }
上面验证用户名和密码2个方法都是一模一样,看起来像是重复代码 违反了DRY原则 下面重构一下
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) { //省略实现逻辑 //跟原来的isValidUsername()或isValidPassword()的实现逻辑一样... return true; } }
合并之后 代码简单了 也没有重复代码,实际上合并之后依然后问题 违反了“单一职责原则”和“接口隔离原则”。
因为 isValidUserName() 和 isValidPassword() 两个函数,虽然从代码实现逻辑上看起来是重复的,但是从语义上并不重复。所谓“语义不重复”指的是:从功能上来看,这两个函数干的是完全不重复的两件事情,一个是校验用户名,另一个是校验密码。尽管在目前的设计中,两个校验逻辑是完全一样的,但如果按照第二种写法,将两个函数的合并,那就会存在潜在的问题。在未来的某一天,如果我们修改了密码的校验逻辑,比如,允许密码包含大写字符,允许密码的长度为 8 到 64 个字符,那这个时候,isValidUserName() 和 isValidPassword() 的实现逻辑就会不相同。我们就要把合并后的函数,重新拆成合并前的那两个函数。尽管代码的实现逻辑是相同的,但语义不同,我们判定它并不违反 DRY 原则。对于包含重复代码的问题,我们可以通过抽象成更细粒度函数的方式来解决。比如将校验只包含 a~z、0~9、dot 的逻辑封装成 boolean onlyContains(String str, String charlist); 函数。
功能语义重复
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验证的情况下 又重复定义了一个验证ip 虽然实现逻辑不重复但是语义重复了
尽管两段代码的实现逻辑不重复,但语义重复,也就是功能重复,我们认为它违反了 DRY 原则。我们应该在项目中,统一一种实现思路,所有用到判断 IP 地址是否合法的地方,都统一调用同一个函数。
会出现2套逻辑 不同的地方在用 如果哪一天 ip验证验证规则要修改.如果漏改了其中一个就会有问题.
在实际项目中也遇到有这样的问题。比如优惠券发卷 存在 单个发卷 批量发卷 等几个方法 不同的地方调用 发卷更改数量改为异步 有一次就漏掉了 批量发卷 就是违反了此原则
代码执行重复
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... } }
getUserByEmail和checkIfUserExisted验证email重复了 ,还有查询了2次db 执行重复了
优化后
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... } }
不过我感觉这个例子不太好 我个人理解getUserByEmail checkIfUserExisted 的校验逻辑是应该存在的,因为这2个方法可能不止这一个地方调用,方法保证自身逻辑正常执行的校验是应该的。