【Java设计模式 经典设计原则】六 KISS、YAGNI和DRY原则

简介: 【Java设计模式 经典设计原则】六 KISS、YAGNI和DRY原则

在学习完SOLID原则后,再了解一些编程中常用到的指导原则,分别是KISS、YAGNI和DRY原则。这些更像是一些心法,了解了后写代码的时候随时给自己提个醒。

理解KISS-保持简单原则

KISS原则:Keep It Simple and Stupid。翻译成中文就是:尽量保持简单。代码的可读性和可维护性是衡量代码质量非常重要的两个标准。而 KISS 原则就是保持代码可读和可维护的重要手段。代码足够简单,也就意味着很容易读懂,bug 比较难隐藏。即便出现 bug,修复起来也比较简单。

什么是代码简单

并不是代码行数越少就越“简单”,还要考虑逻辑复杂度、实现难度、代码的可读性等。

// 第一种实现方式: 使用正则表达式
public boolean isValidIpAddressV1(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 isValidIpAddressV2(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;
}
// 第一种实现方式: 使用正则表达式
public boolean isValidIpAddressV1(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 isValidIpAddressV2(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;
}
// 第三种实现方式: 不使用任何工具类
public boolean isValidIpAddressV3(String ipAddress) {
  char[] ipChars = ipAddress.toCharArray();
  int length = ipChars.length;
  int ipUnitIntValue = -1;
  boolean isFirstUnit = true;
  int unitsCount = 0;
  for (int i = 0; i < length; ++i) {
    char c = ipChars[i];
    if (c == '.') {
      if (ipUnitIntValue < 0 || ipUnitIntValue > 255) return false;
      if (isFirstUnit && ipUnitIntValue == 0) return false;
      if (isFirstUnit) isFirstUnit = false;
      ipUnitIntValue = -1;
      unitsCount++;
      continue;
    }
    if (c < '0' || c > '9') {
      return false;
    }
    if (ipUnitIntValue == -1) ipUnitIntValue = 0;
    ipUnitIntValue = ipUnitIntValue * 10 + (c - '0');
  }
  if (ipUnitIntValue < 0 || ipUnitIntValue > 255) return false;
  if (unitsCount != 3) return false;
  return true;
}

上面这段检查输入的字符串 ipAddress 是否是合法的 IP 地址的代码,第一种实现方式利用的是正则表达式,第二种实现方式使用了 StringUtils 类、Integer 类提供的一些现成的工具函数,来处理 IP 地址字符串。第三种实现方式,不使用任何工具函数,而是通过逐一处理 IP 地址中的字符,来判断是否合法。

  • 第一种方式不符合KISS原则,虽然代码行数最少,看似最简单,实际上却很复杂,因为它使用了正则表达式,复杂的正则表达式会导致代码的可读性和可维护性变差
  • 第三种方式不符合KISS原则,第三种虽然和第二种代码行数了类似,但实现起来要比第二种更加有难度,更容易写出 bug,也就是逻辑复杂度较高

只有第二种方式使用了现成的工具类,代码逻辑不复杂,易读也易维护,满足KISS原子。但即使这样也需要看适配场景,如果本身问题比较复杂,那如果一段代码的逻辑复杂、实现难度大、可读性也不太好,但能解决这样复杂的问题,也满足KISS原则。例如用KMP算法解决字符串匹配问题,本身就复杂的问题,用复杂的方法解决,并不违背 KISS 原则

如何写出满足KISS的代码

KISS原则其实本来就比较主观,不过还是有一些验证方式的:

  1. 不要使用同事可能不懂的技术来实现代码。比如前面例子中的正则表达式,还有一些编程语言中过于高级的语法等。如果想用,培训一下大家
  2. 不要重复造轮子,要善于使用已经有的工具类库。经验证明,自己去实现这些类库,出 bug 的概率会更高,维护的成本也比较高。
  3. 不要过度优化。不要过度使用一些奇技淫巧(比如,位运算代替算术运算、复杂的条件语句代替 if-else、使用一些过于底层的函数等)来优化代码,牺牲代码的可读性。

主观验证的方式就是代码CR,让大家看看是否满足。

理解YAGNI-不要做过度设计原则

YAGNI 原则的英文全称是:You Ain’t Gonna Need It。直译就是:你不会需要它。当用在软件开发中的时候,它的意思是:不要去设计当前用不到的功能;不要去编写当前用不到的代码。实际上,这条原则的核心思想就是:不要做过度设计

比如,系统暂时只用 Redis 存储配置信息,以后可能会用到 ZooKeeper。根据 YAGNI 原则,在未用到 ZooKeeper 之前,我们没必要提前编写这部分代码。当然,这并不是说我们就不需要考虑代码的扩展性。我们还是要预留好扩展点,等到需要的时候,再去实现 ZooKeeper 存储配置信息这部分代码

理解DRY-不要写重复代码原则

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

什么是重复的代码

下面说到的这三种重复:实现逻辑重复,功能语义重复,代码逻辑重复。实现逻辑重复,但功能语义不重复的代码,并不违反 DRY 原则,可以通过更细粒度的SRP改造来去除重复代码。实现逻辑不重复,但功能语义重复的代码,也算是违反 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 函数。重复的代码被敲了两遍,或者简单 copy-paste 了一下,看起来明显违反 DRY 原则。为了移除重复的代码,我们对上面的代码做下重构,将 isValidUserName() 函数和 isValidPassword() 函数,合并为一个更通用的函数 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) {
    //省略实现逻辑
    //跟原来的isValidUsername()或isValidPassword()的实现逻辑一样...
    return true;
  }
}

