10k

设计模式之美-课程笔记13-设计原则7-DIY

理论七:重复的代码就一定违背DRY吗?如何提高代码的复用性?

DRY原则(Don't Repeat Yourself)

  1. 不要重复自己,即不要写重复的代码。
  2. 针对三种情况讨论:实现逻辑重复,功能语义重复和代码执行重复。

实现逻辑重复

先看一个例子是否违背了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;
  }
}
  1. 我个人看来整个用户名和密码的逻辑是一样的,可以合并成一个validateAuthStr的函数,但是一个担心是密码和用户名从业务逻辑上来说是不同的类型,可能后续会增加更多的不同的校验逻辑。

  2. 作者也是同样的思路,这个重构后的函数违反了单一职责原则和接口隔离原则。concern也是一样的例子。

  3. 所以尽管实现逻辑是一样的,但是语义不一样,我们判定它并不违反DRY原则。对于重复代码的问题,我们可通过抽象更细粒度函数的方式来解决。比如将校验只包含 a~z、0~9、dot 的逻辑封装成 boolean onlyContains(String str, String charlist); 函数。

功能语义重复

  1. 现在我们再来看另外一个例子。在同一个项目代码中有下面两个函数:isValidIp() 和 checkIfIpValid()。尽管两个函数的命名不同,实现逻辑不同,但功能是相同的,都是用来判定 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;
    }
  1. 不仅代码的易读性变差了(混合两个不同的实现,但是其实在做同一件事);而且可维护性也变差,如果以后ip地址的校验逻辑变化了,要同时对这些个方法去做修改。

  2. 应该统一实现,对于一件事只调用相同的函数逻辑。

代码执行重复

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...
  }
}
  1. 这个例子也比较明显,在登录的函数中,做了一次检查用户存在与否的操作checkIfUserExisted这个函数中又是validate了email和password;在调用完checkIfUserExisted之后,又去获取用户,getUserByEmail中又去对email做了一次validation。
  2. 为去除重复可以将getUserByEmail中的校验逻辑删掉。而实际上从代码设计来说,login函数不需要validate email和password只需要将从db查得到的信息和用户传进来的作对比即可。
  3. 这样的优化很有必要,因为重复的地方都是对db的读操作,这样的I/O操作又是相对耗时的环节。

可以重构成为如下:删掉checkIfUserExisted函数的调用在login中,直接校验用户名和密码即可。

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

代码复用性(Code Reusability)

什么是代码的复用性

  1. 区分三个概念:代码复用性,代码复用,DRY原则
    • 代码复用表示一种行为,我们在开发新功能的时候,尽量复用已经存在的代码。
    • 代码可复用性表示一段代码被复用的特性或能力:在编写代码的时候,让代码尽量可复用。
    • DRY是一条原则:不要写重复代码。
  2. 代码不重复和可复用是两回事。
  3. 复用和可复用性关注角度不同。复用是代码使用者的角度,可复用性是代码的开发者角度来讲。

如何提升代码的复用性

  • 减少代码耦合:如果我们想复用一段代码,往往是将其抽离出来,高度耦合的代码很难去做修改。
  • 满足单一职责原则:还是有点类似上一点,不满足单一职责原则,同时做几件事,比较难会被抽离出来复用在其他地方。
  • 模块化:类、函数、一组类的封装。
  • 业务与非业务逻辑分离。针对业务的代码逻辑多数时候难以复用。
  • 通用代码下沉:越底层的代码越通用,被更多的模块调用。代码分层之后,为了避免交叉调用导致关系混乱,一般只允许上层代码调用下层代码以及同层调用。
  • 继承多态抽象封装:类似上面一点,比如子类和父类的分层。抽象和封装是更广义的分层。
  • 应用模板等设计模式。

辩证思考灵活运用

对于当下没有复用需求,为未来考虑的场景,编写可复用的代码就比较难。避免过度考虑和设计,当下或者近期没有特别需求要复用不需要花很多精力在上面。

除此之外,有一个著名的原则,叫作“Rule of Three”。这条原则可以用在很多行业和场景中,你可以自己去研究一下。如果把这个原则用在这里,那就是说,我们在第一次写代码的时候,如果当下没有复用的需求,而未来的复用需求也不是特别明确,并且开发可复用代码的成本比较高,那我们就不需要考虑代码的复用性。在之后我们开发新的功能的时候,发现可以复用之前写的这段代码,那我们就重构这段代码,让其变得更加可复用。也就是说,第一次编写代码的时候,我们不考虑复用性;第二次遇到复用场景的时候,再进行重构使其复用。需要注意的是,“Rule of Three”中的“Three”并不是真的就指确切的“三”,这里就是指“二”。

Thoughts? Leave a comment