【設計模式】設計原則之K.Y.D.L 四原則

K.Y.D.L 四原則

K:KISS(Keep it Simple and Stupid)簡單原則
Y:YAGNI(You Ain't Gonna Need It)不編寫不需要代碼原則
D:DRY(Don't repeat yourself)不要重復代碼原則
L:LOD(Law of Demter)迪米特原則(最少知識原則)

1. KISS(Keep it Simple and Stupid)原則

1.1 定義

盡量保持簡單。

1.2 KISS 中簡單的含義

1. 代碼行數(shù)越少越簡單?

判斷 IP 地址是否合法的三種實現(xiàn)方式:

// 第一種實現(xiàn)方式: 使用正則表達式
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);
}

// 第二種實現(xiàn)方式: 使用現(xiàn)成的工具類
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;
}

// 第三種實現(xiàn)方式: 不使用任何工具類
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;
}

第一種使用正則表達式實現(xiàn)的方式,代碼行數(shù)確定較少,但由于正則表達式本身較復雜,難以理解,所以,整個代碼的實現(xiàn)并不簡單。這種實現(xiàn)方式導致代碼的可讀性和可維護性變差,并不符合 KISS 原則。

第二種和第三種實現(xiàn)思路是差不多的,唯一的區(qū)別是第二種實現(xiàn)方式使用了工具類,而第三種完全是原生實現(xiàn)。第二種實現(xiàn)方式相比第三種,邏輯更加清晰,更容易讓人理解,所以,相比較而言,第二種更“簡單”,更加符合 KISS 原則。

2. 代碼邏輯復雜就違背 KISS 原則

// KMP algorithm: a, b分別是主串和模式串;n, m分別是主串和模式串的長度。
public static int kmp(char[] a, int n, char[] b, int m) {
  int[] next = getNexts(b, m);
  int j = 0;
  for (int i = 0; i < n; ++i) {
    while (j > 0 && a[i] != b[j]) { // 一直找到a[i]和b[j]
      j = next[j - 1] + 1;
    }
    if (a[i] == b[j]) {
      ++j;
    }
    if (j == m) { // 找到匹配模式串的了
      return i - m + 1;
    }
  }
  return -1;
}

// b表示模式串,m表示模式串的長度
private static int[] getNexts(char[] b, int m) {
  int[] next = new int[m];
  next[0] = -1;
  int k = -1;
  for (int i = 1; i < m; ++i) {
    while (k != -1 && b[k + 1] != b[i]) {
      k = next[k];
    }
    if (b[k + 1] == b[i]) {
      ++k;
    }
    next[i] = k;
  }
  return next;
}

KMP 是一個高效的匹配單模式字符串的算法,其實現(xiàn)本來就比較復雜,但效率卻非常高。如果對于處理長文本字符串匹配這類復雜問題,使用 KMP 算法也就是本身就復雜的問題,用復雜的方法解決,并不違反 KISS 原則。

如果在平時的開發(fā)中,只是簡單的字符串匹配,這種情況下,再使用 KMP 算法,那就算是違背 KISS 原則了。

從此可以看出,是否違反某個設計原則,主要還是取決于當前的應用場景。

1.3 如何寫出滿足 KISS 原則的代碼

  1. 盡量不要使用同事不懂的技術來實現(xiàn)代碼,如:正則表達式...
  2. 不要重復造輪子,要善于使用已有的工具類庫
  3. 避免過度優(yōu)化來犧牲代碼的可讀性

