爲什麼要學習設計模式
設計模式並不是什麼新的知識,它只是一種經驗的總結,所以必然是先有人這麼去做了,然後纔有人去總結提煉,從而變成了設計模式。
那麼既然設計模式是前人總結的經驗,我們何不站在巨人的肩膀上,去體會經驗帶來的好處呢?
所以我們在學習設計模式的過程中,最重要的是掌握其中的設計思想,而設計模式最重要的思想就是解耦。我們需要將其解耦思想爲自己所用,從而提升自己編碼能力,使自己的代碼更加容易維護、擴展。
軟件設計七大原則
在軟件開發過程中,爲了提高系統的可維護性、可複用性、可擴展性以及靈活性,產生了七大設計原則,這些原則也會貫穿體現在設計模式中。設計模式會盡量遵循這些原則,但是也可能爲了某一個側重點從而犧牲某些原則,在我們日常開發中也只能說盡量遵守,但是並不必爲了遵守而遵守。
開閉原則
開閉原則:Open-Closed Principle
,簡稱爲 OCP
。其核心是指在一個軟件實體中(如類,函數等),我們應該對擴展開放、對修改關閉,這樣就可以提高軟件系統的可複用性和可維護性。
開閉原則是面向對象設計的最基本原則,而遵守開閉原則的核心思想就是面向抽象編程。
下面我們以超市中的商品爲例進行說明,請大家跟着我一起完成這個實驗。
因爲我們有七大原則需要講解,有些原則之間類會同名,爲了方便區分,我們以每一個原則的簡稱來新建一個目錄,比如開閉原則新建的目錄名爲 ocp
,然後相關的類就創建在 ocp
目錄下。
- 新建一個商品接口
IGoods.java
,接口中定義了兩個方法:一個獲取商品名,一個獲取商品出售價。
package ocp;
import java.math.BigDecimal;
public interface IGoods {
String getName();//獲取商品名稱
BigDecimal getSalePrice();//獲取商品每kg出售價格
- 新建一個具體商品蔬菜類
Cabbage.java
來實現商品接口。
package ocp;
import java.math.BigDecimal;
public class Cabbage implements IGoods {
@Override
public String getName() {//獲取商品名稱
return "蔬菜";
}
@Override
public BigDecimal getSalePrice() {//獲取商品每kg出售價格
return new BigDecimal("3.98");
}
上面我們看到,蔬菜售價是 3.98/kg,那麼這時候到了晚上,需要打折,售價要改爲 1.98/kg,這時候普通的做法有三種選擇:
- 直接修改
Cabbage
類的getSalePrice
。 - 接口中再新增一個打折價方法。
- 直接在
Cabbage
方法中新增一個獲取打折後價錢的方法。
這三種方法中:
第一種可能影響到其它不需要打折的地方或者後面不打折了又要改回來,那麼就需要反覆修改源碼,不可行。
第二種直接修改接口,影響就太大了,每個實現類都被迫需要改動(當然如果是 JDK 1.8 之後的版本可以選擇新增
default
方法,這樣方法二就和第三種方法等價了)。第三種方法貌似改動是最小的,但畢竟還是修改了源碼。
簡而言之,這三種方法都需要修改源碼,違背了開閉原則中的對修改關閉這一條。所以如果要遵循開閉原則,那麼我們的做法應該是再新建一個蔬菜打折類來實現 IGoods
商品。
- 新建一個打折蔬菜類
DiscountCabbage.java
。
package ocp;
import java.math.BigDecimal;
public class DiscountCabbage implements IGoods {
@Override
public String getName() {//獲取商品名稱
return "蔬菜";
}
@Override
public BigDecimal getSalePrice() {//獲取商品每kg出售價格
return new BigDecimal("1.98");
}
}
- 最後讓我們新建一個測試類
TestOCP.java
來看看運行結果。
package ocp;
public class TestOCP {
public static void main(String[] args) {
System.out.println("蔬菜原價:" + new Cabbage().getSalePrice());//獲取蔬菜原價
System.out.println("蔬菜打折價:" + new DiscountCabbage().getSalePrice());//獲取蔬菜打折價
}
}
接下來需要執行 javac ocp/*.java
命令進行編譯。
最後再執行 java ocp.TestOCP
命令運行測試類(大家一定要自己動手運行哦,只有自己實際去運行了才能更深入體會其中的思想)。
這樣就符合開閉原則了,而且後面有其它商品需要打折或者其它活動,都可以通過新建一個類來實現,擴展非常方便。
里氏替換原則
里氏替換原則:Liskov Substitution Principle
,簡稱爲 LSP
。其指的是繼承必須確保超類所擁有的性質在子類中仍然成立,也就是說如果對每一個類型爲 T1 的對象 o1 都有類型爲 T2 的對象 o2,使得以 T1 所定義的程序 P 在所有的對象 o1 都替換成爲 o2 時,程序 P 的行爲沒有發生改變。
里氏替換原則具體可以總結爲以下 4 點:
- 子類可以實現父類的抽象方法,但是不能覆蓋父類的非抽象方法。
- 子類中可以增加自己的特有方法。
- 當子類方法重載父類的方法時,方法的前置條件(即方法的輸入/入參)要比父類方法輸入的參數更寬鬆。
- 當子類實現父類的方法(重載/重寫/實現抽象方法),方法的後置條件(即方法的輸出/返回值)要比父類更嚴格或者相等。
我們以動物鳥類飛翔舉例進行說明。同樣的,這裏需要新建一個 lsp
目錄,相關類創建在 lsp
目錄下。
- 新建一個鳥類
Bird.java
。
package lsp;
public class Bird {
public void fly() {
System.out.println("我正在天上飛");
}
}
- 再新建一個鷹類
Eagle.java
來繼承 Bird,並重寫其中的 fly 方法。
package lsp;
public class Eagle extends Bird {
@Override
public void fly() {
System.out.println("我正在8000米高空飛翔");
}
}
- 新建一個測試類
TestLSP.java
來測試。
package lsp;
public class TestLSP {
public static void main(String[] args) {
Bird bird = new Bird();
bird.fly();//輸出:我正在天上飛
Eagle eagle = new Eagle(); //替換成子類Eagle,子類重寫了父類Bird的fly方法
eagle.fly();//輸出:我正在8000米高空飛翔
}
}
接下來我們需要先執行 javac lsp/*.java
命令進行編譯。然後再執行 java lsp.TestLSP
命令運行測試類(大家一定要自己動手運行哦,只有自己實際去運行了纔會更能體會其中的思想)。
可以看到上面的例子中將父類替換成子類之後,fly
方法變成了在 8000 米高空飛翔(普通鳥類達不到),這就改變了父類的行爲,導致替換不成立,所以這個例子就違背了里氏替換原則。
依賴倒置原則
依賴倒置原則:Dependence Inversion Principle
,簡稱爲 DIP
。其指的是在設計代碼結構時,高層模塊不應該依賴低層模塊,而是都應該依賴其抽象。抽象不應該依賴細節,細節應該依賴抽象。通過依賴倒置原則可以減少類與類之間的耦合性,提高系統的穩定性,提高代碼的可讀性和可維護性,而且能夠降低修改程序所帶來的風險。
我們以超市出售商品舉例進行說明(同樣的,這裏我們需要新建一個 dip
目錄,相關類創建在 dip
目錄下)。
- 假設一家超市剛開張,只有青菜賣,所以我們新建一個超市類
SuperMarket.java
,裏面只定義一個賣蔬菜的方法。
package dip;
public class SuperMarket {
public void saleCabbage(){
System.out.println("我有蔬菜可以賣");
}
}
這個超市類直接和蔬菜綁定了,也就是依賴了具體的商品。假如要賣其它商品就需要修改源碼,違背了開閉原則,我們應該修改超市類依賴於抽象商品,而不能直接綁定具體商品。
- 新增一個商品接口
IGoods.java
,接口中定義一個出售商品方法。
package dip;
public interface IGoods {
void sale();
}
- 再新建一個蔬菜類
Cabbage.java
實現商品接口。
package dip;
public class Cabbage implements IGoods{
@Override
public void sale() {
System.out.println("我有蔬菜可以賣");
}
}
- 然後還需要編輯
SuperMarket.java
文件,將原先的超市類 SuperMarket 修改一下。
package dip;
public class SuperMarket {
public void sale(IGoods goods){
goods.sale();
}
}
- 最後讓我們新建一個測試類
TestDIP.java
來測試結果。
package dip;
public class TestDIP {
public static void main(String[] args) {
SuperMarket superMarket = new SuperMarket();
superMarket.sale(new Cabbage());//輸出:我有蔬菜可以賣
}
}
接下來我們需要先執行 javac dip/*.java
命令進行編譯。然後再執行 java dip.TestDIP
命令運行測試類(大家一定要自己動手運行哦,只有自己實際去運行了纔會更能體會其中的思想)。
這時候如果需要新增其它商品,只需要新建一個具體商品類來實現 IGoods
接口,並作爲參數傳入 sale
方法就可以了。
單一職責原則
單一職責原則:Single Responsibility Principle
,簡稱爲 SRP
。其指的是不要存在多於一個導致類變更的原因。假如我們有一個類裏面有兩個職責,一旦其中一個職責發生需求變更,那我們修改其中一個職責就有可能導致另一個職責出現問題,在這種情況應該把兩個職責放在兩個 Class 對象之中。
單一職責可以降低類的複雜度,提高類的可讀性和系統的可維護性,也降低了變更職責引發的風險。
我們以超市的進貨和出售舉例進行說明(同樣的,這裏我們需要新建一個 srp
目錄,相關類創建在 srp
目錄下)。
- 新建一個商品類
Goods.java
。
package srp;
public class Goods {
public void action(String type){
if ("進貨".equals(type)){
System.out.println("我要去進貨了");
}else if("售賣".equals(type)){
System.out.println("我要賣商品");
}
}
}
這個方法裏面有兩個分支:進貨和售賣。也就是一個方法裏面有兩個功能(職責),假如業務邏輯非常複雜,那麼一個功能發生變化需要修改有很大的風險導致另一個功能也發生異常。所以爲了符合單一職責原則我們應該進行如下改寫,將這兩個職責拆分成兩個類。
- 商品進貨類
BuyGoods.java
。
package srp;
public class BuyGoods {
public void action(){
System.out.println("我要去進貨了");
}
}
- 商品售賣類
SaleGoods.java
。
package srp;
public class SaleGoods {
public void action(){
System.out.println("我要賣商品");
}
}
- 最後我們寫一個測試類
TestSRP.java
來對比一下兩種寫法。
package srp;
public class TestSRP {
public static void main(String[] args) {
//不符合單一職責寫法
Goods goods = new Goods();
goods.action("進貨");//輸出:我要去進貨了
goods.action("售賣");//輸出:我要賣商品
//符合單一職責寫法
BuyGoods buyGoods = new BuyGoods();
buyGoods.action();//輸出:我要去進貨了
SaleGoods saleGoods = new SaleGoods();
saleGoods.action();//輸出:我要賣商品
}
}
接下來我們需要先執行 javac srp/*.java
命令進行編譯。然後再執行 java srp.TestSRP
命令運行測試類(大家一定要自己動手運行哦,只有自己實際去運行了纔會更能體會其中的思想)。
這樣對比之後大家應該一目瞭然,單一職責原則的兩個行爲是兩個獨立的類,修改其中一個功能,就絕對不會導致另一個功能出現異常,符合了單一職責原則。
接口隔離原則
接口隔離原則:Interface Segregation Principle
,簡稱爲 ISP
。接口隔離原則符合我們所說的高內聚低耦合的設計思想,從而使得類具有很好的可讀性、可擴展性和可維護性,在設計接口的時候應該注意以下三點:
- 一個類對其它類的依賴應建立在最小的接口之上。
- 建立單一的接口,不建立龐大臃腫的接口。
- 儘量細化接口,接口中的方法應適度。
我們以常見的動物的行爲舉例進行說明(同樣的,這裏我們需要新建一個 isp
目錄,相關類創建在 isp
目錄下)。
- 新建一個動物接口
IAnimal.java
,定義三個方法:run,swim,fly。
package isp;
public interface IAnimal {
void run();//地上跑
void swim();//水裏遊
void fly();//天上飛
}
- 新建一個 Dog 類
Dog.java
來實現 IAnimal 接口。
package isp;
public class Dog implements IAnimal {
@Override
public void run() {
System.out.println("我跑的很快");
}
@Override
public void swim() {
System.out.println("我還會游泳");
}
@Override
public void fly() {
}
}
可以看到,fly 方法我什麼也沒做,因爲狗不會飛,但是因爲 fly 方法和其它方法定義在了同一個接口裏面,所以使得狗具備了不該具有的行爲,這就屬於接口沒有隔離,我們應該把不同特徵的行爲進行隔離,即拆分成不同的接口。
- 新建一個接口
IFlyAnimal.java
,只定義一個 fly 方法。
package isp;
public interface IFlyAnimal {
void fly();
}
- 新建一個接口
IRunAnimal.java
,只定義一個 run 方法。
package isp;
public interface IRunAnimal {
void run();
}
- 新建一個接口
ISwimAnimal.java
,只定義一個 swim 方法。
package isp;
public interface ISwimAnimal {
void swim();
}
- 然後對上面的 Dog 類
Dog.java
進行改寫。
package isp;
public class Dog implements IRunAnimal,ISwimAnimal {
@Override
public void run() {
System.out.println("我跑的很快");
}
@Override
public void swim() {
System.out.println("我還會游泳");
}
}
- 最後我們新建一個測試類
TestISP.java
來看一下運行效果。
package isp;
public class TestISP {
public static void main(String[] args) {
Dog dog = new Dog();
dog.run();//狗會跑
dog.swim();//狗會游泳
}
}
接下來我們需要先執行 javac isp/*.java
命令進行編譯。然後再執行 java isp.TestISP
命令運行測試類(大家一定要自己動手運行哦,只有自己實際去運行了纔會更能體會其中的思想)。
這時候 Dog 需要什麼行爲就實現什麼行爲,不會再具有自己不該有的行爲 fly 了。
迪米特法則(最少知道原則)
迪米特法則:Law of Demeter
,簡稱爲 LoD
,又叫作最少知道原則(Least Knowledge Principle
,LKP
)。是指一個對象對其它對象應該保持最少的瞭解,儘量降低類與類之間的耦合。
我們以超市售賣青菜,老闆和經理想知道賣出去了多少斤舉例進行說明(同樣的,這裏我們需要新建一個 lod
目錄,相關類創建在 lod
目錄下)。
- 新建一個蔬菜商品類
Cabbage.java
。
package lod;
public class Cabbage {
public void getName(){//獲取商品名
System.out.println("蔬菜");
}
public void saleRecord(){//獲取蔬菜售賣記錄
System.out.println("蔬菜今天賣出去了100斤");
}
}
這時候經理和老闆都需要知道商品出售情況,那麼是不是經理和老闆都需要直接和商品打交道呢?實際上不需要,按常理老闆只需要向經理詢問就可以了,而經理直接和商品打交道就行了。
- 新建一個經理類
Manager.java
。
package lod;
public class Manager {
private Cabbage cabbage;
public Manager(Cabbage cabbage) {
this.cabbage = cabbage;
}
public void getCabbageSaleMoney(){
cabbage.saleRecord();
}
}
可以看到經理類裏面集成了具體的商品,然後調用了商品的方法獲得商品的出售記錄。
- 新建老闆類
Boss.java
。
package lod;
public class Boss {
public void getCabbageSaleRecord(Manager manager){
manager.getCabbageSaleMoney();
}
}
- 新建一個測試類
TestLoD.java
來看看運行結果。
package lod;
public class TestLoD {
public static void main(String[] args) {
Boss boss = new Boss();//構建Boss實例
Manager manager = new Manager(new Cabbage());//構建經理實例,經理需要和商品打交道
//這裏老闆只需要和經理打交道就行了
boss.getCabbageSaleRecord(manager);//獲得蔬菜售賣機記錄
}
}
接下來我們需要先執行 javac lod/*.java
命令進行編譯。然後再執行 java lod.TestLoD
命令運行測試類(大家一定要自己動手運行哦,只有自己實際去運行了纔會更能體會其中的思想)。
上面 Boss 類不會直接和商品打交道,而是通過經理去獲取想要的接口,這就是迪米特法則。不該知道的不要知道,我只要讓該知道的人知道就好了,你想知道那你就去找那個該知道的人。後面我們要介紹的中介者模式就是一種典型的遵守了迪米特法則的設計模式。
合成複用原則
合成複用原則:Composite Reuse Principle
,簡稱爲 CRP
,又叫組合/聚合複用原則(Composition/Aggregate Reuse Principle
,CARP
)。指的是在軟件複用時,要儘量先使用組合(has-a)或者聚合(contains-a)等關聯關係來實現,這樣可以使系統更加靈活,降低類與類之間的耦合度,一個類的變化對其它類造成的影響相對較少。
繼承通常也稱之爲白箱複用,相當於把所有的實現細節都暴露給子類。組合/聚合也稱之爲黑箱複用,對類以外的對象是無法獲取到實現細節的。
這個原則還是非常好理解的,像我們開發中經常用的依賴注入,其實就是組合,還有上面的迪米特法則中的經理類就是組合了商品類,所以在這裏就不再單獨舉例子了。
寫在最後
設計模式是一種思想,而軟件設計七大原則就是設計思想的基石,設計模式之中可以處處看到這些設計原則,所以想要學好設計模式,那麼這軟件設計的七大原則還是需要好好體會並理解,只有這樣,後面學習設計模式纔會知其然更知其所以然。