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 原則的代碼
- 盡量不要使用同事不懂的技術來實現(xiàn)代碼,如:正則表達式...
- 不要重復造輪子,要善于使用已有的工具類庫
- 避免過度優(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 原則
不要寫重復的代碼。
如何提高代碼復用性
- 減少代碼耦合
- 滿足單一職責
- 模塊化
- 業(yè)務與非業(yè)務邏輯分離
- 通用代碼下沉
- 繼承、多態(tài)、抽象和封裝
- 應用模塊方法等設計模式,復用通用的算骨架
- 運用泛型技術編程,提高代碼的抽象程度
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 類存在三個主要問題。
- 構(gòu)造函數(shù)中邏輯過于復雜,耗時長,不應該放在構(gòu)造函數(shù)中,影響代碼的可測試性
- 所依賴的 HtmlDownloader 對象直接使用 new 的方式來創(chuàng)建,違反了基于接口而非實現(xiàn)編程的設計思想,也會影響代碼的可測試性
- 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)。