2. YAGNI(You Ain't Gonna Need It)

2.1 定義

不要去設計當前用不到的功能;不要去編寫當前用來到的代碼。核心思想就是:不要過度設計。

2.2 例子

配置文件

目前系統(tǒng)暫時使用 Redis 來存儲配置信息,以后可能使用到 ZooKeeper。如果根據(jù) YAGNI 原則,在未用到 ZooKeeper 之前,沒有必要提前寫好這部分代碼。當然,我們還是要預留好擴展點,等到需要的時候,再去實現(xiàn) ZooKeeper 這部分的代碼。

依賴開發(fā)包

通常,項目中會依賴很多第三方的開發(fā)包,而有些開發(fā)者嫌每次添加依賴配置較麻煩,往往會添加一個大而全的依賴配置,而將一些項目中根本用不到的第三方類庫也添加到項目中去。這樣做是違反 YAGNI 設計原則的。

2.3 YAGNI 和 KISS 的區(qū)別

KISS 原則講的是“如何做”的問題(盡可能保持簡單)。

YAGNI 原則講的是“要不要做”的問題(當前不需要的就不要做)。

3. DRY(Don't repeat youself)原則

3.1 定義

不要寫重復的代碼。

3.2 DRY 原則中關于重復的定義

1. 實現(xiàn)邏輯重復

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

上面的 isValidPassword()isValidUsername() 兩個函數(shù)的實現(xiàn)是一樣的,那這種算重復代碼么?

實際上是不算的,雖然兩者的代碼實現(xiàn)是一樣的,但兩個函數(shù)干的其實是兩件事情,一個是效驗用戶名,一個是效驗密碼。盡管目前兩個函數(shù)的代碼是完全一樣的,這也只是說剛好一樣而已。以后,隨著需求的變更,兩個函數(shù)的實現(xiàn)邏輯就可能是不一樣的。盡管代碼的實現(xiàn)邏輯是一樣的,但語義不同,所以,其并不違反 DRY 原則。至于包含重復代碼的問題,可以通過更小粒度的函數(shù)來達到代碼復用的目的。

2. 功能語義重復

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

上面的代碼,雖然函數(shù)名和函數(shù)實現(xiàn)都是不一樣的,但功能是一樣的,都是用來判斷 IP 地址是否合法。這種情況,往往是由于開發(fā)同學分開開發(fā)導致的。由于要實現(xiàn)的功能是完全一樣的,即使具體的函數(shù)實現(xiàn)不同,也是違反 DRY 原則的,需要將刪除其中一個,讓整個項目統(tǒng)一使用一個實現(xiàn)。

功能語義重復可能導致的問題:

項目中使用了兩個同樣功能的不同函數(shù),如果哪天判斷的規(guī)則變了,只改了一個,而另一個沒有被改變,這種情況下,就可以會引入 BUG。

3. 代碼執(zhí)行重復

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

所謂代碼執(zhí)行重復,就是同一段代碼被執(zhí)行了多次。上面的代碼,在 login 函數(shù)中 email 的效驗被調(diào)用了兩次,所以,是代碼執(zhí)行重復,屬于違反了 DRY 原則。

3.3 什么是代碼的復用性

代碼的復用性

指的是一段代碼可被復用的特性或能力。代碼的可復用性,是從代碼開發(fā)者的角度來講的。

代碼復用

在開發(fā)過程中,盡量使用已經(jīng)存在的代碼。代碼復用,是從代碼使用者的角度來講的。

DRY 原則

不要寫重復的代碼。

如何提高代碼復用性

  1. 減少代碼耦合
  2. 滿足單一職責
  3. 模塊化
  4. 業(yè)務與非業(yè)務邏輯分離
  5. 通用代碼下沉
  6. 繼承、多態(tài)、抽象和封裝
  7. 應用模塊方法等設計模式,復用通用的算骨架
  8. 運用泛型技術編程,提高代碼的抽象程度

3.4 Rule of Three

也就是說,第一次編寫代碼的時候,我們不考慮其復用性;第二次遇到復用場景的時候,再進行重構(gòu)使其復用。這里的 Three,指的是二,而不是三。

4. 迪米特原則(最少知識原則) LOD(Law of Demeter)

4.1 定義

不該有直接依賴關系的類之間,不要依賴;有依賴關系的類之間,盡量只依賴必要的接口。

4.2 什么是高內(nèi)聚、松耦合

高內(nèi)聚用來指導類本身的設計,松耦合用來指導類與類之間依賴關系的設計。高內(nèi)聚有助于松耦合,松耦合又需要高內(nèi)聚的支持。

所謂高內(nèi)聚指的是:相近的功能應該放到同一個類中,不相近的功能不要放到同一個類中。相近的功能往往會被同時修改,放到同一個類中,修改比較集中,代碼也容易維護。實際上,單一職責原則是實現(xiàn)代碼高內(nèi)聚的非常有效的設計原則。

所謂松耦合指的是:類與類之間的依賴關系簡單清晰。即有依賴關系的兩個類,一個類的代碼改動不會或很少導致依賴類代碼的改動。依賴注入、接口隔離原則、依賴接口而非實現(xiàn)以及迪米特原則都是為了實現(xiàn)代碼的松耦合。

4.3 不應該有依賴關系的類之間,不要有依賴例子

public class NetworkTransporter {
    // 省略屬性和其他方法...
    public Byte[] send(HtmlRequest htmlRequest) {
      //...
    }
}

public class HtmlDownloader {
  private NetworkTransporter transporter;//通過構(gòu)造函數(shù)或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 類作用一個底層通信類,其功能應該盡可能通用。而目前的設計依賴了太具體的 HtmlRequest 類,從這一種來講,違反了迪米特原則,依賴了不該有直接依賴關系的類。

重構(gòu)后的 NetworkTransporter

public class NetworkTransporter {
    // 省略屬性和其他方法...
    public Byte[] send(String address, Byte[] data) {
      //...
    }
}

存在問題二:Document 類存在三個主要問題。

  1. 構(gòu)造函數(shù)中邏輯過于復雜,耗時長,不應該放在構(gòu)造函數(shù)中,影響代碼的可測試性
  2. 所依賴的 HtmlDownloader 對象直接使用 new 的方式來創(chuàng)建,違反了基于接口而非實現(xiàn)編程的設計思想,也會影響代碼的可測試性
  3. Document 網(wǎng)頁文檔沒必要依賴 HtmlDownloader 類,違反了迪米特原則

優(yōu)化后的 Document 類

public class Document {
  private Html html;
  private String url;
  
  public Document(String url, Html html) {
    this.html = html;
    this.url = url;
  }
  //...
}

// 通過一個工廠方法來創(chuàng)建Document
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);
  }
}

