【架構設計】你真的理解軟體設計中的SOLID原則嗎?

JAVA旭陽發表於2023-01-14

前言

在軟體架構設計領域,有一個大名鼎鼎的設計原則——SOLID原則,它是由由Robert C. Martin(也稱為 Uncle Bob)提出的,指導我們寫出可維護、可以測試、高擴充套件、高內聚、低耦合的程式碼。是不是很牛,但是你們都理解這個設計原則嗎,如果理解不深入的話,更這我透過JAVA示例深入淺出的明白這個重要的原則吧。SOLID實際上是由5條原則組成, 我們逐一介紹。

S:單一職責原則(SRP)

O : 開閉原則 (OSP)

L : 里氏替換原則 (LSP)

I:介面隔離原則(ISP)

D:依賴倒置原則(DIP)

歡迎關注個人公眾號【JAVA旭陽】交流學習

單一職責原則(SRP)

這個原則指出“一個類應該只有一個改變的理由”,這意味著每個類都應該有單一的責任或單一的目的。

舉個例子來理解其中的核心思想,假設有一個BankService的類需要執行以下操作:

  1. 存錢
  2. 取錢
  3. 列印通票簿
  4. 獲取貸款資訊
  5. 傳送一次性密碼
package com.alvin.solid.srp;


public class BankService {

    // 存錢
    public long deposit(long amount, String accountNo) {
        //deposit amount
        return 0;
    }

    // 取錢
    public long withDraw(long amount, String accountNo) {
        //withdraw amount
        return 0;
    }

    // 列印通票簿
    public void printPassbook() {
        //update transaction info in passbook
    }

    // 獲取貸款資訊
    public void getLoanInterestInfo(String loanType) {
        if (loanType.equals("homeLoan")) {
            //do some job
        }
        if (loanType.equals("personalLoan")) {
            //do some job
        }
        if (loanType.equals("car")) {
            //do some job
        }
    }

    // 傳送一次性密碼
    public void sendOTP(String medium) {
        if (medium.equals("email")) {
            //write email related logic
            //use JavaMailSenderAPI
        }
    }

}

現在我們來看看這麼寫會帶來什麼問題?

比如對於獲取貸款資訊 getLoanInterestInfo() 方法,現在銀行服務只提供個人貸款、房屋貸款和汽車貸款的資訊,假設將來銀行的人想要提供一些其他貸款功能,如黃金貸款和學習貸款,那麼你需要修改這個類實現對嗎?

同樣,考慮 sendOTP() 方法,假設 BankService 支援將 OTP 媒體作為電子郵件傳送,但將來他們可能希望引入將 OTP 媒體透過手機簡訊傳送,這時候需要再次修改BankService來實現。

發現沒有,它不遵循單一職責原則,因為這個類有許多責任或任務要執行,不僅會讓BankService這個類很龐大,可維護性差。

為了實現單一職責原則的目標,我們應該實現一個單獨的類,它只執行單一的功能。

  • 列印相關的工作PrinterService
public class PrinterService {
 	public void printPassbook() {
        //update transaction info in passbook
    }
}
  • 貸款相關的工作LoanService
public class LoanService {
	public void getLoanInterestInfo(String loanType) {
        if (loanType.equals("homeLoan")) {
            //do some job
        }
        if (loanType.equals("personalLoan")) {
            //do some job
        }
        if (loanType.equals("car")) {
            //do some job
        }
    }
}
  • 通知相關的工作NotificationService
public class NotificationService{
	public void sendOTP(String medium) {
        if (medium.equals("email")) {
            //write email related logic
            //use JavaMailSenderAPI
        }
    }
}

現在,如果你觀察到每個類都有單一的責任來執行他們的任務。這正是 單一職責 SRP 的核心思想。

開閉原則(OSP)

該原則指出“軟體實體(類、模組、函式等)應該對擴充套件開放,但對修改關閉”,這意味著您應該能夠擴充套件類行為,而無需修改它。

讓我們透過一個例子來理解這個原則。讓我們考慮一下我們剛剛建立的同一個通知服務。

public class NotificationService {
	public void sendOTP(String medium) {
        if (medium.equals("email")) {
            //write email related logic
            //use JavaMailSenderAPI
        }
    }
}

