DRY 原则
1.DRY 原则我们今天讲了三种代码重复的情况:实现逻辑重复、功能语义重复、代码执行重复。
实现逻辑重复,但功能语义不重复的代码,并不违反 DRY 原则。
/**
* 问题:isValidUserName() 和 isValidPassword() 两个函数是否违反了DRY原则?
*
* 答案:尽管两个函数的从代码实现逻辑上看起来是重复的,但是从语义上并不重复。所谓“语义不重复”指的是:
* 从功能上来看,这两个函数干的是完全不重复的两件事情,一个是校验用户名,另一个是校验密码。尽管在目前
* 的设计中,两个校验逻辑是完全一样的,但如果将两个函数的合并,那就会存在潜在的问题。在未来的某一天,如
* 果我们修改了密码的校验逻辑,比如,允许密码包含大写字符,允许密码的长度为 8 到 64 个字符,那这个时候
* ,isValidUserName() 和 isValidPassword() 的实现逻辑就会不相同。我们就要把合并后的函数,重新拆
* 成合并前的那两个函数。
*
* 尽管代码的实现逻辑是相同的,但语义不同,我们判定它并不违反 DRY 原则。对于包含重复代码的问题,我们可以
* 通过抽象成更细粒度函数的方式来解决。比如将校验只包含 a~z、0~9、dot 的逻辑封装成
* boolean onlyContains(String str, String charlist); 函数。
*/
public class UserAuthenticator {
public void authenticate(String userName, String password) throws InvalidException {
if (!isValidUsername(userName)) {
throw new InvalidException("userName", "User name" + userName + "is not valid.");
}
if (!isValidPassword(password)) {
throw new InvalidException("password", "Password" + password + "is not valid.");
}
}
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;
}
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;
}
}
实现逻辑不重复,但功能语义重复的代码,也算是违反 DRY 原则。
/**
* 问题:在同一个项目代码中有下面两个函数:isValidIp() 和 checkIfIpValid()。尽管两个函数的命名不同,
* 实现逻辑不同,但功能是相同的,都是用来判定 IP 地址是否合法的,此时是否违反DRY原则?
*
* 答案:这个例子中,尽管两段代码的实现逻辑不重复,但语义重复,也就是功能重复,我们认为它违反了 DRY 原则。
* 我们应该在项目中,统一一种实现思路,所有用到判断IP地址是否合法的地方,都统一调用同一个函数。假设我们不
* 统一实现思路,那有些地方调用了isValidIp()函数,有些地方又调用了checkIfIpValid()函数,这就会导致
* 代码看起来很奇怪,相当于给代码“埋坑”,给不熟悉这部分代码的同事增加了阅读的难度。同事有可能研究了半天,
* 觉得功能是一样的,但又有点疑惑,觉得是不是有更高深的考量,才定义了两个功能类似的函数,最终发现居然是代码
* 设计的问题。
* 除此之外,如果哪天项目中IP地址是否合法的判定规则改变了,比如:255.255.255.255不再被判定为合法的了,
* 相应地,我们对isValidIp()的实现逻辑做了相应的修改,但却忘记了修改checkIfIpValid()函数。又或者,
* 我们压根就不知道还存在一个功能相同的checkIfIpValid()函数,这样就会导致有些代码仍然使用老的IP地址
* 判断逻辑,导致出现一些莫名其妙的 bug。
*/
public class IpValidater {
public boolean isValidIpAddress(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原则?
* 答案:违反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) throws AuthenticationFailureException, InvalidException {
boolean existed = userRepo.checkIfUserExisted(email, password);
if(!existed){
throw new AuthenticationFailureException();
}
User user = userRepo.getUserByEmail(email);
return user;
}
}
public class UserRepo {
public boolean checkIfUserExisted(String email, String password) throws InvalidException {
if(!EmailValidation.validate(email)){
throw new InvalidException("email","");
}
if(!PasswordValidation.validate(password)){
throw new InvalidException("password","");
}
//TODO query db to check if email&password exists.
return false;
}
public User getUserByEmail(String email) throws InvalidException {
if(!EmailValidation.validate(email)){
throw new InvalidException("email","");
}
//TODO query db to get user by email.
return null;
}
}
优化后代码
public class UserService {
private UserRepo userRepo;//通过依赖注入或者IOC框架注入
public User login(String email, String password) {
if (!EmailValidation.validate(email)) {
throw new InvalidException("email","");
}
if (!PasswordValidation.validate(password)) {
throw new InvalidException("password","");
}
User user = userRepo.getUserByEmail(email);
if (user == null || !password.equals(user.getPassword()) {
throw new AuthenticationFailureException();
}
return user;
}
}
public class UserRepo {
public boolean checkIfUserExisted(String email, String password) {
//TODO query db to check if email&password exists.
}
public User getUserByEmail(String email) {
//TODO query db to get user by email.
}
}
- 代码复用性今天,我们讲到提高代码可复用性的一些方法,有以下 7 点。
- 减少代码耦合
- 满足单一职责原则
- 模块化
- 业务与非业务逻辑分离
- 通用代码下沉
- 继承、多态、抽象、封装
- 应用模板等设计模式
- 实际上,除了上面讲到的这些方法之外,复用意识也非常重要。在设计每个模块、类、函数的时候,要像设计一个外部 API 一样去思考它的复用性。我们在第一次写代码的时候,如果当下没有复用的需求,而未来的复用需求也不是特别明确,并且开发可复用代码的成本比较高,那我们就不需要考虑代码的复用性。在之后开发新的功能的时候,发现可以复用之前写的这段代码,那我们就重构这段代码,让其变得更加可复用。相比于代码的可复用性,DRY 原则适用性更强一些。我们可以不写可复用的代码,但一定不能写重复的代码。
迪米特法则
-
迪米特法则的英文翻译是:Law of Demeter,缩写是 LOD。也叫作最小知识原则,英文翻译为:The Least Knowledge Principle。
关于这个设计原则的英文定义:Each unit should have only limited knowledge about other units: only units “closely” related to the current unit. Or: Each unit should only talk to its friends; Don’t talk to strangers.我们把它直译成中文,就是下面这个样子:每个模块(unit)只应该了解那些与它关系密切的模块(units: only units “closely” related to the current unit)的有限知识(knowledge)。或者说,每个模块只和自己的朋友“说话”(talk),不和陌生人“说话”(talk)。我们对刚刚的定义重新描述一下。注意,为了统一讲解,我把定义描述中的“模块”替换成了“类”。不该有直接依赖关系的类之间,不要有依赖;
/** * 需求描述:实现了简化版的搜索引擎爬取网页的功能。代码中包含三个主要的类。 * * NetworkTransporter 类负责底层网络通信,根据请求获取数据; * HtmlDownloader 类用来通过 URL 获取网页; * Document 表示网页文档,后续的网页内容抽取、分词、索引都是以此为处理对象。 */ public class NetworkTransporter { public Byte[] send(HtmlRequest htmlRequest){ return null; } } public class HtmlDownloader { private NetworkTransporter transporter;//通过构造函数或IOC注入 public Html downloadHtml(String url) { Byte[] rawHtml = transporter.send(new HtmlRequest(url)); return new Html(rawHtml); } } public class Document { private Html html; private String url; public Document(String url) { this.url = url; HtmlDownloader downloader = new HtmlDownloader(); this.html = downloader.downloadHtml(url); } } //代码重构 //NetworkTransporter 类。作为一个底层网络通信类,我们希望它的功能尽可能通用,而不只是服务于下载 HTML,所以,我们不应该直接依赖太具体的发送对象 HtmlRequest。从这一点上讲,NetworkTransporter 类的设计违背迪米特法则,依赖了不该有直接依赖关系的 HtmlRequest 类。 public class NetworkTransporter { public Byte[] send(String address, byte[] data){ return null; } } public class HtmlDownloader { private NetworkTransporter transporter;//通过构造函数或IOC注入 public Html downloadHtml(String url) { HtmlRequest htmlRequest = new HtmlRequest(url); Byte[] rawHtml = transporter.send(htmlRequest.getAddress(),htmlRequest.getContent().getBytes()); return new Html(rawHtml); } } //Document 类。这个类的问题比较多,主要有三点。 // 第一,构造函数中的downloader.downloadHtml()逻辑复杂,耗时长,不应该放到构造函数中,会影响代码的可测试性。 // 第二,HtmlDownloader 对象在构造函数中通过new来创建,违反了基于接口而非实现编程的设计思想,也会影响到代码的可测试性。 // 第三,从业务含义上来讲,Document 网页文档没必要依赖 HtmlDownloader 类,违背了迪米特法则。 public class Document { private Html html; private String url; public Document(String url, Html html) { this.html = html; this.url = url; } } public class DocumentFactory { private HtmlDownloader downloader; public DocumentFactory(HtmlDownloader downloader) { this.downloader = downloader; } public Document createDocument(String url){ Html html = downloader.downloadHtml(url); return new Document(url,html); } }
有依赖关系的类之间,尽量只依赖必要的接口(也就是定义中的“有限知识”)。
/** * 需求:假设在我们的项目中,有些类只用到了序列化操作,而另一些类只用到反序列化操作。 * 那基于迪米特法则后半部分“有依赖关系的类之间,尽量只依赖必要的接口”,只用到序列化操作的那部分类不应该依赖反序列化接口。 * 同理,只用到反序列化操作的那部分类不应该依赖序列化接口。 * 如果分别设计两个类来实现上述功能又会违背了高内聚的设计思想。 * * 解决思路:分别设计两个接口,然后让Serialization类实现两个接口即可,该代码实现思路,也体现了“基于接口而非实现编程” * 的设计原则,结合迪米特法则,我们可以总结出一条新的设计原则,那就是“基于最小接口而非最大实现编程”。 */ public interface Serializable { String serialize(Object object); } public interface Deserializable { Object deserialize(String text); } public class Serialization implements Serializable,Deserializable { @Override public Object deserialize(String text) { return null; } @Override public String serialize(Object object) { return null; } } public class Demo { public static void main(String[] args) { //序列化 Serializable serializable = new Serialization(); serializable.serialize(new Object()); //反序列化 Deserializable deserializable = new Serialization(); deserializable.deserialize(""); } }
-
如何理解“高内聚、松耦合”?
“高内聚、松耦合”是一个非常重要的设计思想,能够有效提高代码的可读性和可维护性,缩小功能改动导致的代码改动范围。“高内聚”用来指导类本身的设计,“松耦合”用来指导类与类之间依赖关系的设计。所谓高内聚,就是指相近的功能应该放到同一个类中,不相近的功能不要放到同一类中。相近的功能往往会被同时修改,放到同一个类中,修改会比较集中。所谓松耦合指的是,在代码中,类与类之间的依赖关系简单清晰。即使两个类有依赖关系,一个类的代码改动也不会或者很少导致依赖类的代码改动。
-
如何理解“迪米特法则”?
不该有直接依赖关系的类之间,不要有依赖;有依赖关系的类之间,尽量只依赖必要的接口。迪米特法则是希望减少类之间的耦合,让类越独立越好。每个类都应该少了解系统的其他部分。一旦发生变化,需要了解这一变化的类就会比较少。
网友评论