設計模式學習筆記 - 設計原則 - 7.DRY 原則及提高代碼復用性

前言

DRY 原則,英文描述為: Don’t Repeat Yourself。中文直譯:不要重復自己。將它應用在編程中,可理解為:不要寫重讀的代碼。

可能你認為,這個原則很簡單。只要兩段代碼長得一樣,那就是違反 DRY 原則了。真的是這樣嗎? 答案是否定的。這是很多人對這條原則存在的誤解。實際上,重讀的代碼不一定違反 DRY 原則,而有些看似不重復的代碼也可能違反 DRY 原則。


DRY 原則(Don’t Repeat Yourself)

DRY 原則的定義非常簡單,我就不再過度解讀了。今天,主要將三種典型的代碼重復情況,它們分別是:實現邏輯重復、功能語義重復和代碼執行重復。這三種代碼重復,有些看似違反 DRY 原則,實際上并不違反;有的看似不違反,實際上卻違反了。

實現邏輯重復

先看一段代碼

public class UserAuthenticator {public void authenticate(String username, String password) {if (!isValidUsername(username)) {// throw new InvalidUsernameException...}if (!isValidPassword(password)) {// throw new InvalidPasswordException...}// 省略其他代碼...}private boolean isValidUsername(String username) {if (StringUtils.isEmpty(username)) { return false; }// check length: 4-64int length = username.length();if (length < 4 || length > 64) { return false; }// contains only lower case lettersif (StringUtils.isAllLowerCase(username)) { return false; }// contains only z~z,0~9,dotfor (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) {if (StringUtils.isEmpty(password)) { return false; }// check length: 4-64int length = password.length();if (length < 4 || length > 64) { return false; }// contains only lower case lettersif (StringUtils.isAllLowerCase(password)) { return false; }// contains only z~z,0~9,dotfor (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()。重復的代碼被敲了兩遍,看起來明顯違反了 DRY 原則。為了移除重復的代碼,我們進行下重構,將 isValidUsername()isValidPassword() 合并Wie一個更通用的函數, isValidUsernameOrPassword()

public class UserAuthenticator {public void authenticate(String username, String password) {if (!isValidUsernameOrPassword(username)) {// throw new InvalidUsernameException...}if (!isValidUsernameOrPassword(password)) {// throw new InvalidPasswordException...}// 省略其他代碼...}private boolean isValidUsernameOrPassword(String usernameOrPassword) {if (StringUtils.isEmpty(usernameOrPassword)) { return false; }// check length: 4-64int length = usernameOrPassword.length();if (length < 4 || length > 64) { return false; }// contains only lower case lettersif (StringUtils.isAllLowerCase(usernameOrPassword)) { return false; }// contains only z~z,0~9,dotfor (int i = 0; i < length; ++i) {char c = usernameOrPassword.charAt(i);if (!((c >= 'a' && c <= 'z') || !(c >= '0' && c <= '9') || c != '.')) { return false; }}return true;}
}

重構之后,代碼行數減少了,也沒有重復代碼了,是不是更好呢?

單從名字上看,合并之后的 isValidUsernameOrPassword() 函數,負責兩件事情:驗證用戶名和密碼,違反了單一職責原則和接口隔離原則。實際上,即便將兩個函數合并成 isValidUsernameOrPassword() ,代碼仍然存在問題。

因為 isValidUsername()isValidPassword(),雖然代碼實現邏輯上看起來是重復的,但是從語義上并不重復。盡管在目前的設計中,兩個校驗邏輯完全一樣,但是如果按照第二種寫法,將兩個函數合并,那就回農村在潛在的問題。在未來的某一天,如果我們修改了密鑰校驗邏輯,比如,允許密碼包含大寫字符,允許密碼長度為 8 到 64 個字符,那這個時候, isValidUsername()isValidPassword() 的實現邏輯就會不相同。我們需要把合并后的函數,重新拆分成合并前的兩個函數。

所謂 “語義不重復” 是指:從功能上看,這兩個函數干的是完全不重復的事情,一個是校驗用戶名,一個是校驗密碼。

盡管代碼的實現邏輯相同,但語義不同,我們判定它并不違反 DRY 原則。對于包含重復代碼的問題,我們可以通過抽象成更細粒度函數的方式來解決。比如將校驗只包含 a-z、0-9、dot 的邏輯都封裝成函數。

功能語義重復

在看另一個例子。在同一個項目代碼中有下面兩個函數: isValidIp()checkIfIpValid()。盡管命名不同、實現邏輯不同,但是功能是相同的,都是用來判定 IP 是否合法的。

出現這個現象的原因,可能是其中的一個同事不知道已有了 isValidIp() 的情況下,自己又定義并實現了相同用來校驗 IP 地址是否合法的 checkIfIpValid() 函數。

這兩個函數如下所示,它們是否違反了 DRY 原則?

public boolean isValidIp(String ip) {if (StringUtils.isBlank(ip)) { 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 ip.matches(regex);
}public boolean checkIfIpValid(String ip) {if (StringUtils.isBlank(ip)) { return false; }String[] ipUnits = StringUtils.split(ip, ".");if (ipUnits.length != 4) { return false; }for (int i = 0; i < ip.length(); 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;
}

在這個例子中,盡管兩段代碼的實現邏輯不重復,但語義重復(即功能重復),我們認為它們違反了 DRY 原則

我們應該在項目中,統一一種實現思路,所有用到判斷 IP 地址是否合法的地方,都統一調用一個函數。

假設,我們不統一實現思路,有些地方調用了 isValidIp(),有些地方又調用了 checkIfIpValid(),這就會導致代碼看起來很奇怪,相當于給代碼 “挖坑”,給不熟悉的這部分代碼的同事增加了閱讀難度。同事可能研究了半天,覺得功能一樣,但又有點疑問,覺得是不是有更高深的考量,才定義了兩個功能類似的函數,最終發現居然是代碼設計的問題。

另外,如果哪天項目中 IP 地址是否合法的判定邏輯改了,比如: 255.255.255.255 判定不合法,相應地,我們對 isValidIp() 的實現邏輯做了修改,但卻忘記修改 checkIfIpValid(),這樣就會導致有些代碼仍然用老的 IP 判定邏輯,導致出現一些莫名其妙的 BUG。

代碼執行重復

前兩個例子,一個是實現邏輯重復,一個是語義重復,在看下第三個例子。其中 UserServicelogin() 用來校驗用戶登錄是否成功。如果失敗,就返回異常;如果成功就返回用戶信息。具體代碼如下所示:

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

上面的代碼,既沒有邏輯重復,也沒有語義重復,但仍然違反了 DRY 原則。這是因為代碼存在 “執行重復”

重復執行最明顯的地阿福,就是在 login() 中,email 的校驗邏輯執行了兩次。一次是在調用 checkIfUserExisted() 函數的時候,另一次是調用 getUserByEmail() 的時候。這個問題解決起來比較簡答,只要將校驗邏輯從 UserRepo 中移除,統一放到 UserService 中就可以了。

此外,代碼中,還有移除比較隱藏的執行重復: login() 函數并不需要調用 checkIfUserExisted(),只需要調用一次 getUserByEmail() ,從數據庫中獲取用戶的 email、password 等信息,然后跟輸入的 email、password 信息做比對,判斷是否登錄成功。

這樣的優化是很有必要的。因為 checkIfUserExisted()getUserByEmail() 都需要查詢數據庫,而數據庫類的 I/O 操作是比較耗時的。我們在寫代碼的時候,應該盡量減少 I/O 操作。

按照剛剛的思路,我們重構下代碼。

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)

什么是代碼的復用性?

首先區分三個概念: 代碼復用性(Code Reusability)、代碼復用(Code Reuse)、DRY 原則。

  • 代碼復用表示一種行為:我們在開發新功能的時候,盡量復用已存在的代碼。
  • 代碼復用性表示一段代碼可被復用的特性或能力:我們在編寫代碼的時候,盡量讓代碼可復用。
  • DRY 原則是一條原則:不要寫重復的代碼。

首先,“不重復” 不代表 “可復用”。在一個項目中,可能不存在任何重復的代碼,但也不表示里面有可復用的代碼,不重復和可復用完全是兩個概念。所以,從這個角度來說,DRY 原則和可復用性將的是兩回事。

其次,“復用” 和 “可復用性” 關注角度不同。代碼 “可復用性” 是從代碼開發者的角度來講的,“復用” 是從代碼使用者角度來講的。比如 A 同事編寫了一個 UrlUrils 類,代碼的 “可復用性” 很好。同事 B 在開發新功能是,直接 “復用” A 同事編寫的 UrlUrils 類。

雖然復用性、復用、DRY 原則這三者從理解上有區別,但是它們的目的是一樣的,都是為了較少代碼量,提高代碼的可讀性、可維護性。此外,復用已經過測試的老代碼,bug 會比從零開發的要少。

“復用” 這個概念不僅可以指導細粒度的模塊、類、函數的設計開發,實際上,一些框架、類庫、組件等的生產也都是為了達到復用的目的。比如,Spring 框架、UI 組件等等。

怎么提高代碼復用性?

一共有 7 條規則:

  • 減少代碼耦合。對于高耦合的代碼,當希望復用其中的一個功能,想把這個功能的代碼抽取出來成為一個獨立的模塊、類或者函數時,往往會牽一發而動全身。所以,高耦合度的代碼會影響到代碼的可復用性。
  • 滿足單一職責原則。 前面講過,如果職責不夠單一,模塊、類設計得大而全,那就增加了代碼的耦合度(依賴它的,它依賴的代碼就會比較多)。也會影響到代碼的可復用性。相反,粒度越細的代碼,代碼的通用性會越好,容易被復用。
  • 模塊化。這里的 “模塊”,不單單只一組類構成的模塊,還可以理解為單個類、函數。我們要善于將功能獨立的代碼,封裝成模塊。獨立的模塊就像積木,更加容易復用,直接拿來搭建更加復雜的系統。
  • 業務與非業務邏輯分離。越是和業務無關的代碼余額容易復用,越是針對特定業務的代碼越難復用。所以,為了復用跟業務無關的代碼,我們將業務和非業務邏輯代碼分離,抽取成一些通用的框架、類庫、組件等。
  • 通用代碼下層。從分層角度來看,越底層的代碼越通用、會被越多的模塊調用,越應該設計得足夠可復用。一般情況下,在代碼分層之后,為了避免交叉調用導致調用關系混亂,我們只允許上層代碼調用下層代碼及同層代碼,杜絕下層代碼調用上層代碼。所以,通用的代碼我們盡量下沉到更下層。
  • 繼承、多態、抽象、封裝。在講面向對象特性的時候,我們講過,利用繼承可以將公共代碼抽取到父類,子類復用父類的屬性和方法。利用多態,可以動態替換一段代碼的部分邏輯,讓這段代碼可復用。此外,抽象和封裝,從更加廣義的層面、而非狹義的面向對象特性層面來理解的話,越抽象、越不依賴具體實現,越容易復用。代碼封裝成模塊,隱藏可變細節、暴露不變的接口,就越容易復用。
  • 應用模板等設計模式。一些設計模式,也能提高代碼復用性。比如,模板模式利用了多態技術來實現,可以靈活地替換其中的部分代碼,整個流程模板代碼可復用。

除了上面講到的 7 點,還有一些跟編程語言相關的特性,也可以提高代碼的復用性,比如泛型編程等。另外,除了上面講到的知識,復用意識也很重要。在寫代碼的時候,要取多思考,這部分代碼是否可以抽取出來,作為一個獨立模塊、類或者函數供多出使用。在設計每個模塊、類、函數的時候,要像設計一個外部 API 一樣,去思考它的復用性。

辯證思考和靈活應用

編寫可復用的代碼并不簡單。如果在編寫代碼時,已經有復用的需求場景,那根據復用的需求去開發可復用的代碼,可能還不算難。但是,如果當下沒有復用的需求,只是希望現在編寫的代碼具有可復用的特點,能在未來某個同事開發某個新功能時復用得上。在這種沒有具體復用需求的情況下,就需要去預測未來代碼會如何復用,這就比較有挑戰了。

實際上,除非有明確的復用需求,否則,為了暫時用不到的復用需求,花費太多時間、精力,投入太多成的開發成本,并不是一個值得推薦的做法。也違反我們之前講到的 YAGNI 原則。

實際上,我們在第一次寫代碼的時候如果當下沒有復用的需求,而未來的需求也不是特別明確,并且開發復用代碼的成本比較高,那我們就不需要考慮它的復用性。在之后,開發新能夠的時候,發現可以復用的之前的代碼,那我們就重構它,讓其變得更加復用。

總結

1.DRY 原則

講解了三種重復的情況:實現邏輯重復、語義重復、執行邏輯重復。

  • 實現邏輯重復,但功能語義不重復,并不違反 DRY 原則。
  • 實現邏輯不重復,但是功能語義重復,則違反了 DRY 原則。
  • 此外,代碼執行重復也是違反 DRY 原則。

2.代碼復用性

提高代碼可復用性的 7 點方法:

  • 減少代碼耦合
  • 滿足單一職責原則
  • 模塊化
  • 業務與非業務邏輯分離
  • 通用代碼下沉
  • 繼承、多態、抽象、封裝
  • 應用模板等設計模式

除了上面降到的方法外,復用意識也非常重要。在設計每個模塊、類、函數時,要像設計一個外部 API 一樣思考它的復用性。

在第一編寫代碼時,如果當下沒有復用需求,而未來的復用需求也不是特別明確,并且開發可復用代碼的成本比較高,那我們就不需要考慮代碼的復用性。在之后開發新功能時,發現可以復用之前的代碼,那我們就重構這段代碼,讓其變得更加可復用。

相對于代碼可復用,DRY 原則適用性更強一些。我們可以不寫可復用的代碼,但一定不能寫重復的代碼。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/716254.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/716254.shtml
英文地址,請注明出處:http://en.pswp.cn/news/716254.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

【機器學習】包裹式特征選擇之遞歸特征消除法

&#x1f388;個人主頁&#xff1a;豌豆射手^ &#x1f389;歡迎 &#x1f44d;點贊?評論?收藏 &#x1f917;收錄專欄&#xff1a;機器學習 &#x1f91d;希望本文對您有所裨益&#xff0c;如有不足之處&#xff0c;歡迎在評論區提出指正&#xff0c;讓我們共同學習、交流進…

電磁兼容(EMC):電解電容低阻如何選擇詳解

目錄 1 為何要選低阻電解電容 2 電解電容等效高頻等效電路 3 不同廠家ESR參數 4 高頻ESR特性 5 Low ESR鋁電解電容 1 為何要選低阻電解電容 在EMI超標時&#xff0c;將普通電解電容更換為低阻電解電容時&#xff0c;便通過了。這是因為低阻電解電容降低了功率回路的輻射電…

數字化轉型導師堅鵬:證券公司數字化轉型戰略、方法與案例

證券公司數字化轉型戰略、方法與案例 課程背景&#xff1a; 數字化轉型背景下&#xff0c;很多機構存在以下問題&#xff1a; 不清楚證券公司數字化轉型的發展戰略&#xff1f; 不知道證券公司數字化轉型的核心方法&#xff1f; 不知道證券公司數字化轉型的成功案例&am…

LLM 系列——BERT——論文解讀

一、概述 1、是什么 是單模態“小”語言模型&#xff0c;是一個“Bidirectional Encoder Representations fromTransformers”的縮寫&#xff0c;是一個語言預訓練模型&#xff0c;通過隨機掩蓋一些詞&#xff0c;然后預測這些被遮蓋的詞來訓練雙向語言模型&#xff08;編碼器…

【計算機網絡通信】計算機之間的局域網通信和互聯網通信方法(附Python和C#代碼)

文章目錄 前言一、局域網通信1.1 基本原理和方法1.1.1 獲取本地ip1.1.2 實現局域網內的廣播1.1.3 進行局域網通信 1.2 實現多客戶端連接1.3 Python源碼1.4 C#源碼1.5 可能存在的問題 二、互聯網通信2.1 實現原理2.1.1 內網穿透軟件2.1.2 實現互聯網通信 2.2 Python源碼2.3 C#源…

基于Java的超市商品管理系統(Vue.js+SpringBoot)

目錄 一、摘要1.1 簡介1.2 項目錄屏 二、研究內容2.1 數據中心模塊2.2 超市區域模塊2.3 超市貨架模塊2.4 商品類型模塊2.5 商品檔案模塊 三、系統設計3.1 用例圖3.2 時序圖3.3 類圖3.4 E-R圖 四、系統實現4.1 登錄4.2 注冊4.3 主頁4.4 超市區域管理4.5 超市貨架管理4.6 商品類型…

牛客小白月賽85_D-阿里馬馬和四十大盜

非常非常非常有意思的一道題,正好寫一下做題思路 對于到不了的情況,那就是存在連續>0的區間,該區間和>m,這樣不管怎么補血一定過不去,cin的時候,就可以判斷 最開始我以為是貪心,發現當前區間走不過去那就返回上一個0點補血,但就是過不去 突然我發現這個樣例很有意思 1…

Vant Weapp

Vant Weapp - 輕量、可靠的小程序 UI 組件庫 van-radio name 是一個字符串&#xff0c;無法傳對象的處理 以及 mpx 多層嵌套 for 循環處理 <viewwx:for"{{questionList}}"wx:for-item"question" // item 重命名wx:for-index"questionIndex"…

一文了解docker與k8s

隨著 k8s 作為容器編排解決方案變得越來越流行&#xff0c;有些人開始拿 Docker 和 k8s 進行對比&#xff0c;不禁問道&#xff1a;Docker 不香嗎&#xff1f; k8s 是 kubernetes 的縮寫&#xff0c;8 代表中間的八個字符。 其實 Docker 和 k8s 并非直接的競爭對手兩者相互依存…

Qt外部調用進程類QProcess的使用

有的時候我們需要在自己程序運行過程中調用其他進程&#xff0c;那么就需要用到QProcess。 首先可以了解一些關于進程的相關知識&#xff1a;線程與進程&#xff0c;你真得理解了嗎_進程和線程的區別-CSDN博客 進程是計算機中的程序關于某數據集合上的一次運行活動&#xff0…

Java面試——Redis

優質博文&#xff1a;IT-BLOG-CN 一、Redis 為什么那么快 【1】完全基于內存&#xff0c;絕大部分請求是純粹的內存操作&#xff0c;非常快速。數據存在內存中。 【2】數據結構簡單&#xff0c;對數據操作也簡單&#xff0c;Redis中的數據結構是專門進行設計的。 【3】采用單線…

【Vue3】全局切換字體大小

VueUse 先安裝VueUse <template><header><div class"left">left</div><div class"center">center</div><div class"right">right</div></header><div><button click"cha…

飛天使-學以致用-devops知識點4-SpringBoot項目CICD實現(實驗失敗,了解大概流程)

文章目錄 代碼準備創建jenkins 任務測試推送使用項目里面的jenkinsfile 進行升級操作 文字版本流程項目構建 代碼準備 推送代碼到gitlab 代碼去叩叮狼教育找 k8s 創建jenkins 任務 創建一個k8s-cicd-demo 流水線任務 將jenkins 里面構建時候的地址還有token&#xff0c; 給到…

azure devops工具實踐分析

對azure devops此工具的功能深挖&#xff0c;結合jira的使用經驗的分析 1、在backlog的功能描述&#xff0c;可理解為需求項&#xff0c;這里包括了bug&#xff0c;從開發的角度修復bug也是個工作項&#xff0c;所以需求的范圍是真正的需求&#xff08;開發接收到的已經確認的…

已解決org.springframework.web.multipart.MultipartException處理多部分請求異常的正確解決方法,親測有效!!!

已解決org.springframework.web.multipart.MultipartException處理多部分請求異常的正確解決方法&#xff0c;親測有效&#xff01;&#xff01;&#xff01; 目錄 問題分析 出現問題的場景 報錯原因 解決思路 解決方法 總結 在Web開發過程中&#xff0c;我們經常需要處…

基于JAVA協同過濾算法網上海鮮水產推薦購物商城系統設計與實現(Springboot框架)可行性分析

博主介紹&#xff1a;黃菊華老師《Vue.js入門與商城開發實戰》《微信小程序商城開發》圖書作者&#xff0c;CSDN博客專家&#xff0c;在線教育專家&#xff0c;CSDN鉆石講師&#xff1b;專注大學生畢業設計教育和輔導。 所有項目都配有從入門到精通的基礎知識視頻課程&#xff…

【PDF技巧】網上下載的pdf文件怎么才能編輯

不知道大家有沒有遇到過網上下載的PDF文件不能編輯的情況&#xff0c;今天我們來詳細了解一下導致無法編輯的原因即解決方法有哪些。 第一種原因&#xff1a;PDF文件中的內容是否是圖片&#xff0c;如果確認是圖片文件&#xff0c;那么我們想要編輯&#xff0c;就可以先使用PD…

分享經典、現代以及前沿軟件工程課程

https://www.icourse163.org/course/PKU-1003177002 隨著信息技術的發展&#xff0c;軟件已經深入到人類社會生產和生活的各個方面。軟件工程是將工程化的方法運用到軟件的開發、運行和維護之中&#xff0c;以達到提高軟件質量&#xff0c;降低開發成本的目的。軟件工程已經成為…

第三方支付牌照出讓,具備何種優勢的買方容易成功

在支付牌照并購的過程中&#xff0c;選擇一個合適的并購方是至關重要的。基于多年的支付牌照公司股權并購居間經驗&#xff0c;我發現具備以下特質的并購方在并購過程中表現得較為靠譜&#xff0c;他們不僅使得并購過程更為順暢&#xff0c;還能顯著提高并購的成功率。 并購方…

字符函數和字符串函數(下)

個人主頁&#xff08;找往期文章包括但不限于本期文章中不懂的知識點&#xff09;&#xff1a;我要學編程(?_?)-CSDN博客 目錄 strncpy函數的使用 函數原型&#xff1a; strncpy的使用 strncat函數的使用 函數原型&#xff1a; strncat的使用 strncmp函數的使用 函…