4.4 有依賴關系的類之間,盡量只依賴必要的接口

public class Serialization {
  public String serialize(Object object) {
    String serializedResult = ...;
    //...
    return serializedResult;
  }
  
  public Object deserialize(String str) {
    Object deserializedResult = ...;
    //...
    return deserializedResult;
  }
}

上面代碼沒有什么問題,但如果把其放到具體的應用場景中:假設在我們的項目中,有些類只用到了序列化方法,另一些類只用到了反序列化方法,那根據(jù)迪米特原則的后半部分“有依賴關系的兩個類,盡量依賴必要的接口”,只用到了序列化的類不應該依賴反序列化接口,反之亦然。

滿足迪米特原則的優(yōu)化

public class Serializer {
  public String serialize(Object object) {
    String serializedResult = ...;
    ...
    return serializedResult;
  }
}

public class Deserializer {
  public Object deserialize(String str) {
    Object deserializedResult = ...;
    ...
    return deserializedResult;
  }
}

但滿足迪米特原則的優(yōu)化版本,又違反了高內(nèi)聚的設計思想,即相近的功能要放到同一個類中,方便統(tǒng)一修改。那如何優(yōu)化讓其即滿足高內(nèi)聚設計思想,又滿足迪米特原則呢?

通過接口隔離原則,引入兩個接口,再根據(jù)多態(tài)特性,在使用序列化類的時候,依賴具體的單個u接口,而非具體類。

引入接口隔離原則后的優(yōu)化版本

public interface Serializable {
  String serialize(Object object);
}

public interface Deserializable {
  Object deserialize(String text);
}

public class Serialization implements Serializable, Deserializable {
  @Override
  public String serialize(Object object) {
    String serializedResult = ...;
    ...
    return serializedResult;
  }
  
  @Override
  public Object deserialize(String str) {
    Object deserializedResult = ...;
    ...
    return deserializedResult;
  }
}

public class DemoClass_1 {
  private Serializable serializer;
  
  public Demo(Serializable serializer) {
    this.serializer = serializer;
  }
  //...
}

public class DemoClass_2 {
  private Deserializable deserializer;
  
  public Demo(Deserializable deserializer) {
    this.deserializer = deserializer;
  }
  //...
}

說明

此文是根據(jù)王爭設計模式之美相關專欄內(nèi)容整理而來,非原創(chuàng)。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

友情鏈接更多精彩內(nèi)容