設計模式(一)——物件導向六大原則

liangxingwei發表於2017-12-14

本文原創掘金:L_Sivan

記在前面:這個《設計模式》系列的文章,想了很久才決定寫的,一是還是本人的原則,只有通過自己表達出來的東西,才是真正屬於你的東西,所以即使寫的不好,有什麼理解不到位的,被人指出來也挺好的,證明屬於我的東西還是有缺漏嘛。二是設計模式這個東西有點虛,特別這篇原則,總覺得還欠缺很多理解。三是看了好幾篇設計模式,下面的討論基本分兩種,要麼就是一堆Mark,要麼就是一堆FXXk,也是有點擔心會被罵吧,不過錯了被指正也是很正常。不說廢話了,上正文。

一. 單一職責原則

  • 定義:不要存在多於一個導致類變更的原因。簡單說,就是一個類只負責一項職責。
  • 為什麼要這個原則:試想,一個類T,用來實現兩個職責t1和t2,在需求變更的情況下需要修改t1,可能導致原本正常工作的t2職責出錯。

但是這個原則,其實看到的話,很多人應該都會覺得沒啥好看的吧,因為太簡單了,而且在寫程式碼的時候,應該都不希望因為修改了一個功能導致其他的功能發生故障。 但是翻了好幾篇部落格,還有看到的書的作者也是說到,其實並沒有很多類設計能完全遵守到單一職責原則,因為這個原則受太多因素制約了,比如下面這個例子:

我們要造車,然後要車跑起來

public class Car {
	public void run(String carType){
		System.out.println("啟動"+carType+"引擎跑起來了");
	}	
}
public class Client {
	public static void main(String[] args) {
		Car c = new Car();
		c.run("BMW");
		c.run("Benz");
	}
}
結果:
  啟動BMW引擎跑起來了
  啟動Benz引擎跑起來了
複製程式碼

後面發現還有類似上世紀的小包車這種需要拉才能跑起來的車,我們怎麼辦呢? 首先按照單一職責原則,我們來另起一個類,來實現這個功能

public class Trolley {
	public void run(String carType){
		System.out.println("拉著"+carType+"跑起來了");
	}
}
public class Client {
	public static void main(String[] args) {
		Car c = new Car();
		c.run("BMW");
		c.run("Benz");
		Trolley trolley = new Trolley();
		trolley.run("上世紀的小包車");
	}
}
結果:
啟動BMW引擎跑起來了
啟動Benz引擎跑起來了
拉著上世紀的小包車跑起來了
複製程式碼

但是這裡就有一個問題了,如果後期還有斗車這種要推的車,還有自行車這種要騎的車,還有幾十種其他的車,那我們難道要一個類一個類的寫嗎?這樣會造成類膨脹的。而且修改了類,還得大幅度修改客戶端 所以在這裡,再引入這個原則的同時,就得違背下這個原則了,如下:

public class Car {
	public void run(String carType){
		System.out.println("啟動"+carType+"引擎跑起來了");
	}
	public void run2(String carType){
		System.out.println("拉著"+carType+"跑起來了");
	}
}
public class Client {
	public static void main(String[] args) {
		Car c = new Car();
		c.run("BMW");
		c.run("Benz");
		c.run2("上世紀的小包車");
	}
}
結果:
啟動BMW引擎跑起來了
啟動Benz引擎跑起來了
拉著上世紀的小包車跑起來了
複製程式碼

這樣做的一個好處就是方便,直接在類裡面新新增一個方法,在客戶端只需要做小小的改動就可以用。這只是其中一個辦法,當然,還有很多種辦法,比如在run方法中根據carType判斷來選擇方式等等,但是這都違背了單一職責原則。所以這個原則怎麼用呢?個人理解,很多時候我們在實際的編碼過程中並不能完全遵守這個原則,我們只能儘量做到這個原則。不然的話就是死守教規了。

二. 里氏替換原則

  • 定義:所有引用父類的地方必須能透明地使用其子類的物件。

  • 為什麼要用這個原則:試想一下,在一個類P1中,實現了方法m1,然後現在需要擴充套件m1方法,新的方法m2由P1的子類來完成,在完成m2的同時,有可能會將m1的方法破壞掉,使得m1不能正常工作。

