DRY 原则
DRY 原则(Don’t Repeat Yourself):不要写重复的代码。
实现逻辑重复的代码不一定违反 DRY 原则
如:
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) {
// 同isValidUsername()
}
isValidUserName()
函数和 isValidPassword()
函数看起来明显违反 DRY 原则,
合并两个函数:
private boolean isValidUsernameOrPassword(String usernameOrPassword)
- 合并之后的
isValidUserNameOrPassword()
函数,
负责两件事情:验证用户名和验证密码,违反了 单一职责原则 和 接口隔离原则。 isValidUserName()
和isValidPassword()
两个函数,
虽然从代码实现逻辑上看起来是重复的,但是从语义上并不重复(从功能上来看,这两个函数干的是完全不重复的两件事情)。- 将两个函数的合并,那就会存在潜在的问题。
如果我们修改了密码的校验逻辑,就要把合并后的函数,重新拆成合并前的那两个函数。
尽管代码的实现逻辑是相同的,但语义不同,我们判定它并不违反 DRY 原则。
对于包含重复代码的问题,我们可以通过抽象成更细粒度函数的方式来解决。
功能语义重复违反 DRY 原则
在同一个项目代码中有下面两个函数:isValidIp()
和 checkIfIpValid()
。
尽管两个函数的命名不同,实现逻辑不同,但功能是相同的,都是用来判定 IP 地址是否合法的。
尽管两段代码的实现逻辑不重复,但语义重复,也就是功能重复,我们认为它违反了 DRY 原则。
代码执行重复违反 DRY 原则
如,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...
}
}
上面这段代码,既没有逻辑重复,也没有语义重复,但仍然违反了 DRY 原则。
这是因为代码中存在 执行重复。
-
在
login()
函数中,email 的校验逻辑被执行了两次。一次是在调用checkIfUserExisted()
函数的时候,另一次是调用getUserByEmail()
函数的时候。
这个问题解决起来比较简单,我们只需要将校验逻辑从UserRepo
中移除,统一放到UserService
中就可以了。 -
login()
函数并不需要调用checkIfUserExisted()
函数,只需要调用一次getUserByEmail()
函数,
从数据库中获取到用户的 email、password 等信息,然后跟用户输入的 email、password 信息做对比,依次判断是否登录成功。checkIfUserExisted()
函数和getUserByEmail()
函数都需要查询数据库,而数据库这类的 I/O 操作是比较耗时的。我们在写代码的时候,应当尽量减少这类 I/O 操作。
重构后的代码:
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...
}
}