如前所述,如果您想透過手機號碼傳送 OTP,那麼您需要修改 NotificationService,對嗎?

但是根據OSP原則,對擴充套件開放,對修改關閉, 因此不建議為增加一個通知方式就修改NotificationService類,而是要擴充套件,怎麼擴充套件呢?

  • 定義一個通知服務介面
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
	}
}

這樣的方式就是遵循開閉原則的,你不用修改核心的業務邏輯,這樣可能帶來意向不到的後果,而是擴充套件實現方式,由呼叫方根據他們的實際情況呼叫。

里氏替換原則(LSP)

該原則指出“派生類或子類必須可替代其基類或父類”。換句話說,如果類 A 是類 B 的子型別,那麼我們應該能夠在不中斷程式行為的情況下用 A 替換 B。

這個原理有點棘手和有趣,它是基於繼承概念設計的,所以讓我們透過一個例子更好地理解它。

讓我們考慮一下我有一個名為 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);
}

社交媒體可以有多個實現或可以有多個子類,如 FacebookWechatWeiboTwitter 等。

現在讓我們假設 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都提供了所有上述的功能,所以這裡我們可以認為FacebookSocialMedia類的完全替代品,兩者都可以無中斷地替代。

現在讓我們討論 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。所以我們認為它是不符合裡式替換原則。

那有什麼解決方案嗎?

那就把功能拆開唄。

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。

介面隔離原則(ISP)

這個原則是第一個適用於介面而不是 SOLID 中類的原則,它類似於單一職責原則。它宣告“不要強迫任何客戶端實現與他們無關的介面”。

例如,假設您有一個名為 UPIPayment 的介面,如下所示

public interface UPIPayments {
    
    public void payMoney();
    
    public void getScratchCard();
    
    public void getCashBackAsCreditBalance();
}

現在讓我們談談 UPIPayments 的一些實現,比如 Google PayAliPay

Google Pay 支援這些功能所以他可以直接實現這個 UPIPaymentsAliPay 不支援 getCashBackAsCreditBalance() 功能所以這裡我們不應該強制客戶端 AliPay 透過實現 UPIPayments 來覆蓋這個方法。

我們需要根據客戶需要分離介面,所以為了支援這個ISP,我們可以如下設計:

  • 建立一個單獨的介面來處理現金返還。
public interface CashbackManager{ 
	public void getCashBackAsCreditBalance(); 
}

現在我們可以從 UPIPayments 介面中刪除getCashBackAsCreditBalanceAliPay也不需要實現getCashBackAsCreditBalance()這個它沒有的方法了。

依賴倒置原則(DIP)

該原則指出我們需要使用抽象(抽象類和介面)而不是具體實現,高階模組不應該直接依賴於低階模組,但兩者都應該依賴於抽象。

我們直接上例子來理解。

假如你去當地一家商店買東西,並決定使用刷卡付款。因此,當您將卡交給店員進行付款時,店員不會檢查你提供的是哪種卡,簽帳金融卡還是信用卡,他們只會進行刷卡,這就是店員和你之間傳遞“卡”這個抽象。

現在讓我們用程式碼替換這個例子,以便更好地理解它。

  • 簽帳金融卡
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); 
}
  • 現在 DebitCardCreditCard 都實現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);
    }
}

現在,如果您觀察購物中心與 BankCard 鬆散耦合,任何型別的卡處理支付都不會產生任何影響,這就是符合依賴倒置原則的。

總結

我們再來回顧總結下SOLID原則,

單一職責原則:每個類應該負責系統的單個部分或功能。

開閉原則:軟體元件應該對擴充套件開放,而不是對修改開放。

裡式替換原則:超類的物件應該可以用其子類的物件替換而不破壞系統。

介面隔離原則不應強迫客戶端依賴於它不使用的方法。

依賴倒置原則:高層模組不應該依賴低層模組,兩者都應該依賴抽象。

這些原則看起來都很簡單,但用起來用的好就比較難了,希望大家在平時的開發的過程中多多思考、多多實踐。

歡迎關注個人公眾號【JAVA旭陽】交流學習

相關文章