上程式碼,還是用上文的例子:

public class Car {
	public void run(String carType){
		System.out.println("啟動"+carType+"引擎跑起來了");
	}
}
public class Client {
	public static void main(String[] args) {
		Car c = new Car();
		c.run("BMW");
	}
}
結果:
啟動BMW引擎跑起來了
複製程式碼

現在需要加一個功能,就是對行車的速度進行彙報,怎麼做?首先想到的是不是直接修改Car的run方法,但是要注意,這個不是一個好的方法,修改了這個run方法,可能會導致本來在使用這個方法的其他物件產生錯誤的結果,所以不能這樣,我們使用繼承,然後重寫run方法,如下:

public class AdvancedCar extends Car{
	@Override
	public void run(String carType) {
		super.run(carType);
		System.out.println("車的速度是80KM/h");
	}
}
public class Client {
	public static void main(String[] args) {
		AdvancedCar car = new AdvancedCar();
		car.run("BMW");
	}
}
結果:
啟動BMW引擎跑起來了
車的速度是80KM/h
複製程式碼

然後這裡的確實現了功能,而且程式碼還很整潔,只是一個繼承然後加多一句就好了,但是問題就來了,假設再來需求,上班高峰期的時候塞車,不需要彙報速度,下班加班趕回家才需求彙報速度(保平安嘛),那咋辦?在這個AdvancedCar中,run方法一經呼叫,就會彙報速度的喔。用里氏替換原則來看,Car出現的地方,AdvancedCar就不能用了,我們來用里氏替換原則修改下程式碼:

public class AdvancedCar extends Car{
	public void showSpeed(){
		System.out.println("車的速度是80KM/h");
	}
}
public class Client {
	public static void main(String[] args) {
		AdvancedCar car = new AdvancedCar();
		car.run("BMW");
		car.showSpeed();
	}
}
結果:
啟動BMW引擎跑起來了
車的速度是80KM/h
複製程式碼

這樣修改之後,Car出現的地方,AdvancedCar也能直接使用,因為run方法的原有功能並沒有被破壞,而且要滿足上一段需求的話,只需要直接在Client這裡加判斷就好。 所以我自己的粗略理解就是:子類可以擴充套件父類的方法,但不應該複寫父類的方法。

三. 依賴倒置原則

  • 定義:高層模組不應該依賴低層模組,二者都應該依賴其抽象;抽象不應該依賴細節;細節應該依賴抽象。

  • 為什麼要用這個原則:假設一個類P組合了一個類A,然後用A實現了相關的功能,然後現在要將A實現的功能改成B類來實現,這裡的修改方法是隻能去修改P的程式碼,而這種直接修改程式碼是會帶來不必要的風險的。

上例子,還是用車的例子:

public class Car {
    // 給汽車加油
	public void refuel(Gasoline90 gasoline){
		System.out.println("加了型號為"+gasoline.getClass().getSimpleName()+"的汽油");
	}
}
public class Gasoline90 { }
public class Client {
	public static void main(String[] args) {
		Car car = new Car();
		car.refuel(new Gasoline90());
	}
}
結果:
加了型號為Gasoline90的汽油
複製程式碼

現在問題來了,加油站沒有90汽油了,只有93和97,而汽車沒油了,難道但是加油refuel(Gasoline90 gasoline)只能加90汽油,咋辦,難不成直接修改car的程式碼,給它多加兩個方法,分別可以加93和97汽油?很明顯不科學嘛,一點程式碼複用都沒有,更別談設計模式了,所以在這裡,就要改了。

// 定義一個介面Gasoline
public interface Gasoline { }

//在Car類的refuel方法中傳入Gasoline引數
public class Car {
	public void refuel(Gasoline gasoline){
	System.out.println("加了型號為"+gasoline.getClass().getSimpleName()+"的汽油");
	}
}

// 寫90和97兩個汽油的類
public class Gasoline90 implements Gasoline{ }
public class Gasoline97 implements Gasoline{ }

// 場景
public class Client {
	public static void main(String[] args) {
	Car car = new Car();
 //car.refuel(new Gasoline90());
	System.out.println("----汽車站沒有90汽油了-----");
	car.refuel(new Gasoline97());
	}
}

