參考鏈接:https://www.imooc.com/read/53/article/1078
1、基本概念
本節開始介紹設計模式的七大原則的基本概念,其中包括開閉原則、單一職責原則、里氏替換原則、依賴倒置原則,剩下的三大原則(接口隔離原則、迪米特法則、合成複用原則)會在下一節繼續講解。
本節以介紹基本概念爲主,其中會加入部分演示代碼、uml 類圖講解,能理解基本概念即可。後續章節設計模式的講解會詳細介紹這些原則的應用。
本節主要內容有:
-
什麼是單一職責原則、里氏替換原則及依賴倒置原則
-
爲何要遵循這些原則
2.開閉原則:
開閉原則(Open Closed Principle,OCP)由勃蘭特・梅耶(Bertrand Meyer)提出,他在 1988 年的著作《面向對象軟件構造》(Object Oriented Software Construction)中提出:軟件實體應當對擴展開放,對修改關閉(Software entities should be open for extension,but closed for modification),這就是開閉原則的經典定義。
開閉原則是設計模式中的總原則,開閉原則就是說:對拓展開放、對修改關閉。 模塊應該在儘量不修改代碼的前提下進行拓展,這就需要使用接口和抽象類來實現預期效果。
我們舉例說明什麼是開閉原則,以 4s 店銷售汽車爲例,其類圖如圖所示:
ICar 接口定義了汽車的兩個屬性:名稱和價格。BenzCar 是一個奔馳車的實現類,代表所有奔馳車的總稱。Shop4S 代表售賣的 4s 店,ICar 接口的代碼清單如下:
public interface ICar {
// 車名
public String getName();
// 車價格
public int getPrice();
}
一般情況下 4s 店只售出一種品牌車,這裏以梅賽德斯奔馳爲例,奔馳車類如代碼清單所示:
public class BenzCar implements ICar{
// 車名
private String name;
// 車價格
private int price;
// 通過構造方法實例化
public BenzCar(String _name, int _price) {
this.name = _name;
this.price = _price;
}
// 獲取車名
@Override
public String getName() {
return this.name;
}
// 獲取車價格
@Override
public int getPrice() {
return this.price;
}
}
然後我們模擬下 4s 店售車記錄,Shop4S 類代碼清單如下所示:
import java.util.ArrayList;
public class Shop4S {
private final static ArrayList<ICar> carList = new ArrayList<ICar>();
// 使用static代碼塊模擬數據初始化操作
static {
carList.add(new BenzCar("梅賽德斯-邁巴赫S級轎車",138));
carList.add(new BenzCar("梅賽德斯-AMG S 63 L 4MATIC+", 230));
carList.add(new BenzCar("梅賽德斯-奔馳V級", 50));
}
public static void main(String[] args) {
System.out.println("4s店售車記錄:");
for (ICar car: carList){
System.out.println("車名:" + car.getName() + "\t價格:" + car.getPrice() + "萬元");
}
}
}
在 Shop4S 類中,使用 static 代碼塊來模擬數據初始化過程,使用私有變量集合 carList 來記錄所有售出車輛信息,一般項目中這部分都由持久化曾來完成,運行效果如下:
4s店售車記錄:
車名:梅賽德斯-邁巴赫S級轎車 價格:138萬元
車名:梅賽德斯-AMG S 63 L 4MATIC+ 價格:230萬元
車名:梅賽德斯-奔馳V級 價格:50萬元
暫時來看,以上設計是沒有啥問題的。但是,某一天,4s 店老闆說奔馳轎車統一要收取一筆金融服務費,收取規則是價格在 100 萬元以上的收取 5%,50~100 萬元的收取 2%,其餘不收取。爲了應對這種需求變化,之前的設計又該如何呢?
目前,解決方案大致有如下三種:
- 修改 ICar 接口:在 ICar 接口上加一個 getPriceWithFinance 接口,專門獲取加上金融服務費後的價格信息。這樣的後果是,實現類 BenzCar 也要修改,業務類 Shop4S 也要做相應調整。ICar 接口一般應該是足夠穩定的,不應頻繁修改,否則就失去了接口鍥約性了。
- 修改 BenzCar 實現類:直接修改 BenzCar 類的 getPrice 方法,添加金融服務費的處理。這樣的一個直接後果就是,之前依賴 getPrice 的業務模塊的業務邏輯就發生了改變了,price 也不是之前的 price 了。
- 使用子類拓展來實現:增加子類 FinanceBenzCar,覆寫父類 BenzCar 的 getPrice 方法,實現金融服務費相關邏輯處理。這樣的好處是:只需要調整 Shop4S 中的靜態模塊區中的代碼,main 中的邏輯是不用做任何修改的。
修改後的 FinanceBenzCar 類代碼清單如下:
public class FinanceBenzCar extends BenzCar{
public FinanceBenzCar(String _name, int _price) {
super(_name, _price);
}
// 覆寫價格信息
@Override
public int getPrice() {
// 獲取原價
int selfPrice = super.getPrice();
int financePrice = 0;
if (selfPrice >= 100) {
financePrice = selfPrice + selfPrice * 5 / 100; // 收取5%的金融服務費
} else if (selfPrice >= 50) {
financePrice = selfPrice + selfPrice * 2 / 100; // 收取2%的金融服務費
} else {
financePrice = selfPrice; // 其餘不收取服務費
}
return financePrice;
}
}
再來看看修改後的 Shop4S 類代碼清單如下:
import java.util.ArrayList;
public class Shop4S {
private final static ArrayList<ICar> carList = new ArrayList<ICar>();
private final static ArrayList<ICar> financeCarList = new ArrayList<ICar>();
// 使用static代碼塊模擬數據初始化操作
static {
carList.add(new BenzCar("梅賽德斯-邁巴赫S級轎車",138));
carList.add(new BenzCar("梅賽德斯-AMG S 63 L 4MATIC+", 230));
carList.add(new BenzCar("梅賽德斯-奔馳V級", 50));
financeCarList.add(new FinanceBenzCar("梅賽德斯-邁巴赫S級轎車",138));
financeCarList.add(new FinanceBenzCar("梅賽德斯-AMG S 63 L 4MATIC+", 230));
financeCarList.add(new FinanceBenzCar("梅賽德斯-奔馳V級", 50));
}
public static void main(String[] args) {
System.out.println("4s店售車記錄(不含金融服務費):");
for (ICar car: carList){
System.out.println("車名:" + car.getName() + "\t價格:" + car.getPrice() + "萬元");
}
System.out.println("\n4s店售車記錄(包含金融服務費):");
for (ICar car: financeCarList) {
System.out.println("車名:" + car.getName() + "\t價格:" + car.getPrice() + "萬元");
}
}
}
運行效果如下:
4s店售車記錄(不含金融服務費):
車名:梅賽德斯-邁巴赫S級轎車 價格:138萬元
車名:梅賽德斯-AMG S 63 L 4MATIC+ 價格:230萬元
車名:梅賽德斯-奔馳V級 價格:50萬元
4s店售車記錄(包含金融服務費):
車名:梅賽德斯-邁巴赫S級轎車 價格:144萬元
車名:梅賽德斯-AMG S 63 L 4MATIC+ 價格:241萬元
車名:梅賽德斯-奔馳V級 價格:51萬元
這樣,在業務規則發生改變的情況下,我們通過拓展子類及修改持久層(高層次模塊)便足以應對多變的需求。開閉原則要求我們儘可能通過拓展來實現變化,儘可能少地改變已有模塊,特別是底層模塊。
開閉原則總結:
- 提高代碼複用性
- 提高代碼的可維護性
3.單一職責原則
單一職責原則,簡單得來說就是保證設計類、接口、方法時做到功能單一,權責明確。
怎麼理解呢?比如應用開發時經常會有修改用戶信息的接口,如下:這裏我們定義 “更新用戶” 的接口,倘若有一天新來的前端要求加一個修改用戶密碼的接口,後端直接說:“你去調 updateUser ” 接口吧,傳入密碼信息就行。這種後端往往不是太懶就是新手,updateUser 接口的粒度太粗,接口職責不夠單一,所以應該將接口拆分爲各個細分接口,比如修改如下:
這裏很明顯,我們看到分拆後的接口職責更加單一,權責更加清楚,日後維護開發也更加便捷。
單一職責原則,指的是一個類或者模塊有且只有一個改變的原因。 如果模塊或類承擔的職責過多,就等於這些職責耦合在一起,這樣一個模塊的變快可能會削弱或抑制其它模塊的能力,這樣的耦合是十分脆弱地。所以應該儘量保持單一職責原則,此原則的核心就是解耦和增強內聚性。
在現在流行的微服務架構體系中,最頭疼的就是服務拆分,拆分的粒度也很有講究,標準的應該是遵從單一原則,避免服務拆分時發生各種撕逼行爲:” 本應該在 A 服務中的被安排在了 B 服務中 “,所以服務的職責劃分尤爲重要。
再有就是,做 service 層開發時,早期的開發人員會將數據庫操作放在 service 中,比如 getConnection,然後執行 prepareStatement,再就是 service 邏輯處理等等。可是後來發現數據庫要由原來的 mysql 變更爲 oracle,service 層代碼豈不是需要重寫一遍,天了嚕… 直接崩潰跑路。
” 我單純,所以我快樂 “用來形容單一職責原則再恰當不過了。
單一職責原則總結:
-
單一職責可以降低類的複雜性,提高代碼可讀性、可維護性
-
但是用 “職責” 或 “變化原因” 來衡量接口或類設計得是否優良,但是 “職責” 和 “變化原因” 都是不可度量的,因項目、環境而異;指責劃分稍微不當,很容易造成資源浪費,代碼量增多,好比微服務時服務邊界拆分不清
4.里氏替換原則
里氏替換原則的解釋是,所有引用基類的地方必須能透明地使用其子類的對象。 通俗來講的話,就是說,只要父類能出現的地方子類就可以出現,並且使用子類替換掉父類的話,不會產生任何異常或錯誤,使用者可能根本就不需要知道是父類還是子類。反過來就不行了,有子類的地方不一定能使用父類替換。
比如某個方法接受一個 Map 型參數,那麼它一定可以接受 HashMap、LinkedHashMap 等參數,但是反過來的話,一個接受 HashMap 的方法不一定能接受所有 Map 類型參數。
里氏替換原則是開閉原則的實現基礎,它告訴我們設計程序的時候儘可能使用基類進行對象的定義及引用,具體運行時再決定基類對應的具體子類型。
接下來舉個栗子,我們定義一個抽象類 AbstractAnimal 對象,該對象聲明內部方法 “跳舞”,其中,Rabbit、Dog、Lion 分別繼承該對象,另外聲明一個 Person 類,該類負責餵養各種動物,Client 類負責邏輯調用,類圖如下:
其中,Person 類代碼如下:
public class Person {
private AbstractAnimal animal;
public void feedAnimal(AbstractAnimal _animal) {
this.animal = _animal;
}
public void walkAnimal(){
System.out.println("人開始溜動物...");
animal.dance();
}
}
main 函數調用的時候如下:
public class Main {
public static void main(String[] args) {
Person person = new Person();
person.feedAnimal(new Rabbit());
person.walkAnimal();
}
}
打印輸出:
人開始溜動物…
小白兔跳舞…
這裏,Person 類中本該出現的父類 AbstractAnimal 我們運行時使用具體子類代替,只要是父類能出現的地方子類就能出現,這就要求我們模塊設計時儘量以基類進行對象的定義及應用。
里氏替換原則總結:
-
里氏替換可以提高代碼複用性,子類繼承父類時自然繼承到了父類的屬性和方法
-
提高代碼可拓展性,子類通過實現父類方法進行功能拓展,個性化定製
-
里氏替換中的繼承有侵入性。繼承,就必然擁有父類的屬性和方法
-
增加了代碼的耦合性。父類方法或屬性的變更,需要考慮子類所引發的變更
5.依賴倒置原則
依賴倒置原則的定義:程序要依賴於抽象接口,不要依賴於具體實現。簡單的說就是要求對抽象進行編程,不要對實現進行編程,這樣就降低了客戶與實現模塊間的耦合。
依賴倒置原則要求我們在程序代碼中傳遞參數時或在關聯關係中,儘量引用層次高的抽象層類,即使用接口和抽象類進行變量類型聲明、參數類型聲明、方法返回類型聲明,以及數據類型的轉換等,而不要用具體類來做這些事情。
依賴倒置原則,高層模塊不應該依賴低層模塊,都應該依賴抽象。抽象不應該依賴細節,細節應該依賴抽象。其核心思想是:要面向接口編程,不要面向實現編程。
舉個栗子,拿顧客商店購物來說,定義顧客類如下,包含一個 shopping 方法:
public class Customer {
public void shopping (YanTaShop shop) {
System.out.println(shop.sell());
}
}
以上表示顧客在 "雁塔店" 進行購物,假如再加入一個新的店鋪 "高新店",表示修改如下:
public class Customer {
public void shopping (GaoXinShop shop) {
System.out.println(shop.sell());
}
}
這顯然是設計不合理的,違背了開閉原則。同時,顧客類的設計和店鋪類綁定了,違背了依賴倒置原則。解決辦法很簡單,將 Shop 抽象爲具體接口,shopping 入參使用接口形式,顧客類面向接口編程,如下:
public class Customer {
public void shopping (Shop shop) {
System.out.println(shop.sell());
}
}
interface Shop{
String sell();
}
類圖關係如下:
依賴倒置原則總結:
-
高層模塊不應該依賴低層模塊,都應該依賴抽象(接口或抽象類)
-
接口或抽象類不應該依賴於實現類
-
實現類應該依賴於接口或抽象類
6、總結
本節介紹了設計模式的幾個原則,分別是開閉原則、單一職責原則、里氏替換原則、依賴倒置原則,重在理解即可,下節我們還會再介紹剩餘幾個原則。