第二部分:理论七

第二部分:理论七

理论七

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”原则:第一次编写代码的时候,我们不考虑复用性;第二次遇到复用场景的时候,再进行重构使其复用。
posted @ 2021-10-04 13:15  起床睡觉  阅读(86)  评论(0编辑  收藏  举报