他們在學校里不會教你的編程原則
前言
在大學的時候,學校一般只會教你你寫編程語言,比如C、C++、JAVA等編程語言。但是當你離開大學進入這個行業開始工作時,才知道編程不只是知道編程語言、語法等,要想寫好代碼,必須還要了解一些編程原則才行。本文主要討論KISS
、DRY
和SOLID
這些常見的編程原則,而且你會發現隨著工作時間越久,越能感受這些編程原則的精妙之處,歷久彌香。
KISS原則
Keep It Simple, Stupid!
你是不是有過接手同事的代碼感到十分頭疼的經歷,明明可以有更加簡單、明白的寫法,非要繞來繞去,看不明白?
其實,我們在寫代碼的時候應該要遵守KISS
原則,核心思想就是盡量保持簡單。代碼的可讀性和可維護性是衡量代碼質量非常重要的兩個標準。而 KISS
原則就是保持代碼可讀和可維護的重要手段。代碼足夠簡單,也就意味著很容易讀懂,bug 比較難隱藏。即便出現 bug,修復起來也比較簡單。
我們寫代碼的的時候要站在別人的角度出發,就像馬丁·福勒說的,我們寫的代碼不是給機器看的,而是給人看的。
“任何傻瓜都可以編寫計算機可以理解的代碼。優秀的程序員編寫出人類可以理解的代碼?!?— 馬丁·福勒
那么如何才能寫出滿足KISS原則的代碼呢?
如何寫出KISS原則的代碼?
我們直接上例子,下面的校驗IP是否合法的3種實現方式,大家覺得哪個最KISS
?
- 寫法一
- 寫法二
- 寫法三
- 寫法一代碼量最少,正則表達式本身是比較復雜的,寫出完全沒有 bug 的正則表達本身就比較有挑戰;另一方面,并不是每個程序員都精通正則表達式。對于不怎么懂正則表達式的同事來說,看懂并且維護這段正則表達式是比較困難的。這種實現方式會導致代碼的可讀性和可維護性變差,所以,從
KISS
原則的設計初衷上來講,這種實現方式并不符合KISS
原則。 - 寫法二使用了
StringUtils
類、Integer
類提供的一些現成的工具函數,來處理 IP地址字符串,邏輯清晰,可讀性好。 - 寫法三不使用任何工具函數,而是通過逐一處理 IP 地址中的字符,來判斷是否合法,容易出bug,不好理解。
所以說,符合KISS
原則的代碼并不是代碼越少越好,還要考慮代碼是否邏輯清晰、是否容易理解、是否夠穩定。
總結以下如何寫出KISS
原則的代碼:
- 不要使用同事可能不懂的技術來實現代碼。比如前面例子中的正則表達式,還有一些編程語言中過于高級的語法等。
- 不要重復造輪子,要善于使用已經有的工具類庫。經驗證明,自己去實現這些類庫,出
bug
的概率會更高,維護的成本也比較高。 - 不要過度優化。不要過度使用一些奇技淫巧(比如,位運算代替算術運算、復雜的條件語句代替
if-else
、使用一些過于底層的函數等)來優化代碼,犧牲代碼的可讀性。 - 主觀站在別人的角度上編寫代碼。你在編寫代碼的時候就要思考我這個同事看這段代碼是不是很快就能夠明白理解。
DRY原則
Don't Repeat Yourself
你是不是有過這樣的經歷,項目中很多重復邏輯的代碼,然后修改一個地方,另外一個地方忘記修改,導致測試給你提了很多bug?
DRY
原則,英文全稱Don’t Repeat Yourself
,直譯過來就是不要重復你自己。這里的重復不僅僅是代碼一模一樣,還包括實現邏輯重復、功能語義重復、代碼執行重復等。我們不要偷懶,有責任把這些存在重復的地方識別出來,然后優化它們。
如何寫出DRY原則的代碼呢?
我們直接上例子,代碼重復的我就不講了,很好理解,關于實現邏輯或者功能語義重復的我覺個例子。
還是上面校驗IP的例子,團隊中兩個同事由于不知道就有了兩種寫法。
- 同事A寫法
- 同事B寫法
盡管兩段代碼的實現邏輯不重復,但語義重復,也就是功能重復,我們認為它違反了 DRY
原則。我們應該在項目中,統一一種實現思路,所有用到判斷 IP
地址是否合法的地方,都統一調用同一個函數。不然哪天校驗規則變了,很容易只改了其中一個,另外一個漏改,就會出現莫名其妙的bug
。
其他的比如邏輯重復的意思是雖然功能是不一致的,但是里面的邏輯都是一模一樣的。舉個例子,比如校驗用戶名和校驗密碼,雖然功能不一致,但是校驗邏輯都是相似,判空、字符長度等等,這種情況我們就需要把相似的邏輯抽取到一個方法中,不然也是不符合DRY
原則。
那么我們平時寫代碼注意些什么才是符合DRY
原則呢?
- 使用現成的輪子,不輕易造輪子
其實最關鍵的就是寫代碼帶腦子,用到一個方法先看看有沒有現成的,不要看看不看,就動手在那里造輪子。
- 減少代碼耦合
對于高度耦合的代碼,當我們希望復用其中的一個功能,想把這個功能的代碼抽取出來成為一個獨立的模塊、類或者函數的時候,往往會發現牽一發而動全身。移動一點代碼,就要牽連到很多其他相關的代碼。所以,高度耦合的代碼會影響到代碼的復用性,我們要盡量減少代碼耦合。
- 滿足單一職責原則
我們前面講過,如果職責不夠單一,模塊、類設計得大而全,那依賴它的代碼或者它依賴的代碼就會比較多,進而增加了代碼的耦合。根據上一點,也就會影響到代碼的復用性。相反,越細粒度的代碼,代碼的通用性會越好,越容易被復用。
- 模塊化
這里的“模塊”,不單單指一組類構成的模塊,還可以理解為單個類、函數。我們要善于將功能獨立的代碼,封裝成模塊。獨立的模塊就像一塊一塊的積木,更加容易復用,可以直接拿來搭建更加復雜的系統。
- 業務與非業務邏輯分離
越是跟業務無關的代碼越是容易復用,越是針對特定業務的代碼越難復用。所以,為了復用跟業務無關的代碼,我們將業務和非業務邏輯代碼分離,抽取成一些通用的框架、類庫、組件等。
- 通用代碼下沉
從分層的角度來看,越底層的代碼越通用、會被越多的模塊調用,越應該設計得足夠可復用。一般情況下,在代碼分層之后,為了避免交叉調用導致調用關系混亂,我們只允許上層代碼調用下層代碼及同層代碼之間的調用,杜絕下層代碼調用上層代碼。所以,通用的代碼我們盡量下沉到更下層。
- 繼承、多態、抽象、封裝
在講面向對象特性的時候,我們講到,利用繼承,可以將公共的代碼抽取到父類,子類復用父類的屬性和方法。利用多態,我們可以動態地替換一段代碼的部分邏輯,讓這段代碼可復用。除此之外,抽象和封裝,從更加廣義的層面、而非狹義的面向對象特性的層面來理解的話,越抽象、越不依賴具體的實現,越容易復用。代碼封裝成模塊,隱藏可變的細節、暴露不變的接口,就越容易復用。
- 應用模板等設計模式
一些設計模式,也能提高代碼的復用性。比如,模板模式利用了多態來實現,可以靈活地替換其中的部分代碼,整個流程模板代碼可復用。
SOLID原則
SOLID
原則不是一個單一的原則,而是對軟件開發至關重要的 5 條原則,遵循這些原則有助于我們寫出高內聚、低耦合、可擴展、可維護性好的代碼。
S—單一職責原則
一個類應該有一個,而且只有一個改變它的理由。
單一職責原則在我看來是最容易理解也是最重要的一個原則。它的核心思想就是一個模塊、類或者方法只做一件事,只有一個職責,千萬不要越俎代庖。它可以帶來下面的好處:
- 可以讓代碼耦合度更低
- 使代碼更容易理解和維護
- 使代碼更易于測試和維護,使軟件更易于實施,并有助于避免未來更改的意外副作用
舉個例子,我們有兩個類Person
和Account
。 兩者都負有存儲其特定信息的單一責任。 如果要更改Person
的狀態,則無需修改類Account
,反之亦然, 不要把賬戶的行為比如修改賬戶名changeAcctName
寫在Person
類中。
public class Person {
private Long personId;
private String firstName;
private String lastName;
private String age;
private List<Account> accounts;
// 錯誤做法
public void changeAcctName(Account account, String acctName) {
acccount.setAccountName(acctName);
// 更新到數據庫
}
}
public class Account {
private Long guid;
private String accountNumber;
private String accountName;
private String status;
private String type;
}
所以大家在編寫代碼的時候,一定要停頓思考下這個段代碼真的寫在這里嗎?另外很關鍵的一點是如果發現一個類或者一個方法十分龐大,那么很有可能已經違背單一職責原則了,后續維護可想而知十分痛苦。
O—開閉原則
軟件實體(類、模塊、函數等)應該對擴展開放,對修改關閉。
對擴展開放,對修改關閉,什么意思?很簡單,其實就是我們要盡量通過新增類實現功能,而不是修改原有的類或者邏輯。因為修改已有代碼很有可能對已有功能引入bug。
讓我們通過一個例子來理解這個原則,比如一個通知服務。
public class NotificationService {
public void sendOTP(String medium) {
if (medium.equals("email")) {
//email 發送
} else if (medium.equals("mobile")) {
// 手機發送
}
}
現在需要新增微信的方式通知,你要怎么做呢? 是在加一個if else
嗎? 這樣就不符合開閉原則了,我們看下開閉原則該怎么寫。
- 定義一個通知服務接口
public interface NotificationService {
public void sendOTP();
}
- E-mail方式通知類
EmailNotification
public class EmailNotification implements NotificationService{
public void sendOTP(){
// write Logic using JavaEmail api
}
}
- 手機方式通知類
MobileNotification
public class MobileNotification implements NotificationService{
public void sendOTP(){
// write Logic using Twilio SMS API
}
}
- 同樣可以添加微信通知服務的實現
WechatNotification
public class WechatNotification implements NotificationService{
public void sendOTP(String medium){
// write Logic using wechat API
}
}
這樣的方式就是遵循開閉原則的,你不用修改核心的業務邏輯,這樣可能帶來意向不到的后果,而是擴展實現方式,由調用方根據他們的實際情況調用。
是不是想到了設計模式中的策略模式,其實設計模式就是指導我們寫出高內聚、低耦合的代碼。
L—里氏替換原則
派生類或子類必須可替代其基類或父類
這個原則稍微有點難以理解,它的核心思想是每個子類或派生類都應該可以替代/等效于它們的基類或父類。這樣有一個好處,就是無論子類是什么類型,客戶端通過父類調用都不會產生意外的后果。
理解不了?那我我們通過一個例子來理解一下。
讓我們考慮一下我有一個名為 SocialMedia
的抽象類,它支持所有社交媒體活動供用戶娛樂,如下所示:
package com.alvin.solid.lsp;
public abstract class SocialMedia {
public abstract void chatWithFriend();
public abstract void publishPost(Object post);
public abstract void sendPhotosAndVideos();
public abstract void groupVideoCall(String... users);
}
社交媒體可以有多個實現或可以有多個子類,如 Facebook
、Wechat
、Weibo
和 Twitter
等。
現在讓我們假設 Facebook
想要使用這個特性或功能。
package com.alvin.solid.lsp;
public class Wechat extends SocialMedia {
public void chatWithFriend() {
//logic
}
public void publishPost(Object post) {
//logic
}
public void sendPhotosAndVideos() {
//logic
}
public void groupVideoCall(String... users) {
//logic
}
}
我們都知道Facebook
都提供了所有上述的功能,所以這里我們可以認為Facebook
是SocialMedia
類的完全替代品,兩者都可以無中斷地替代。
現在讓我們討論 Weibo
類
package com.alvin.solid.lsp;
public class Weibo extends SocialMedia {
public void chatWithFriend() {
//logic
}
public void publishPost(Object post) {
//logic
}
public void sendPhotosAndVideos() {
//logic
}
public void groupVideoCall(String... users) {
//不適用
}
}
我們都知道Weibo
微博這個產品是沒有群視頻功能的,所以對于 groupVideoCall
方法來說 Weibo
子類不能替代父類 SocialMedia
。所以我們認為它是不符合里式替換原則。
如果強行這么做的話,會導致客戶端用父類SocialMedia
調用,但是實現類注入的可能是個Weibo
的實現,調用groupVideoCall
行為,產生意想不到的后果。
那有什么解決方案嗎?
那就把功能拆開唄。
public interface SocialMedia {
public void chatWithFriend();
public void sendPhotosAndVideos()
}
public interface SocialPostAndMediaManager {
public void publishPost(Object post);
}
public interface VideoCallManager{
public void groupVideoCall(String... users);
}
現在,如果您觀察到我們將特定功能隔離到單獨的類以遵循LSP。
現在由實現類決定支持功能,根據他們所需的功能,他們可以使用各自的接口,例如 Weibo
不支持視頻通話功能,因此 Weibo
實現可以設計成這樣:
public class Instagram implements SocialMedia,SocialPostAndMediaManager{
public void chatWithFriend(){
//logic
}
public void sendPhotosAndVideos(){
//logic
}
public void publishPost(Object post){
//logic
}
}
這樣子就是符合里式替換原則LSP。
I—接口隔離原則
接口不應該強迫他們的客戶依賴它不使用的方法。
大家可以看看自己的工程,是不是一個接口類中有很多很多的接口,每次調用API
方法的時候IDE
工具給你彈出一大堆,十分的"臃腫肥胖"。所以該原則的核心思想要將你的接口拆小,拆細,打破”胖接口“,不用強迫客戶端實現他們不需要的接口。是不是和單一職責原則有點像?
例如,假設有一個名為 UPIPayment
的接口,如下所示
public interface UPIPayments {
public void payMoney();
public void getScratchCard();
public void getCashBackAsCreditBalance();
}
現在讓我們談談 UPIPayments
的一些實現,比如 Google Pay
和 AliPay
。
Google Pay
支持這些功能所以他可以直接實現這個 UPIPayments
但 AliPay
不支持 getCashBackAsCreditBalance()
功能所以這里我們不應該強制客戶端 AliPay
通過實現 UPIPayments
來覆蓋這個方法。
我們需要根據客戶需要分離接口,所以為了滿足接口隔離原則,我們可以如下設計:
- 創建一個單獨的接口來處理現金返還。
public interface CashbackManager{
public void getCashBackAsCreditBalance();
}
現在我們可以從 UPIPayments
接口中刪除getCashBackAsCreditBalance
,AliPay
也不需要實現getCashBackAsCreditBalance()
這個它沒有的方法了。
D—依賴倒置原則
高層模塊不應該依賴低層模塊,兩者都應該依賴于抽象(接口)。抽象不應該依賴于細節(具體實現),細節應該取決于抽象。
這個原則我覺得也不是很好理解,所謂高層模塊和低層模塊的劃分,簡單來說就是,在調用鏈上,調用者屬于高層,被調用者屬于低層。比如大家都知道的MVC模式,controller
是調用service
層接口這個抽象,而不是實現類。這也是我們經常說的要面向接口編程,而非細節或者具體實現,因為接口意味著契約,更加穩定。
我們通過一個例子加深一下理解。
- 借記卡
public class DebitCard {
public void doTransaction(int amount){
System.out.println("tx done with DebitCard");
}
}
- 信用卡
public class CreditCard{
public void doTransaction(int amount){
System.out.println("tx done with CreditCard");
}
}
現在用這兩張卡你去購物中心購買了一些訂單并決定使用信用卡支付
public class ShoppingMall {
private DebitCard debitCard;
public ShoppingMall(DebitCard debitCard) {
this.debitCard = debitCard;
}
public void doPayment(Object order, int amount){
debitCard.doTransaction(amount);
}
public static void main(String[] args) {
DebitCard debitCard=new DebitCard();
ShoppingMall shoppingMall=new ShoppingMall(debitCard);
shoppingMall.doPayment("some order",5000);
}
}
上面的做法是一個錯誤的方式,因為 ShoppingMall
類與 DebitCard
緊密耦合。
現在你的借記卡余額不足,想使用信用卡,那么這是不可能的,因為 ShoppingMall
與借記卡緊密結合。
當然你也可以這樣做,從構造函數中刪除借記卡并注入信用卡。但這不是一個好的方式,它不符合依賴倒置原則。
那該如何正確設計呢?
- 定義依賴的抽象接口
BankCard
public interface BankCard {
public void doTransaction(int amount);
}
- 現在
DebitCard
和CreditCard
都實現BankCard
public class CreditCard implements BankCard{
public void doTransaction(int amount){
System.out.println("tx done with CreditCard");
}
}
public class DebitCard implements BankCard {
public void doTransaction(int amount){
System.out.println("tx done with DebitCard");
}
}
- 現在重新設計購物中心這個高級類,他也是去依賴這個抽象,而不是直接低級模塊的實現類
public class ShoppingMall {
private BankCard bankCard;
public ShoppingMall(BankCard bankCard) {
this.bankCard = bankCard;
}
public void doPayment(Object order, int amount){
bankCard.doTransaction(amount);
}
public static void main(String[] args) {
BankCard bankCard=new CreditCard();
ShoppingMall shoppingMall1=new ShoppingMall(bankCard);
shoppingMall1.doPayment("do some order", 10000);
}
}
我們還可以拿 Tomcat
這個 Servlet
容器作為例子來解釋一下。
Tomcat
是運行 Java Web
應用程序的容器。我們編寫的 Web
應用程序代碼只需要部署在Tomcat
容器下,便可以被 Tomcat
容器調用執行。按照之前的劃分原則,Tomcat
就是高層模塊,我們編寫的 Web
應用程序代碼就是低層模塊。Tomcat
和應用程序代碼之間并沒有直接的依賴關系,兩者都依賴同一個“抽象”,也就是 Sevlet
規范。Servlet
規范不依賴具體的 Tomcat
容器和應用程序的實現細節,而 Tomcat
容器和應用程序依賴 Servlet
規范。
總結
本文總結了軟件編程中的黃金原則,KISS
原則,DRY
原則,SOLID
原則。這些原則不僅僅適用于編程,也可以指導我們在架構設計上。雖然其中有些原則很抽象,但是大家多多實踐和思考,會體會到這些原則的精妙。
歡迎關注個人公眾號【JAVA旭陽】交流學習
本文來自博客園,作者:JAVA旭陽,轉載請注明原文鏈接:http://www.otorinolaringoiatradellepiane.com/alvinscript/p/17433913.html