合并之后的 isValidUserNameOrPassword函数,负责两件事情:验证用户名和验证密码,违反了单一职责原则和接口隔离原则

isValidUserNameisValidPassword两个函数,虽然从代码实现逻辑上看起来是重复的,但是从语义上并不重复。所谓语义不重复指的是:从功能语义上来看,这两个函数干的是完全不重复的两件事情,一个是校验用户名,另一个是校验密码。尽管在目前的设计中,两个校验逻辑是完全一样的,但如果按照第二种写法,将两个函数的合并,那就会存在潜在的问题。在未来的某一天,如果我们修改了密码的校验逻辑,比如,允许密码包含大写字符,允许密码的长度为 8 到 64 个字符,那这个时候,isValidUserName() 和 isValidPassword() 的实现逻辑就会不相同。我们就要把合并后的函数,重新拆成合并前的那两个函数。

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

功能语义重复

功能语义重复很多情况下看似符合 DRY,实际上却违反了DRY。在同一个项目代码中有下面两个函数: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;
}

一个基于正则,一个基于满足KISS原则的工具类。在这个例子中,尽管两段代码的实现逻辑不重复,但语义重复,也就是功能重复,我们认为它违反了 DRY 原则。例如使用的同事有可能研究了半天,觉得功能是一样的,但又有点疑惑,觉得是不是有更高深的考量,才定义了两个功能类似的函数,更危险的是如果有一天,判断IP是否有效的逻辑变了,只改了一个方法,另一个方法却察觉不到,这就导致系统功能bug。这一点我深有体会,现在的项目中自己的依据流程去获取门店的相似功能方法就有好几个,违反了DRY。

我们应该在项目中,统一一种实现思路,所有用到判断 IP 地址是否合法的地方,都统一调用同一个函数

代码逻辑重复

这种情况下是一个方法中既不存在实现逻辑重复,也不存在功能语义重复,而是代码逻辑被重复执行,例如:UserService 中 login() 函数用来校验用户登录是否成功。如果失败,就返回异常;如果成功,就返回用户信息。

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

这段代码有如下问题:

  • EmailValidation.validate(email)被重复执行了两次,一次是校验的时候,一次是获取用户的时候
  • getUserByEmailcheckIfUserExisted都对数据库进行了获取数据操作,进行了两次IO,实际上一次即可

代码重构后如下:

public class UserService {
  private UserRepo userRepo;//通过依赖注入或者IOC框架注入
  public User login(String email, String password) {
    check(String email, String password);
    User user = userRepo.getUserByEmail(email);
    if (user == null || !password.equals(user.getPassword()) {
      // ... throw AuthenticationFailureException...
    }
    return user;
  }
  public void check(String email, String password){
    if (!EmailValidation.validate(email)) {
      // ... throw InvalidEmailException...
    }
    if (!PasswordValidation.validate(password)) {
      // ... throw InvalidPasswordException...
    }
 }
}
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...
  }
}