結果:
----汽車站沒有90汽油了-----
加了型號為Gasoline97的汽油
複製程式碼

在上面的例子,傳參的時候傳入了一個Gasoline型別,只要汽車要加的汽油,全都實現這個藉口,就可以讓汽車自由加油,管它什麼93,97,1997都好,而且還不用修改Car的程式碼,只需要實現Gasoline介面就好。

四. 介面隔離原則

咋看一下,我以為像現實生活中那樣,隔離病原體是把病原體隔離開來,那介面隔離原則難道是隔離介面?程式設計界的名詞確實不能一般對待。

  • 定義:客戶端不應該依賴它不需要的介面;一個類對另一個類的依賴應該建立在最小的介面上
  • 為什麼需要這個原則:假想我們設計了一個介面I,裡面有五個方法,分別是m1,m2,m3,m4,m5,而有兩個類A和B,分別需要用m1m2m5,m3,m4,m5方法。那麼無論是那一個類,在實現介面I的時候,都要將其本身不需要的類進行實現,很明顯,這不是一個好設計。 上程式碼,還是用車的例子:
// 介面
public interface ICar {
	public void run(String carType);
	public void showSpeed();
	public void playMusic(String songName);
}

// 實現類
public class Car implements ICar{
    public void run(String carType){
     System.out.println("啟動"+carType+"引擎跑起來了");
	}
	public void showSpeed() {
     System.out.println("汽車的速度為80KM/h");
	}
	public void playMusic(String songName) {
	System.out.println("放起了動聽的"+songName);
	}
}

// 場景
public class Client {
	public static void main(String[] args) {
	Car car = new Car();
	car.run("BMW");
	car.showSpeed();
	car.playMusic("成都");
	}
}
複製程式碼

這裡問題就來了,並不是所有的車都有放音樂的功能,也並不是所有的車都有展示速度的功能,但是隻有上面程式碼這個車的話,我們在新建其他車物件的時候,卻帶上了全部的功能,顯然,這是不科學的。改進下,將上面的介面拆分成兩個介面專業的車IProfessionalCar和娛樂功能的車IEntertainingCar

// 介面
public interface IProfessionalCar {
	public void run(String carType);
	public void showSpeed();
}
public interface IEntertainingCar {
	public void run(String carType);
	public void playMusic(String songName);
}

// 實現類
public class ProfessionalCar implements IProfessionalCar {
	public void run(String carType){
	System.out.println("啟動"+carType+"引擎跑起來了");
	}
	public void showSpeed() {
	System.out.println("汽車的速度為80KM/h");
	}
}
public class EntainingCar implements IEntertainingCar {
	public void run(String carType){
	System.out.println("啟動"+carType+"引擎跑起來了");
	}
	public void playMusic(String songName) {
	System.out.println("放起了動聽的"+songName);
	}
}

// 場景
public class Client {
	public static void main(String[] args) {
	IProfessionalCar professionalCar = new ProfessionalCar();
	professionalCar.run("F1方程式");
	professionalCar.showSpeed();
	EntainingCar entainingCar = new EntainingCar();
	entainingCar.run("壞了速度儀表盤的SUV");
	entainingCar.playMusic("成都");
	}
}
結果:
啟動F1方程式引擎跑起來了
汽車的速度為80KM/h
啟動壞了速度儀表盤的SUV引擎跑起來了
放起了動聽的成都
複製程式碼

介面隔離原則的要求我們,建立單一介面,不要建立龐大臃腫的介面,儘量細化介面,介面中的方法儘量少。這通過分散定義多個介面,可以預防外來變更的擴散,提高系統的靈活性和可維護性。

五. 迪米特法則

  • 定義:一個物件應該對其他物件保持最少的瞭解。
  • 為什麼需要這個原則:原因就是一個物件對另一個物件瞭解得越多,那麼,它們之間的耦合性也就越強,當修改其中一個物件時,對另一個物件造成的影響也就越大。

上例子,還是用車:

