K.Y.D.L 四原則
K:KISS(Keep it Simple and Stupid)簡(jiǎn)單原則
Y:YAGNI(You Ain't Gonna Need It)不編寫(xiě)不需要代碼原則
D:DRY(Don't repeat yourself)不要重復(fù)代碼原則
L:LOD(Law of Demter)迪米特原則(最少知識(shí)原則)
1. KISS(Keep it Simple and Stupid)原則
1.1 定義
盡量保持簡(jiǎn)單屋匕。
1.2 KISS 中簡(jiǎn)單的含義
1. 代碼行數(shù)越少越簡(jiǎn)單?
判斷 IP 地址是否合法的三種實(shí)現(xiàn)方式:
// 第一種實(shí)現(xiàn)方式: 使用正則表達(dá)式
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);
}
// 第二種實(shí)現(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;
}
// 第三種實(shí)現(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;
}
第一種使用正則表達(dá)式實(shí)現(xiàn)的方式逻住,代碼行數(shù)確定較少休蟹,但由于正則表達(dá)式本身較復(fù)雜骗村,難以理解浪默,所以疾就,整個(gè)代碼的實(shí)現(xiàn)并不簡(jiǎn)單肺然。這種實(shí)現(xiàn)方式導(dǎo)致代碼的可讀性和可維護(hù)性變差菠发,并不符合 KISS 原則王滤。
第二種和第三種實(shí)現(xiàn)思路是差不多的,唯一的區(qū)別是第二種實(shí)現(xiàn)方式使用了工具類滓鸠,而第三種完全是原生實(shí)現(xiàn)雁乡。第二種實(shí)現(xiàn)方式相比第三種,邏輯更加清晰糜俗,更容易讓人理解踱稍,所以,相比較而言悠抹,第二種更“簡(jiǎn)單”珠月,更加符合 KISS 原則。
2. 代碼邏輯復(fù)雜就違背 KISS 原則
// KMP algorithm: a, b分別是主串和模式串楔敌;n, m分別是主串和模式串的長(zhǎng)度啤挎。
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表示模式串的長(zhǎng)度
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 是一個(gè)高效的匹配單模式字符串的算法卵凑,其實(shí)現(xiàn)本來(lái)就比較復(fù)雜庆聘,但效率卻非常高。如果對(duì)于處理長(zhǎng)文本字符串匹配這類復(fù)雜問(wèn)題氛谜,使用 KMP 算法也就是本身就復(fù)雜的問(wèn)題掏觉,用復(fù)雜的方法解決,并不違反 KISS 原則值漫。
如果在平時(shí)的開(kāi)發(fā)中,只是簡(jiǎn)單的字符串匹配织盼,這種情況下杨何,再使用 KMP 算法,那就算是違背 KISS 原則了沥邻。
從此可以看出危虱,是否違反某個(gè)設(shè)計(jì)原則,主要還是取決于當(dāng)前的應(yīng)用場(chǎng)景唐全。
1.3 如何寫(xiě)出滿足 KISS 原則的代碼
- 盡量不要使用同事不懂的技術(shù)來(lái)實(shí)現(xiàn)代碼埃跷,如:正則表達(dá)式...
- 不要重復(fù)造輪子蕊玷,要善于使用已有的工具類庫(kù)
- 避免過(guò)度優(yōu)化來(lái)犧牲代碼的可讀性
2. YAGNI(You Ain't Gonna Need It)
2.1 定義
不要去設(shè)計(jì)當(dāng)前用不到的功能;不要去編寫(xiě)當(dāng)前用來(lái)到的代碼弥雹。核心思想就是:不要過(guò)度設(shè)計(jì)垃帅。
2.2 例子
配置文件
目前系統(tǒng)暫時(shí)使用 Redis 來(lái)存儲(chǔ)配置信息,以后可能使用到 ZooKeeper剪勿。如果根據(jù) YAGNI 原則贸诚,在未用到 ZooKeeper 之前,沒(méi)有必要提前寫(xiě)好這部分代碼厕吉。當(dāng)然酱固,我們還是要預(yù)留好擴(kuò)展點(diǎn),等到需要的時(shí)候头朱,再去實(shí)現(xiàn) ZooKeeper 這部分的代碼运悲。
依賴開(kāi)發(fā)包
通常,項(xiàng)目中會(huì)依賴很多第三方的開(kāi)發(fā)包项钮,而有些開(kāi)發(fā)者嫌每次添加依賴配置較麻煩扇苞,往往會(huì)添加一個(gè)大而全的依賴配置,而將一些項(xiàng)目中根本用不到的第三方類庫(kù)也添加到項(xiàng)目中去寄纵。這樣做是違反 YAGNI 設(shè)計(jì)原則的鳖敷。
2.3 YAGNI 和 KISS 的區(qū)別
KISS 原則講的是“如何做”的問(wèn)題(盡可能保持簡(jiǎn)單)。
YAGNI 原則講的是“要不要做”的問(wèn)題(當(dāng)前不需要的就不要做)程拭。
3. DRY(Don't repeat youself)原則
3.1 定義
不要寫(xiě)重復(fù)的代碼定踱。
3.2 DRY 原則中關(guān)于重復(fù)的定義
1. 實(shí)現(xiàn)邏輯重復(fù)
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()
兩個(gè)函數(shù)的實(shí)現(xiàn)是一樣的,那這種算重復(fù)代碼么恃鞋?
實(shí)際上是不算的崖媚,雖然兩者的代碼實(shí)現(xiàn)是一樣的,但兩個(gè)函數(shù)干的其實(shí)是兩件事情恤浪,一個(gè)是效驗(yàn)用戶名畅哑,一個(gè)是效驗(yàn)密碼。盡管目前兩個(gè)函數(shù)的代碼是完全一樣的水由,這也只是說(shuō)剛好一樣而已荠呐。以后,隨著需求的變更砂客,兩個(gè)函數(shù)的實(shí)現(xiàn)邏輯就可能是不一樣的泥张。盡管代碼的實(shí)現(xiàn)邏輯是一樣的,但語(yǔ)義不同鞠值,所以媚创,其并不違反 DRY 原則。至于包含重復(fù)代碼的問(wèn)題彤恶,可以通過(guò)更小粒度的函數(shù)來(lái)達(dá)到代碼復(fù)用的目的钞钙。
2. 功能語(yǔ)義重復(fù)
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ù)實(shí)現(xiàn)都是不一樣的,但功能是一樣的芒炼,都是用來(lái)判斷 IP 地址是否合法瘫怜。這種情況,往往是由于開(kāi)發(fā)同學(xué)分開(kāi)開(kāi)發(fā)導(dǎo)致的焕议。由于要實(shí)現(xiàn)的功能是完全一樣的宝磨,即使具體的函數(shù)實(shí)現(xiàn)不同,也是違反 DRY 原則的盅安,需要將刪除其中一個(gè)唤锉,讓整個(gè)項(xiàng)目統(tǒng)一使用一個(gè)實(shí)現(xiàn)。
功能語(yǔ)義重復(fù)可能導(dǎo)致的問(wèn)題:
項(xiàng)目中使用了兩個(gè)同樣功能的不同函數(shù)别瞭,如果哪天判斷的規(guī)則變了窿祥,只改了一個(gè),而另一個(gè)沒(méi)有被改變蝙寨,這種情況下晒衩,就可以會(huì)引入 BUG。
3. 代碼執(zhí)行重復(fù)
public class UserService {
private UserRepo userRepo;//通過(guò)依賴注入或者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í)行重復(fù)墙歪,就是同一段代碼被執(zhí)行了多次听系。上面的代碼,在 login 函數(shù)中 email 的效驗(yàn)被調(diào)用了兩次虹菲,所以靠胜,是代碼執(zhí)行重復(fù),屬于違反了 DRY 原則毕源。
3.3 什么是代碼的復(fù)用性
代碼的復(fù)用性
指的是一段代碼可被復(fù)用的特性或能力浪漠。代碼的可復(fù)用性,是從代碼開(kāi)發(fā)者的角度來(lái)講的霎褐。
代碼復(fù)用
在開(kāi)發(fā)過(guò)程中址愿,盡量使用已經(jīng)存在的代碼。代碼復(fù)用冻璃,是從代碼使用者的角度來(lái)講的响谓。
DRY 原則
不要寫(xiě)重復(fù)的代碼。
如何提高代碼復(fù)用性
- 減少代碼耦合
- 滿足單一職責(zé)
- 模塊化
- 業(yè)務(wù)與非業(yè)務(wù)邏輯分離
- 通用代碼下沉
- 繼承俱饿、多態(tài)歌粥、抽象和封裝
- 應(yīng)用模塊方法等設(shè)計(jì)模式,復(fù)用通用的算骨架
- 運(yùn)用泛型技術(shù)編程拍埠,提高代碼的抽象程度
3.4 Rule of Three
也就是說(shuō),第一次編寫(xiě)代碼的時(shí)候土居,我們不考慮其復(fù)用性枣购;第二次遇到復(fù)用場(chǎng)景的時(shí)候嬉探,再進(jìn)行重構(gòu)使其復(fù)用。這里的 Three棉圈,指的是二涩堤,而不是三。
4. 迪米特原則(最少知識(shí)原則) LOD(Law of Demeter)
4.1 定義
不該有直接依賴關(guān)系的類之間分瘾,不要依賴胎围;有依賴關(guān)系的類之間,盡量只依賴必要的接口德召。
4.2 什么是高內(nèi)聚白魂、松耦合
高內(nèi)聚用來(lái)指導(dǎo)類本身的設(shè)計(jì),松耦合用來(lái)指導(dǎo)類與類之間依賴關(guān)系的設(shè)計(jì)上岗。高內(nèi)聚有助于松耦合福荸,松耦合又需要高內(nèi)聚的支持。
所謂高內(nèi)聚指的是:相近的功能應(yīng)該放到同一個(gè)類中肴掷,不相近的功能不要放到同一個(gè)類中敬锐。相近的功能往往會(huì)被同時(shí)修改,放到同一個(gè)類中呆瞻,修改比較集中台夺,代碼也容易維護(hù)。實(shí)際上痴脾,單一職責(zé)原則是實(shí)現(xiàn)代碼高內(nèi)聚的非常有效的設(shè)計(jì)原則颤介。
所謂松耦合指的是:類與類之間的依賴關(guān)系簡(jiǎn)單清晰。即有依賴關(guān)系的兩個(gè)類明郭,一個(gè)類的代碼改動(dòng)不會(huì)或很少導(dǎo)致依賴類代碼的改動(dòng)买窟。依賴注入、接口隔離原則薯定、依賴接口而非實(shí)現(xiàn)以及迪米特原則都是為了實(shí)現(xiàn)代碼的松耦合始绍。
4.3 不應(yīng)該有依賴關(guān)系的類之間,不要有依賴?yán)?/h3>
public class NetworkTransporter {
// 省略屬性和其他方法...
public Byte[] send(HtmlRequest htmlRequest) {
//...
}
}
public class HtmlDownloader {
private NetworkTransporter transporter;//通過(guò)構(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);
}
//...
}
public class NetworkTransporter {
// 省略屬性和其他方法...
public Byte[] send(HtmlRequest htmlRequest) {
//...
}
}
public class HtmlDownloader {
private NetworkTransporter transporter;//通過(guò)構(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);
}
//...
}
存在問(wèn)題一:NetworkTransporter 類作用一個(gè)底層通信類话侄,其功能應(yīng)該盡可能通用亏推。而目前的設(shè)計(jì)依賴了太具體的 HtmlRequest 類,從這一種來(lái)講年堆,違反了迪米特原則吞杭,依賴了不該有直接依賴關(guān)系的類。
重構(gòu)后的 NetworkTransporter
public class NetworkTransporter {
// 省略屬性和其他方法...
public Byte[] send(String address, Byte[] data) {
//...
}
}
存在問(wèn)題二:Document 類存在三個(gè)主要問(wèn)題变丧。
- 構(gòu)造函數(shù)中邏輯過(guò)于復(fù)雜芽狗,耗時(shí)長(zhǎng),不應(yīng)該放在構(gòu)造函數(shù)中痒蓬,影響代碼的可測(cè)試性
- 所依賴的 HtmlDownloader 對(duì)象直接使用 new 的方式來(lái)創(chuàng)建童擎,違反了基于接口而非實(shí)現(xiàn)編程的設(shè)計(jì)思想滴劲,也會(huì)影響代碼的可測(cè)試性
- Document 網(wǎng)頁(yè)文檔沒(méi)必要依賴 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;
}
//...
}
// 通過(guò)一個(gè)工廠方法來(lái)創(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 有依賴關(guān)系的類之間顾复,盡量只依賴必要的接口
public class Serialization {
public String serialize(Object object) {
String serializedResult = ...;
//...
return serializedResult;
}
public Object deserialize(String str) {
Object deserializedResult = ...;
//...
return deserializedResult;
}
}
上面代碼沒(méi)有什么問(wèn)題班挖,但如果把其放到具體的應(yīng)用場(chǎng)景中:假設(shè)在我們的項(xiàng)目中,有些類只用到了序列化方法芯砸,另一些類只用到了反序列化方法萧芙,那根據(jù)迪米特原則的后半部分“有依賴關(guān)系的兩個(gè)類,盡量依賴必要的接口”假丧,只用到了序列化的類不應(yīng)該依賴反序列化接口双揪,反之亦然。
滿足迪米特原則的優(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)聚的設(shè)計(jì)思想盟榴,即相近的功能要放到同一個(gè)類中,方便統(tǒng)一修改婴噩。那如何優(yōu)化讓其即滿足高內(nèi)聚設(shè)計(jì)思想擎场,又滿足迪米特原則呢?
通過(guò)接口隔離原則几莽,引入兩個(gè)接口迅办,再根據(jù)多態(tài)特性,在使用序列化類的時(shí)候章蚣,依賴具體的單個(gè)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;
}
//...
}
說(shuō)明
此文是根據(jù)王爭(zhēng)設(shè)計(jì)模式之美相關(guān)專欄內(nèi)容整理而來(lái),非原創(chuàng)。