把校验逻辑抽取出来,去掉重复的校验逻辑,并且不再调用checkIfUserExisted,而是增加了check方法专门用于校验参数有效性,单凭getUserByEmail即可判断对象是否存在,这其实也符合SRP原则,方法功能尽可能单一。

DRY和代码的可复用性

简单辨析下DRY原则、代码复用行为、代码复用性能力的区别,以及了解一下常用的提供代码复用性的方法。

辨析DRY原则、代码复用行为、代码复用性能力

代码复用表示一种行为:我们在开发新功能的时候,尽量复用已经存在的代码。代码的可复用性表示一段代码可被复用的特性或能力:我们在编写代码的时候,让代码尽量可复用

DRY原则是一条原则要保证的是不要写重复的代码,但并不意味着DRY就提高了代码的可复用性,举个例子,我们实现了一个方法【通过一系列参数判断比亚迪汽车质量是否合格】,我们不会再去写一个方法【通过一系列参数判断比亚迪汽车质量是否合格】,因为这叫重复造轮子,但这只意味着系统中不会有重复功能的代码,但并不意味着原本的方法【通过一系列参数判断比亚迪汽车质量是否合格】是一段可复用性强的代码,我们只是砍掉一个不应该造的轮子,而不是说本身的这个轮子很强,很通用。所以在这个项目里只有这一个方法是实现该功能的,满足DRY原则,但【通过一系列参数判断比亚迪汽车质量是否合格】并不能复用到所有四轮轿车的质量合格性判断上,它也不是一段可复用性强的代码。

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

提高代码复用性的方法

常用的提高代码复用性的方法有如下几种:

  1. 减少代码耦合,对于高度耦合的代码,当希望复用其中的一个功能,想把这个功能的代码抽取出来成为一个独立的模块、类或者函数的时候,往往会发现牵一发而动全身。移动一点代码,就要牵连到很多其他相关的代码。所以,高度耦合的代码会影响到代码的复用性,要尽量减少代码耦合。
  2. 满足单一职责原则,如果职责不够单一,模块、类设计得大而全,那依赖它的代码或者它依赖的代码就会比较多,进而增加了代码的耦合。根据上一点,也就会影响到代码的复用性。相反,越细粒度的代码,代码的通用性会越好,越容易被复用。
  3. 模块化,模块不单单指一组类构成的模块,还可以理解为单个类、函数。要善于将功能独立的代码,封装成模块。独立的模块就像一块一块的积木,更加容易复用,可以直接拿来搭建更加复杂的系统。
  4. 业务与非业务逻辑分离,越是跟业务无关的代码越是容易复用,越是针对特定业务的代码越难复用。所以,为了复用跟业务无关的代码,需要将业务和非业务逻辑代码分离,抽取成一些通用的框架、类库、组件等。
  5. 通用代码下沉,从分层的角度来看,越底层的代码越通用、会被越多的模块调用,越应该设计得足够可复用。一般情况下,在代码分层之后,为了避免交叉调用导致调用关系混乱,通常只允许上层代码调用下层代码及同层代码之间的调用,杜绝下层代码调用上层代码。所以,通用的代码我们尽量下沉到更下层。
  6. 继承、多态、抽象、封装,利用继承,可以将公共的代码抽取到父类,子类复用父类的属性和方法。利用多态,可以动态地替换一段代码的部分逻辑,让这段代码可复用。除此之外,抽象和封装,从更加广义的层面、而非狭义的面向对象特性的层面来理解的话,越抽象、越不依赖具体的实现,越容易复用。代码封装成模块,隐藏可变的细节、暴露不变的接口,就越容易复用。
  7. 应用模板等设计模式,一些设计模式,也能提高代码的复用性。比如,模板模式利用了多态来实现,可以灵活地替换其中的部分代码,整个流程模板代码可复用。

除此之外还有一些跟编程语言相关的特性,也能提高代码的复用性,比如泛型编程等。实际上,除了上面讲到的这些方法之外,复用意识也非常重要。在写代码的时候要多去思考一下,这个部分代码是否可以抽取出来,作为一个独立的模块、类或者函数供多处使用。在设计每个模块、类、函数的时候,要像设计一个外部 API 那样,去思考它的复用性。

总结一下

