DRY原则

DRY原则

DRY 原则(Don’t Repeat Yourself),中文直译为:不要重复自己。将它应用在编程中,可以理解为:不要写重复的代码。

或许你可能会想当然的认为,只要代码有重复就是违反 DRY 原则了,真的是这样吗?答案是否定的。实际上,重复的代码不一定违反 DRY 原则,而有些看似不重复的代码也有可能违反 DRY 原则。

DRY 原则的典型代码重复情况有三种:实现逻辑重复、功能语义重复和代码执行重复

实现逻辑重复

先来看下这段代码是否违反了 DRY 原则,如果违反了,你觉得应该如何重构才能让它满足 DRY 原则。

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;
    }
}

上面的代码很明显 isValidUsername()isValidPassword() 两个方法是重复的,看起来明显违反 DRY 原则。为了移除重复,我们对上面代码做下重构,将两个方法合并为更通用的 isValidUserNameOrPassword()

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) {
		// 实现逻辑和上面的一样...
		return true;
	}
}

重构后没有了重复代码,是否就不违反 DRY 原则呢?答案是否定的,这个方法已经违反了 DRY 原则。

合并之后的方法 isValidUsernameOrPassword() 单从名字看,它负责两件事情:验证用户名和密码,违反了 “单一职责原则” 和 “接口隔离原则”。

isValidUserName()isValidPassword() 两个函数,从代码实现逻辑上看是重复的,但是从语义上并不重复。所谓“语义不重复”指的是:从功能上看,这两个函数干的是完全不重复的两件事情,即功能不重复,一个是校验用户名,另一个是校验密码。如果合并为 isValidUsernameOrPassword(),在未来的某一天我们修改了密码的校验逻辑,那个时候 isValidUserName()isValidPassword() 实现逻辑就不相同,还是要把合并后的代码重新拆分出来。

尽管代码的实现逻辑是相同的,但语义不同,我们判定它并不违反 DRY 原则。对于包含重复代码的问题,我们可以通过抽象成更细粒度函数的方式来解决。比如将校验只包含a-z、0-9、dot的逻辑封装成函数。

功能语义重复

再来看一个例子。同一个项目代码中有两个函数:isValidIp()checkIfIpValid(),都是检查IP的合法性。它们是否违反 DRY 原则?

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 原则。这个例子虽然两段代码的实现逻辑不重复,但语义重复,也就是功能重复,我们认为它违反了 DRY 原则

在实际项目开发中这很常见,因为两个函数分别由不同的同事开发,如果没有统一一种实现思路,对后续的项目开发相当于是在“埋坑”,可能未来检查IP合法性改了其中一个函数,另外一个函数没有修改。

代码执行重复

再来看第三个例子。UserServicelogin() 用来校验用户是否登陆成功,如果失败就返回异常,否则返回用户信息。

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 (!isEmailValidation.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
	}
}

从上面代码可以发现,login() 调用 UserRepo 的 checkIfUserExisted()getUserByEmail(),这两个函数都校验了一次 email。我们可以将校验代码放到 login() 即可。

另外还有一个隐藏的执行重复问题,login() 并不需要调用 checkIfUserExisted(),只需要调用一次 getUserByEmail(),从数据库中获取用户的 email、password 等信息跟用户输入的 email、password 信息做对比,依次判断是否登陆成功。

实际上,这样的优化是很有必要的,因为两个函数都查询了数据库,数据库I/O操作是比较耗时的,在写代码的时候应当减少这类I/O操作。

按照刚刚的思路把代码重构一下:

public class UserService {
	private UserRepo userRepo;

	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...
	}
}

代码复用性(Code Reusability)

什么是代码的复用性?

我们首先来区分三个概念:代码复用性(Code Reusability)、代码复用(Code Reuse)和DRY原则。

  • 代码复用表示一种行为:我们在开发新功能的时候,尽量复用已经存在的代码

  • 代码的可复用性表示一段代码可被复用的特性或能力:我们在编写代码的时候,让代码尽量可复用

  • DRY 原则是一条原则:不要写重复的代码

