設計原則
設計原則,是設計模式的內功心法,基本所有的設計模式都是基於設計原則進行的具體化,如果說設計模式是如何操作的話,那麼設計原則就是為何這麼做的基石,因此,只要我們能充分理解設計原則,那麼在此基礎上,對設計模式就能更好的理解,甚至能自己設計出一種設計模式來。
單一職責原則
定義
一個類或模組,只需要完成一個範圍的功能,而不要搞得大而全。
場景
例如我們設計一個社交網站,現在要儲存使用者資訊,類設計如下:
public class UserInfo {
private String name;
private String like;
private String location;
}
現在,我們想想該類的設計是否符合單一職責原則?
答案是可能符合,也可能不符合。那麼判斷依據是什麼呢?
原因就是類或模組職責的判斷是根據業務來定的,並沒有一個普遍認同的規則。例如,如果該需要要的網站還提供賣東西的功能,那麼使用者的地址資訊,就是一個十分關鍵的資訊,且該塊功能就需要抽離出來作為一個單獨的模組,此時,地址資訊放在這裡就不合適了,違反了單一職責原則。
但如果地址資訊,只是一個值物件,也就是說其只是一個展示屬性,那麼放在這裡就是合適的。
綜上所述,可以看到單一職責原則並不是設計出來就一成不變的,其需要結合業務發展的具體情況來判斷。因此我們在設計之初,可以考慮一個大而全的類,但隨著業務的發展需要,需要持續不斷的進行優化(也就是持續重構的思想)。
用處
單一職責,因為類的設計比較小而精,因此可以極大提高程式碼的可維護性和可讀性。
此外因為每個類或模組只涉及自己的功能部分,因此,也做到了高內聚。
其他
但類的設計也不是越單一越好,因為如果拆分的過細的話,可能上層一個類需要修改,會導致下層所有依賴其的類都要修改,又影響了程式碼的可維護性,因此還是要根據業務需要來合理評估,重點是感覺要對。
開閉原則
定義
字面意思,一個類的設計,應該要對擴充開放,對修改關閉。因此這裡的重點就是以下定義該如何判斷:
- 什麼樣的程式碼修改是擴充;
- 什麼樣的程式碼修改是修改;
- 修改程式碼就一定是違反了該原則嗎
場景
public static void main(String[] args) {
Demo demo = new Demo();
demo.consume(1);
}
// 根據傳遞過來的級別來進行不同的會員邏輯判斷
public void consume(int type) {
if (type == 1) {
Console.log("您好,1級會員!");
}
if (type == 2) {
Console.log("您好,2級會員!");
}
}
現在,又提出一個新的需求,還需要根據對應的會員等級進行對應的金額扣除,如果是上述的設計方式,那麼修改的方式則是下面這樣:
public void consume(int type, int price) {
if (type == 1) {
Console.log("您好,1級會員,扣除金額{}", price);
}
if (type == 2) {
Console.log("您好,2級會員,扣除金額{}", price);
}
}
很明顯,這樣的方式有問題,如果還要再傳遞一個欄位,例如優惠比例,那麼依照該方案,則還需要修改介面定義,這就意味著呼叫方都需要修改,測試用例也需要對應的修改。
那麼如果按照開閉原則的話,該如何設計呢?
首先我們對程式碼進行下重構
// 將所有相關屬性封裝起來
public class Vip {
private int type;
private int price;
private int radio;
}
針對每種處理方式,根據他們的公有行為抽象出一個抽象層:
public interface VipHandler {
void consume(Vip vip);
}
每種特殊處理方式實現對應的抽象:
public class FirstVipHandler implements VipHandler {
@Override
public void consume(Vip vip) {
if (vip.getType() == 1) {
Console.log("您好,1級會員,扣除金額{}", vip.getPrice() * vip.getRadio());
}
}
}
public class SecondVipHandler implements VipHandler {
@Override
public void consume(Vip vip) {
if (vip.getType() == 2) {
Console.log("您好,2級會員,扣除金額{}", vip.getPrice() * vip.getRadio());
}
}
}
通過這樣的處理方式,在每次接到新的任務後,就不需要重新修改原有的邏輯方法,可以直接進行擴充即可:
// 根據傳遞過來的級別來進行不同的會員邏輯判斷
public void consume(Vip vip, VipHandler vipHandler) {
vipHandler.consume(vip);
}
其他
可以看到即使是上述的方式來擴充程式碼,仍舊會修改原有程式碼,那麼這種方式是違反了開閉原則嗎?
在這裡,我們判斷其並符合了開閉原則,因為我們判斷是修改還是擴充,並不能只是簡單的根據看是否修改了原有程式碼,真正核心的關鍵問題應該是:
- 改動沒有破壞原有程式碼的正常執行;
- 改動沒有破壞原有單元測試
**
在上述的修改後,我們如果加一種特殊的情況,並沒有修改到原先的處理邏輯類,這也就意味著原先的程式碼不會引入一些可能的 bug,針對原始程式碼的測試用例也還是可以照常的進行編寫,而不用再根據新的改動而進行改動。
用途
開閉原則的關鍵點是程式碼的可擴充性,即如何快速的擁抱變化,當每次新的任務來後,不必修改原始程式碼,而直接在原有的基礎上進行擴充即可。
關閉修改是保持原有程式碼的穩定性。
里氏替換原則
定義
子類物件可以代替父類物件出現的任何地方,並保證原來程式邏輯行為不被破壞。
因為要保證子類物件不能破壞原有程式邏輯行為,因此該方式跟多型的區別是:如果子類進行了重寫,並在重寫的邏輯中加入了跟父類對應方法不同的邏輯,那麼該方式可以稱之為多型,但就不符合里氏替換原則了。
用途
該原則最重要的作用是指導子類的設計,保證在替換父類的時候,不改變原有程式的邏輯行為。
在這裡,重點是邏輯行為的不改變,這就意味著,我們可以對實現的細節進行修改,只要保證業務含義不變,那麼就是符合里氏替換原則的。
因此,針對這種情況,有一種用途是可以 改進 原有的實現,例如原先採用的排序演算法比較低效,那麼可以設計一個子類,然後重寫對應排序演算法,保證邏輯不發生變化。重點是,我們做的是改進,不管如何改,排序的業務含義是不變的。
實現方式,則是按照協議進行程式設計,關鍵是子類重寫過程中,要保證不破壞原有函式宣告的輸入、輸出、異常以及註釋中羅列的任何特殊情況宣告。
介面隔離原則
定義
首先,我們要對介面進行定義,明確其特殊含義,對於介面來說,我們將其分為兩種型別的表現形式。
- 一種語法定義,其代表了一組方法的集合;
- 向外暴露的單個API介面或函式實現
下面,我們分別對其進行介紹。
場景:一組API聚合
public interface UserInfoService {
boolean login();
void getUserInfoByPhone();
void deleteUserByPhone();
}
看上述這個介面定義是否符合介面隔離原則???
其實這跟單一職責原則一樣,也是要看業務發展的。例如,如果該介面是提供給後臺管理系統來使用的,那麼沒有問題,作為一個後臺系統的 admin 許可權人員當然可以有很多操作的能力。
但如果該介面是給第三方使用者來使用的話,就不是很合適了。因為刪除操作是一個高許可權能力,作為使用者來說,一般是沒有許可權做的,那麼在設計時,對應實現類就不應該實現全部介面內定義的方法,這就是介面隔離原則中所說的,不強制依賴介面中的所有方法。
**
而是,根據具體的定義,將其進行拆分,對應許可權的實現類實現對應的許可權行為。
場景:單個API介面或函式實現
public class Statistics {
private int max;
private int min;
private int sum;
//......
public Statistics count(List<Integer> data){
Statistics statistics = new Statistics();
// 計算 Statistics 中的每個值
return statistics;
}
}
首先,我們來看上述方法定義是否符合介面隔離原則???
在這裡,我們還是要結合具體的業務場景才能做出結論,如果使用該函式的呼叫者,在大部分場景下都需要用到其中的大部分欄位,那麼該設計就是可以的。
但是如果每次只用到其中的幾個,那麼該設計就不合理了,其會浪費大量的無效計算能力,影響效能。在該場景下,就需要進行拆分。
public int max(List<Integer> data) {
return data.stream().max(Statistics::compare).get();
}
public int min(List<Integer> data) {
return data.stream().min(Statistics::compare).get();
}
依賴翻轉原則
對於依賴翻轉原則來說,有很多看著很像的定義,我們分別對其進行介紹,看看其都是什麼含義,他們之間又有什麼關聯。
控制翻轉(IOC)
控制翻轉,針對是在原有的程式設計流程中,整個程式的執行流程是直接交由程式設計師來控制的,但是如果使用控制翻轉的思想,則是在一個架子中,已經定義好了執行的流程,而只是預先定義好了擴充點,後續程式設計師所能修改的只有擴充點,開發人員在擴充點裡新增相關業務邏輯即可。
public abstract class VipProcess {
public abstract boolean isVip();
public void consume() {
if (isVip()) {
Console.log("vip hello");
} else {
Console.log("get out");
}
}
}
public class Vip extends VipProcess {
@Override
public boolean isVip() {
return true;
}
public static void main(String[] args) {
VipProcess vip = new Vip();
vip.consume();
}
}
public class CommonPeople extends VipProcess {
@Override
public boolean isVip() {
return false;
}
public static void main(String[] args) {
VipProcess vip = new CommonPeople();
vip.consume();
}
}
上述方式,就是通過模板方法來實現的控制翻轉,提供一個擴充的 isVip() 邏輯來交給程式設計師來實現,而框架根據實際實現的方法返回來決定下面的程式流轉。
依賴注入(DI)
依賴注入更好理解,一句話概括:不用程式設計師顯式通過 new
來建立物件,而是通過建構函式,函式傳遞的方式來傳遞物件。
即A類如果需要依賴B類,不是通過在 A 中 new 一個 B 出來,而是在外面建立好 B 後,傳遞給 A。通過這樣的方式,可以在需求改變中,靈活的替換傳遞引數(B 實現 C 介面的話)。
而更進一步,現在一些框架都提供了 DI 的功能,只需要簡單的配置一下相關類物件,所需引數等,框架就會自動接管物件的建立流程已經生命週期等。(AutoWired)
依賴翻轉(DIP)
定義:高層模組不依賴於底層模組,而是通過一個抽象層來解耦。
該定義其實在我們的平常業務開發中,不怎麼會用到,因為我們平常就是高層依賴著底層,例如 Controller 依賴 Service,Service 依賴 Repository,該原則的重點還是指導框架層面的開發。
例如 Tomcat ,我們知道 Tomcat 的執行是我們將程式寫完後,打成 war 包扔到對應目錄就可以啟動了,而 Tomcat 和應用程式就是通過一個共同的抽象 **Servlet **來關聯的。
Tomcat 不直接依賴於底層實現:Web 程式,而是跟 Web 都依賴於 Servlet,而 Servlet 不依賴於具體的Tomcat 實現的 Web 的具體細節。
通過該實現方式,在編碼中,可以靈活的進行替換,比如我們還是一個 web 程式,但是執行的容器不使用 tomcat 了,也可以無縫的進行切換,只要保證要替換的容器,還是依賴於 Servlet 規範即可。