// 車
public class Car {
	private String carType;
	public Car(String carType){
		this.carType = carType;
	}
	public void run(){
		System.out.println("啟動"+carType+"引擎跑起來了");
	}
	public void refuel(Gasoline gasoline){
		System.out.println("加了型號為"+gasoline.getName()+"汽油");
	}
}
// 汽油
public class Gasoline {
	private String name;
	public Gasoline(String name) {
		this.name = name;
	}
	public String getName() {
		return name;
	}
    private boolean quality = true;
	public boolean getQuality(){
		return this.quality;
	}
}
// 人
public class Person {
	private Car car;
	public void setCar(Car car) {
		this.car = car;
	}
	public void drive(){
		car.run();
	}
	public void refuel(Gasoline gasoline){
        if(gasoline.getQuality()){
			System.out.println("油的質量過關,可以放心加");
			car.refuel(gasoline);
		}
}
// 場景類
public class Client {
	public static void main(String[] args) {
		Person jack = new Person();
		jack.setCar(new Car("Suv"));
		jack.drive();
		System.out.println("*********開了三百公里,沒油了********");
		jack.refuel(new Gasoline("90"));
	}
}
結果:
啟動Suv引擎跑起來了
*********開了三百公里,沒油了********
油的質量過關,可以放心加
加了型號為90汽油
複製程式碼

我們可以看到,一個很符合生活的場景,jack開車,然後開太久了,沒油了,於是加油,但是問題就來了,在生活中,加油這個動作應該是jack和加油站的工作人員進行交涉,然後由加油站的工作人員來完成,而在上面這裡,則是由jack自己完成。而且對油的質量的檢驗,我們普通人怎麼會,肯定不行啊,萬一90,93,97的檢驗方法各不相同,萬一以後的油質量越來越不好,檢驗步驟要變,難道要修改Person類的方法,不對啊。 我們來改下

// 增加類加油站工人
public class WorkerInPetrolStation {
	public void refuel(Car car, String gasolineName) {
	Gasoline gasoline = new Gasoline(gasolineName);
	if (gasoline.getQuality()) {
	System.out.println("油的質量過關,可以放心加");
	car.refuel(gasoline);
	}
	}
}
// 將Person的refuel方法修改成依賴工人
public class Person {
	private Car car;
	public void setCar(Car car) {
	this.car = car;
	}
	public void drive(){
	car.run();
	}
	public void refuel(WorkerInPetrolStation worker, String gasolineName){
	worker.refuel(this.car, gasolineName);
	}
}
// Car,Gasoline不變
// 場景類
public class Client {
	public static void main(String[] args) {
	Person jack = new Person();
	jack.setCar(new Car("Suv"));
	jack.drive();
	System.out.println("*********開了三百公里,沒油了********");
	jack.refuel(new WorkerInPetrolStation(),"90");
	}
}
結果:
啟動Suv引擎跑起來了
*********開了三百公里,沒油了********
油的質量過關,可以放心加
加了型號為90汽油
複製程式碼

現在無論以後油那邊怎麼變,都和我們Persion無關,交給加油站工人嘛,這是他們的飯碗。 迪米特法則的初衷是降低類之間的耦合,由於每個類都減少了不必要的依賴,因此的確可以降低耦合關係。但是凡事都有度,雖然可以避免與非直接的類通訊,但是要通訊,必然會通過一個“中介”來發生聯絡。過分的使用迪米特原則,會產生大量這樣的中介和傳遞類,導致系統複雜度變大。

六. 開閉原則

  • 定義:對修改關閉,對擴充套件開放
  • 為什麼使用這個原則:這不就是程式碼重用的一個終極目標嗎,不實現這個原則,改程式碼改到懷疑人生啊!而上面的五個原則,其實就是這個原則的體現。 但是這個原則,正是因為太高階了,所以太虛了,虛的沒什麼套路可尋,只能靠經驗,領悟來慢慢體會。

總結:

以上就是我對這六個原則的簡單理解,例子也不知道舉得恰不恰當,文字的描述也不知道是不是到位,但是這也就是我的理解了,等深入這行有一定時間,或許會對這篇東西覺得很傻,噗嗤一聲,完全不屑。但也是以後的事了。 歡迎前來責罵。。。

相關文章