它们有点类似,但深究起来三者的区别还是蛮大的。

首先,“不重复” 并不代表 “可复用”。在一个项目代码中,可能不存在任何重复的代码,但也并不表示里面有可复用的代码,不重复和可复用完全是两个概念。所以,从这个角度来说,DRY 原则跟代码的可复用性讲的是两回事

其次,“复用” 和 “可复用性” 关注角度不同。代码 ”可复用性“ 是从代码开发者的角度来讲的,”复用“ 是从代码使用者的角度来讲的。比如,A同事编写了一个 UrlUtils 类,代码的 ”可复用性“ 很好;B同事在开发新功能的时候,直接 ”复用“ A同事编写的 UrlUtils 类。

尽管复用、可复用性、DRY 原则这三者从理解上有所区别,但实际上要达到的目的都是类似的,都是为了减少代码量,提高代码的可读性、可维护性

怎么提高代码复用性?

1、减少代码耦合

高度耦合的代码往往会牵一发而动全身,修改或新增一个功能就要牵连到很多其他相关的代码。高度耦合的代码会影响到代码的复用性,我们要尽量减少代码耦合。

2、满足单一职责原则

模块、类设计得而全,那依赖它得代码或者它依赖得代码就会比较多,进而增加了代码的耦合,也影响代码的复用性。相反,越细粒度的代码,代码的通用性会越好,越容易被复用。

3、模块化

这里的 ”模块“,不单单指一组类构成的模块,还可以理解为单个类、函数。我们要善于讲功能独立的代码,封装成模块,独立的模块更容易复用。

4、业务与非业务逻辑分离

越是跟业务无关的代码越是容易复用,越是针对特定业务的代码越难复用。所以,为了复用跟业务无关的代码,我们将业务和非业务逻辑代码分离,抽取成一些通用的框架、类库、组件等。

5、通用代码下沉

越底层的代码越通用、会被越多的模块调用,越应该设计得足够可复用。只允许上层代码调用下层代码及同层代码之间得调用,杜绝下层代码调用上层代码。

6、继承、多态、抽象、封装

越抽象、越不依赖具体的hi先,越容易复用。代码封装成模块,隐藏可变的细节、暴露不变的接口,就越容易复用。

7、应用模板等设计模式

一些设计模式也能提高代码的复用性。比如,模板模式利用了多态来实现,可以灵活替换其中的部分代码,整个流程模板代码可复用。

辩证思考和灵活应用

实际上,除非有非常明确的复用需求,否则,为了暂时用不到的复用需求,花费太多的时间、精力,投入太多的开发成本,并不是一个值得推荐的做法。这也违反我们之前讲到的 YAGNI 原则

除此之外,有一个著名的原则,叫做 ”Rule Of Three“,`Three" 并不是真的就指确切的 ”三“,只是一个原则名称。这条原则用在编程领域,那就是说,我们在第一次写代码的时候,如果当下没有复用的需求,而未来的复用需求也不是特别明确,并且开发可复用代码的成本比较高,那我们就不需要考虑代码的复用性。在之后我们开发新功能的时候,发现可以复用之前写的这段代码,那我们就重构这段代码,让其变得更加可复用

总结

1、DRY 原则

DRY 原则典型的三种重复情况:实现逻辑重复、功能语义重复、代码执行重复。实现逻辑重复,但功能语义不重复(即功能不同)的代码,并不违反 DRY 原则。实现逻辑不重复,但功能语义重复的代码,也算是违反 DRY 原则。代码执行重复也算是违反 DRY 原则。

2、代码复用性

提高代码复用性有以下7点:

  • 减少代码耦合

  • 满足单一职责原则

  • 模块化

  • 业务与非业务逻辑分离

  • 通用代码下沉

  • 继承、多态、抽象、封装

  • 应用模板等设计模式

我们可以不写可复用的代码,但一定不能写重复的代码。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值