2022-01-03 设计模式之DRY与迪米特法则总结

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.
  }
}
  1. 代码复用性今天,我们讲到提高代码可复用性的一些方法,有以下 7 点。
    • 减少代码耦合
    • 满足单一职责原则
    • 模块化
    • 业务与非业务逻辑分离
    • 通用代码下沉
    • 继承、多态、抽象、封装
    • 应用模板等设计模式
  2. 实际上,除了上面讲到的这些方法之外,复用意识也非常重要。在设计每个模块、类、函数的时候,要像设计一个外部 API 一样去思考它的复用性。我们在第一次写代码的时候,如果当下没有复用的需求,而未来的复用需求也不是特别明确,并且开发可复用代码的成本比较高,那我们就不需要考虑代码的复用性。在之后开发新的功能的时候,发现可以复用之前写的这段代码,那我们就重构这段代码,让其变得更加可复用。相比于代码的可复用性,DRY 原则适用性更强一些。我们可以不写可复用的代码,但一定不能写重复的代码。

迪米特法则

  1. 迪米特法则的英文翻译是: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("");
        }
    }
    
  2. 如何理解“高内聚、松耦合”?

    “高内聚、松耦合”是一个非常重要的设计思想,能够有效提高代码的可读性和可维护性,缩小功能改动导致的代码改动范围。“高内聚”用来指导类本身的设计,“松耦合”用来指导类与类之间依赖关系的设计。所谓高内聚,就是指相近的功能应该放到同一个类中,不相近的功能不要放到同一类中。相近的功能往往会被同时修改,放到同一个类中,修改会比较集中。所谓松耦合指的是,在代码中,类与类之间的依赖关系简单清晰。即使两个类有依赖关系,一个类的代码改动也不会或者很少导致依赖类的代码改动。

  3. 如何理解“迪米特法则”?

    不该有直接依赖关系的类之间,不要有依赖;有依赖关系的类之间,尽量只依赖必要的接口。迪米特法则是希望减少类之间的耦合,让类越独立越好。每个类都应该少了解系统的其他部分。一旦发生变化,需要了解这一变化的类就会比较少。

你可能感兴趣的:(2022-01-03 设计模式之DRY与迪米特法则总结)