第二部分:理论七
第二部分:理论七
理论七
DRY 原则(Don’t Repeat Yourself)
- DRY 原则,英文描述为:Don’t Repeat Yourself。中文直译为:不要重复自己。将它应用在编程中,可以理解为:不要写重复的代码。
- 三种典型的代码重复情况,它们分别是:实现逻辑重复、功能语义重复和代码执行重复。
实现逻辑重复
- 文中举例,用户身份验证类 UserAuthenticator,其中有两个方法:检查用户名 isValidUsername() 和密码 isValidPassword() 合法性。
- 其中 isValidUsername() 和 isValidPassword() 内部逻辑相同,分别检查了输入是否为空、字符串长度、是否小写、是否只包含合法字符。
- 以上两方法的内部代码完全一致,重复代码十分明显,此为实现逻辑重复。
- 但是没有违背 DRY 原则,也不可以合并成一个方法。因为两个方法的语义不重复,一个是校验用户名一个是校验密码,以后很可能校验逻辑不一致,不可合二为一。
- 此处代码的优化倒是可以将方法内部的逻辑做更细粒度的封装,将每种校验都封装成方法,然后分别在两个方法 isValidUsername() 和 isValidPassword() 中调用。
isValidUserName() 函数和 isValidPassword() 函数重复代码示例:
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;
}
}
重构后的代码如下所示:
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;
}
}
功能语义重复
- 文中举例,两个判定 IP 地址是否合法的函数:isValidIp() 和 checkIfIpValid(),尽管两个函数的命名不同,实现逻辑不同,但功能是相同的,都是用来判定 IP 地址是否合法的。
- 之所以在同一个项目中会有两个功能相同的函数,那是因为这两个函数是由两个不同的同事开发的,后来者不知道有前者。
- 尽管两段代码的实现逻辑不重复,但语义重复,也就是功能重复,我们认为它违反了 DRY 原则:
- 项目中有的地方调用 isValidIp(),有的地方调用 checkIfIpValid(),代码既看起来奇怪又给后期维护的同事“埋坑”,增加阅读理解难度。同事觉得功能一样,又不敢轻易合并怕有什么高深的考量,浪费大家时间。
- 另外,如果以后判定 IP 地址是否合法的规则变了,我们只修改了其中一个方法,忘记了修改另一个方法或者压根不知道有另一个方法存在,导致出现一些莫名其妙的 bug。
用来校验 IP 地址是否合法的两个功能相同的函数:
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;
}
代码执行重复
优化前举例
- 类 UserRepo 中有方法:检查用户是否存在 checkIfUserExisted() 和根据邮箱获取用户信息 getUserByEmail()。
- checkIfUserExisted() 中先校验用户邮箱,再校验用户密码
- getUserByEmail() 中校验了用户邮箱
- 类 UserService 中 login() 方法用来校验用户登录是否成功。先调用了userRepo.checkIfUserExisted(),如果用户不存在则返回异常,如果用户存在调用userRepo.getUserByEmail() 返回用户信息。
问题分析
- 以上代码重复执行最明显的一个地方,就是在 login() 函数中,email 的校验逻辑被执行了两次。这个问题解决起来比较简单,我们只需要将校验逻辑从 UserRepo 中移除,统一放到 UserService 中就可以了。
- 另外,login() 函数并不需要调用 checkIfUserExisted() 函数,只需要调用一次 getUserByEmail() 函数,从数据库中获取到用户的 email、password 等信息,然后跟用户的输入对比即可。
- 以上这种减少数据库查询的优化,是十分有必要。
优化后整理
- 类 UserService 中 login() 方法中,先校验用户邮箱,再校验用户密码。然后调用userRepo.getUserByEmail() 返回用户信息,再与用户输入对比。
两处问题:第一处 login() 函数中 email 的校验逻辑被执行了两次,一次是在调用 checkIfUserExisted() 函数的时候,另一次是调用 getUserByEmail() 函数的时候。第二处 login() 函数并不需要调用 checkIfUserExisted() 函数,只需要调用一次getUserByEmail() 函数,代码示例如下:
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...
}
}
代码复用性(Code Reusability)
什么是代码的复用性?
区分三个概念
- 代码复用(Code Resue):表示一种行为,我们在开发新功能的时候,尽量复用已经存在的代码。
- 代码复用性(Code Reusability):表示一段代码可被复用的特性或能力,我们在编写代码的时候,让代码尽量可复用。
- DRY 原则:是一条原则:不要写重复的代码。
总结三者区别
- 首先,“不重复”并不代表“可复用”。
- 其次,“复用”和“可复用性”关注角度不同。代码“可复用性”是从代码开发者的角度来讲的,“复用”是从代码使用者的角度来讲的。
怎么提高代码复用性?
- 减少代码耦合
- 满足单一职责原则
- 模块化
- 业务与非业务逻辑分离
- 通用代码下沉
- 继承、多态、抽象、封装
- 应用模板等设计模式
辩证思考和灵活应用
- 如果我们在编写代码的时候,已经有复用的需求场景,那可能还不算难,如果要编写未来某个功能可复用的代码就比较有挑战了。
- 为了暂时用不到的复用需求,花费太多的时间、精力,投入太多的开发成本。
- Rule of Three”原则:第一次编写代码的时候,我们不考虑复用性;第二次遇到复用场景的时候,再进行重构使其复用。