KISS 原则讲的是“如何做”的问题(尽量保持简单),它的目的是提升代码的可读性、可维护性,而 YAGNI 原则说的是“要不要做”的问题(当前不需要的就不要做),它的目的是降低代码的冗余度,提升代码的可维护性。DRY原则说的是不要做重复的事,它的目的是减少代码量,提高代码的可读性、可维护性。除此之外,复用已经经过测试的老代码,bug 会比从零重新开发要少,也就提高了代码健壮性。所以要有代码复用意识、写可复用性强的代码、不要违反DRY的去添加重复功能。不要违反YAGNI去过度设计,不要违反KISS去单纯的炫技。

相关文章
|
2天前
|
设计模式 Java 程序员
【23种设计模式·全精解析 | 概述篇】设计模式概述、UML图、软件设计原则
本系列文章聚焦于面向对象软件设计中的设计模式,旨在帮助开发人员掌握23种经典设计模式及其应用。内容分为三大部分:第一部分介绍设计模式的概念、UML图和软件设计原则;第二部分详细讲解创建型、结构型和行为型模式,并配以代码示例;第三部分通过自定义Spring的IOC功能综合案例,展示如何将常用设计模式应用于实际项目中。通过学习这些内容,读者可以提升编程能力,提高代码的可维护性和复用性。
【23种设计模式·全精解析 | 概述篇】设计模式概述、UML图、软件设计原则
|
26天前
|
设计模式 消息中间件 搜索推荐
Java 设计模式——观察者模式:从优衣库不使用新疆棉事件看系统的动态响应
【11月更文挑战第17天】观察者模式是一种行为设计模式,定义了一对多的依赖关系,使多个观察者对象能直接监听并响应某一主题对象的状态变化。本文介绍了观察者模式的基本概念、商业系统中的应用实例,如优衣库事件中各相关方的动态响应,以及模式的优势和实际系统设计中的应用建议,包括事件驱动架构和消息队列的使用。
|
1月前
|
设计模式 Java 数据库连接
Java编程中的设计模式:单例模式的深度剖析
【10月更文挑战第41天】本文深入探讨了Java中广泛使用的单例设计模式,旨在通过简明扼要的语言和实际示例,帮助读者理解其核心原理和应用。文章将介绍单例模式的重要性、实现方式以及在实际应用中如何优雅地处理多线程问题。
38 4
|
2月前
|
设计模式 Java 程序员
[Java]23种设计模式
本文介绍了设计模式的概念及其七大原则,强调了设计模式在提高代码重用性、可读性、可扩展性和可靠性方面的作用。文章还简要概述了23种设计模式,并提供了进一步学习的资源链接。
54 0
[Java]23种设计模式
|
1月前
|
设计模式 JavaScript Java
Java设计模式:建造者模式详解
建造者模式是一种创建型设计模式,通过将复杂对象的构建过程与表示分离,使得相同的构建过程可以创建不同的表示。本文详细介绍了建造者模式的原理、背景、应用场景及实际Demo,帮助读者更好地理解和应用这一模式。
|
2月前
|
设计模式 监控 算法
Java设计模式梳理:行为型模式(策略,观察者等)
本文详细介绍了Java设计模式中的行为型模式,包括策略模式、观察者模式、责任链模式、模板方法模式和状态模式。通过具体示例代码,深入浅出地讲解了每种模式的应用场景与实现方式。例如,策略模式通过定义一系列算法让客户端在运行时选择所需算法;观察者模式则让多个观察者对象同时监听某一个主题对象,实现松耦合的消息传递机制。此外,还探讨了这些模式与实际开发中的联系,帮助读者更好地理解和应用设计模式,提升代码质量。
Java设计模式梳理:行为型模式(策略,观察者等)
|
2月前
|
设计模式 Java
Java设计模式
Java设计模式
37 0
|
2月前
|
缓存 Java 数据库
JAVA分布式CAP原则
JAVA分布式CAP原则
71 0
|
2月前
|
设计模式 Java
Java设计模式之外观模式
这篇文章详细解释了Java设计模式之外观模式的原理及其应用场景,并通过具体代码示例展示了如何通过外观模式简化子系统的使用。
35 0
|
2月前
|
设计模式 Java
Java设计模式之桥接模式
这篇文章介绍了Java设计模式中的桥接模式,包括桥接模式的目的、实现方式,并通过具体代码示例展示了如何分离抽象与实现,使得两者可以独立变化。
50 0

热门文章

最新文章