一文搞懂什麼是面向對象編程思想與設計原則(深度)

篇幅較長,請耐心讀完,相信小先會讓你有所收穫。

導讀

  自上而下(抽象到具體)地描述什麼是面向對象編程的思想和設計原則。期間也會涉及到面向對象的三大特性。雖然篇幅較長,但是肯定能給學習面向對象編程的同學帶來一定的幫助。你可以從文章結構中一窺本文的思路,也可以跳到總結中看看本文的行文思路。如果用一張圖概括的話:
在這裏插入圖片描述

1.軟件設計的問題

  從問題出發,依次解決問題,這是本文的思路。

  1.1 軟件有什麼問題?

    軟件的價值,就是對利用計算機的計算能力,對現實世界進行模擬,以此產生比人工更大的效率與生產價值。比如:淘寶就像一個現實世界中有着無數商家的商圈,QQ就像一個能容納十億人的棋牌室。
    所以,當模擬的系統異常龐大時,如何保證軟件系統的正常運行,以及能夠良好的對軟件進行維護與拓展呢?
    軟件的問題就是軟件設計要解決的問題

  1.2 怎樣設計軟件?

    像修房子前先畫建築圖一樣去設計軟件——也就是建模
    所謂軟件建模,就是爲要開發的軟件建造模型。模型是對事物的抽象,比如建築圖紙、或者數學公式模型。
    只有建模,我們才能在紛繁複雜的業務和系統中理清思路。才能搞清軟件的本質規律基本特徵。才能在軟件開發伊始,就能知道軟件做出來後是否穩定是否可維護與拓展
    面向對象建模的標準就是——UML

2. 統一建模語言-UML

  搞軟件就像蓋房子,也需要設計設計。

  2.1 來歷

    20世紀,80年代至90年代。面向對象分析與設計方法發展迅速。相關研究十分活躍,誕生了很多方法與技術。隨着百家爭鳴中的優勝劣汰與兼容並和,UML成爲了面向對象分析與設計建模的標準。

  2.2 UML採用的方法論-4+1視圖

    軟件建模面臨兩個問題,一個我們要解決的領域問題,一個是我們最終的軟件系統。這兩個方面客觀存在的分析、抽象與設計,就是我們的軟件模型。
    那麼軟件有建模,UML有沒有什麼指導性的方法論呢?當然。
    UML的方法論(也叫軟件建模方法論)就是4+1視圖。
    4+1視圖將UML要關注的模型分爲了主要的幾個層次。如下圖:
在這裏插入圖片描述
    邏輯視圖:描述軟件的功能邏輯,由哪些模塊組成,模塊中包含那些類,其依賴關係如何。
    開發視圖:包括系統架構層面的層次劃分,包的管理,依賴的系統與第三方的程序包。開發視圖某些方面和邏輯視圖有一定重複性,不同視角看到的可能是同一個東西,開發視圖中一個程序包,可能正好對應邏輯視圖中的一個功能模塊。
    過程視圖:描述程序運行期的進程、線程、對象實例,以及與此相關的併發、同步、通信等問題。
    物理視圖:描述軟件如何安裝並部署到物理的服務上,以及不同的服務器之間如何關聯、通信。
    場景視圖:針對具體的用例場景,將上述 4 個視圖關聯起來,一方面從業務角度描述,功能流程如何完成,一方面從軟件角度描述,相關組成部分如何互相依賴、調用。
    總的來說,4+1 視圖模型很好地向我們展示瞭如何對一個軟件的不同方面用不同的模型圖進行建模與設計,以完整描述一個軟件的業務場景與技術實現。

  2.3 類圖

    UML是一種圖形化語言。UML 規範包含了十多種模型圖,常用的有 7 種:類圖、序列圖、組件圖、部署圖、用例圖、狀態圖和活動圖等。本文後文只需要類圖的知識即能理清,其他圖由於篇幅請自行拓展學習。
    類圖是最常見的 UML 圖形,用來描述類的特性和類之間的靜態關係。
    一個類包含三個部分:類的名字、類的屬性列表和類的方法列表。類之間有4 種靜態關係:關聯、泛化、依賴、實現。把相關的一組類及其關係用一張圖畫出來,就是類圖。
    關聯:類中引入另一個類當做成員變量。
    泛化:就是繼承。
    依賴:引入一個類作爲一個方法的參數,或者方法內部引入。
    實現:實現接口。
    類圖與各種圖對應了各自的4+1視圖。所以他們都是4+1視圖的具體描述。就像幾何三視圖一樣,分散時可能很抽象,但是合起來看就會勾勒出整體的輪廓。UML模型圖之類圖如下:

類間關係 對應類圖
關聯 在這裏插入圖片描述
泛化 在這裏插入圖片描述
依賴 在這裏插入圖片描述
實現 在這裏插入圖片描述

3. 說面向對象前先討論需求變更

  從實際出發總是發人深省的。

  3.1 一個例子

    假設,你需要開發一個程序,輸入a、b,計算a、b的值並打印到控制檯。任務看起來很簡單,幾行代碼就能搞定:

calculate(int a, int b){
	System.out.println(a + b);
}

    你將程序開發出來,測試沒有問題,很開心得發佈了,其他程序員在他們的項目中依賴你的代碼。過了幾個月,老闆忽然過來說,這個程序需要支持a、b的減法,於是你不得不修改代碼:

//type = 1加法,type = 2減法
calculate(int type, int a, int b){
	if(type == 1){
		System.out.println(a + b);
	} else {
		System.out.println(a - b);
	}
}

    爲了能支持減法,你在方法中增加了形參type,用type判斷增減。並且還好心的寫了註釋。但是始終有些人搞錯了type的含義,導致運行時出了bug。
    雖然沒有人責怪你,但是你還是很沮喪。然而這時候,老闆又來了,想讓你增加乘除法~
    你硬着頭皮改,但是代碼越來越臃腫,複雜,難以理解。導致的bug越來越多,你猶豫着要不要跑路~

  3.2 爲何好的程序員擁抱需求變化

    上述場景,在各種各樣的軟件開發場景中,隨處可見。比如針對上面這個例子,更加靈活,對需求更加有彈性的設計、編程方式可以是下面這樣的:

//操作的抽象接口
interface Operation(){
	operate(int a, int b);
}
//加法實現類
class Add implements Operation{
	@Override
	public void operate(int a, int b){
		System.out.println(a + b);
	}
}
//減法實現類
class Sub implements Operation{
	@Override
	public void operate(int a, int b){
		System.out.println(a - b);
	}
}
//type = 1加法,type = 2減法
calculate(int a, int b){
	//獲得具體實現類的項目包路徑,要什麼操作就在XML中配置操作實現類的項目包路徑。
	String classPath = XML.get();
	//反射實現動態加載類
	Operation op = Class.fromName(classPath);
	//傳入a、b,多態調用方法
	op.operate(a, b);
}

    以上代碼,如果需要乘除法,只需要定義乘除法的具體實現類,然後XML中配置項目包路徑即可。
    我們通過接口,將a、b執行的操作抽象出來。然後針對抽象(接口)編程,在利用語言爲我們實現的多態特性。就能達到在不修改代碼的前提下,實現功能的拓展
    3你能看到,應對需求變更最好的辦法就是一開始的設計就是針對需求變更的,並在開發過程中根據真實的需求變更不斷重構代碼保持代碼對需求變更的靈活性
    我們用類圖來對以上代碼建模:我們後續還會詳細講解此圖。
依賴倒置、面向接口編程
    所以,爲何好的程序員擁抱需求變化,因爲他們老早就爲需求變化設計瞭解決方案,沒有變化,心思豈不白費?
    如何不修改代碼實現需求變更就是面向對象編程的目的之一。

4. 面向對象設計目標

  聰明的人,做每件事情前,總是會設定一個初始的目標或目的。面向對象的軟件系統設計,也會有一個總體的目標。那麼目標有哪些呢?

  4.1 高內聚

    有關的聚集,無關的退散。一個類或方法儘量只關注一個功能,降低依賴性。提高可維護性和可複用性。

  4.2 低耦合

    低耦合是用來度量模塊與模塊直接的依賴關係。依賴越低,越容易複用,複雜性也越低。提高可拓展性和降低複雜性。

  4.3 如何高內聚、低耦合

    遵循面向對象的設計原則,請移步5.面向對象設計原則。

5. 面向對象7大設計原則

  面向對象7大設計原則是解決如何高內聚、低耦合地設計面向對象軟件系統的原則。

  5.1 開閉原則

    描述:對修改關閉,對拓展開放。
    意思:在不修改代碼的前提下,拓展功能。
    爲什麼:參照本文3. 說面向對象前先討論需求變更。
    原則意義:是設計原則的目標,總的指導原則。

  5.2 依賴倒置

    描述:高層模塊不依賴低層模塊,二者都應該依賴抽象。抽象不應該依賴具體實現,具體實現應該依賴抽象。
    意思
      首先,討論一個問題,什麼是依賴?依賴就是上面UML類圖中講的,一個類中引入了另一個類。這兩個類也就存在依賴關係。
      其次,什麼是高層,什麼是底層模塊?計算機中處處體現分層的思想,軟件開發也不例外。控制層依賴業務層,業務層依賴數據存儲層。抽象與具體實現意思差不多。
    爲什麼:接下來我們討論下這種依賴會存在什麼問題。以及依賴倒置的具體例子。從而知道爲什麼需要依賴的倒置。
      首先,依賴會導致維護困難。高層業務依賴實現底層具體實現細節時。當具體實現細節需要拓展改變時,極有可能會改變高層業務邏輯,因爲你是直接依賴。
      其次,依賴會導致複用困難。越是高層的模塊,越有複用的價值。因爲它是抽象的,是所有細節的抽象部分,所有細節都能複用。如果高層直接依賴於底層,那麼高層複用時可能面臨引入不必要的依賴,從而導致複用困難。
    具體例子:我們回到本文3.2中。借用當時建模的類圖。
依賴倒置、面向接口編程
      我們先描述下圖的意圖,然後再談依賴倒置。圖中service可以當做是高層,而Add、Sub或者新操作即是低層,也是具體實現類。關鍵在Operation類,它是一個抽象的高層接口。高層採用面向接口編程的方式,可以從XML配置文件中獲取配置的Add或Sub類的項目包路徑,然後通過反射機制加載,並賦給接口變量op。接下來,當運行時,多態就起作用了,這時的Operation運行時判定爲Operation的子類,調用的是子類的operation(a,b)方法。如果老闆讓你再加新的操作,那麼你可以不用修改代碼,新建一個類實現Operation接口,最後修改配置文件即可。有的小夥伴可能會問了,小先你不是說過,這不是開閉原則嗎?請接着往下看。
      圖中的依賴倒置:終於輪到依賴倒置了,我已經迫不及待了呢。回到依賴倒置的描述-高層模塊不依賴低層模塊,二者都應該依賴抽象——再看圖中,高層service不直接依賴Add,高層service和底層Add都依賴於抽象。之後是-抽象不應該依賴具體實現,具體實現應該依賴抽象——圖中抽象Operation接口不依賴Add,而是Add具體實現依賴了抽象Operation接口。用圖解釋了依賴倒置的描述,那依賴倒置中的倒置到底是什麼意思呢?請看下面依賴倒置的關鍵點。
      依賴倒置的關鍵點:依賴倒置有一個重要的關鍵點,就是抽象接口的所屬問題,也叫做接口所有權的倒置。日常開發中,我們都是如下圖:
傳遞依賴
      圖中的層層依賴,導致了我們所說的維護困難、複用困難。而依賴倒置的一個關鍵點在於。定義的抽象接口要屬於高層。讓低層去實現屬於高層的接口,然後高層再通過抽象接口調用低層。
      這樣高層就不需要直接依賴底層了,而是變成了底層依賴高層定義的接口,從而實現了依賴倒置(因爲接口是高層的,底層依賴了高層的接口)。這就是依賴倒置的最終解釋。我們用依賴倒置,實現了在不修改代碼的情況下拓展系統。
    原則意義:開閉原則是目標,依賴倒置就是實現目標的方法。

  5.3 里氏替換(和繼承)

    描述:所有使用基類的地方,都可以使用子類去替換。
    意思:用另一句話來,子類必須能夠替換掉他們的基類型。有小夥伴可能會懵逼,這不是顯而易見的嗎?下面我會講講我理解的兩層意思。
    爲什麼:爲什麼需要里氏替換原則?
      第一:就是顯而易見的意思,因爲需要多態,這也是面向對象的要求之一。
      第二:就和麪向對象三大特性,封裝、繼承、多態中的繼承有關了。三大特性本文雖然不會單獨拿出來講,但是會在文中一一闡述。說到繼承,可能會覺得,就是子類繼承父類,很簡單啊。但是實際上並非都是如此。繼承也會有出現誤用的時候。比較典型的問題是,正方形可以繼承長方形嗎?我們看下下面這個例子:
      長方形有長-width和高-height。還有一個計算面積的方法=width*height。

public class Rectangle {
	private double width;
	private double height;
	public void setWidth(double w) { width = w; }
	public void setHeight(double h) { height = h; }
	public double getWidth() { return width; }
	public double getHeight() { return height; }
	public double calculateArea() {return width * height;}
}

      如果正方形繼承長方形,正方形中的長寬的set方法需要重寫爲同時設置長寬->width= height = 值。

public class Square extends Rectangle {
	public void setWidth(double w) {
		width = height = w;
	}
	public void setHeight(double h) {
		height = width = w;
	}
}

      我們看下測試類,testArea中傳入一個Square正方形,期待的計算是長方形的長乘以寬,也就是3*4 = 12,但是實際卻是16。說明正方形繼承長方形是有誤的。違背了里氏替換原則。里氏替換原則也有一個較爲通俗的理解——子類不能比父類更嚴格。在這個例子中,正方形繼承了長方形,但是正方形有比長方形更嚴格的契約,即正方形要求長和寬是一樣的。因爲正方形有比長方形更嚴格的契約,那麼在使用長方形的地方,正方形因爲更嚴格的契約而無法替換長方形。

void testArea(Rectangle rect) {
	rect.setWidth(3);
	rect.setHeight(4);
	rect.calculateArea();//計算的面積爲12
}

    原則意義:實現多態,以及採用繼承時用此原則去審視是否正確。

  5.4 單一職責(和封裝)

    描述:一個類,應該只有一個引起它變化的原因。
    意思
      封裝一個類時,應該讓這個類的職責單一。即只有一個原因可以引起類的變化,那麼就說符合單一職責原則。當然,設計時,不可能完全符合單一職責原則。但是,我們要以此爲指導。再說說封裝。
      封裝:封裝就是指隱藏對象的屬性和實現細節,僅僅對外提供公共方法去訪問它。這是籠統的概念,我們從它的作用中理解爲什麼需要封裝。作用一是隱藏屬性和細節——不需要知道一個類是如何工作的,屬性是什麼,細節如何實現,只需要拿來使用,提高了複用性。作用二是隻有公共方法能夠訪問——你只能通過類的設計者暴露出的方法去訪問,然後修改類的狀態(通常指封裝的數據),讓外界使用與內部變化隔離,設計者也可以在方法裏設置訪問限制等,提高了安全性。日常開發中,封裝其實大家都在用,只是用的好與不好的區別。
      封裝和單一職責:單一職責原則是封裝的充分不必要條件。達到單一職責原則,必定用到了封裝。而封裝一個類,則不一定達到了單一職責原則的要求。
    爲什麼:爲什麼需要單一職責原則?
      第一降低耦合。當一個人職責太多,說明這個類耦合了太多的職責。違反了面向對象設計總的目標——低耦合。
      第二降低複雜度。職責太多,則代表代碼多,類太大。很容易違反開閉原則,且修改時容易導致錯誤。
    原則意義:提高內聚性、降低耦合度,提高可複用性。

  5.5 接口隔離

    描述:不應該強迫用戶依賴他們不需要的方法。
    意思:在一個類或接口中,有些方法暴露給調用者可能會被調用者胡亂調用,或難以理解。所以,如何對類的調用者隱藏類的某些方法就是接口隔離。這樣說可能有點迷糊,我們來看一個學生、老師的例子。
      場景:上課
      被調用者:學生
      調用者:老師
      學生功能:被點名,被表揚。
      如果我們是學生,我們在課上會被老師調用,那你一定希望老師調用不到被點名這個方法(假設不希望,你希望我也沒辦法了),而是調用被表揚方法(假設你希望)。那麼該如何實現呢?我們畫下實現的類圖:
接口隔離原則
      可以看到,在類圖中,被表揚、被點名分別由一個接口定義,然後讓學生實現了這兩個方法的接口。最重要的是,我們只將被表揚的接口給了老師,老師使用的是被表揚的接口編程,當我們傳一個被表揚的實現類學生時,因爲老師使用的是被表揚接口編程,他只能看到一個方法,那就是被表揚,肯定也就調用不到我們被點名的接口啦,是不是很完美,哈哈(對不起,我飄了)。所以接口隔離可以採用的方法,即是將一個實現類的不同方法包裝在不同的接口中對外暴露。
    爲什麼:爲什麼需要接口隔離原則?
      一來,用戶看到這些他們不需要,也不理解的方法,這樣無疑會增加他們使用的難度,如果錯誤地調用了這些方法,就會產生錯誤。也可表述爲,減少用戶的依賴複雜度(因爲只依賴必要的)。
      二來,當這些方法如果因爲某種原因需要更改的時候,雖然不需要但是依賴這些方法的用戶程序也必須做出更改,這是一種不必要的耦合。
    原則意義:降低依賴複雜度,降低耦合。

  5.6 合成複用

    描述:多用組合/聚合關聯關係,少用繼承。
    意思:面向對象設計中,有兩種基本方法可以複用已有的設計與實現——通過組合/聚合關聯或通過繼承。合成複用原則則提倡在複用時,要儘量使用組合/組合的關聯關係,少用繼承。
    爲什麼:爲什麼要使用合成複用原則?
      第一降低耦合。採用繼承時,一個類的變化,容易引起另外一個類的變化。而採用合成複用,則這種變化影響相對較少。
      第二繼承會破壞封裝性。說到封裝,再拓展下什麼叫“黑箱”複用和“白箱”複用。我們上面提到過封裝,隱藏細節,只暴露想暴露的。這就可以理解成是一種“黑箱”複用,因爲我確實隱藏了某些細節。但是繼承則是一種“白箱”複用。繼承讓所有的屬性和方法都暴露給了子類。子類可以任意調用修改。所以繼承是與封裝相反的複用,它破壞了封裝性。
    原則意義:降低耦合。

  5.7 迪米特法則

    描述:又稱最少知識原則,指一個軟件實體應當儘可能少地與其他實體發生相互作用。
    意思:限制了軟件實體之間通信的深度和寬度。這樣,當一個模塊改變時,就會少影響其他模塊。深度指可能不僅一個模塊的一個類。寬度指與多個模塊進行通信。
    爲什麼:爲什麼需要迪米特法則?
      第一解耦。這是狹義的迪米特法則,如果兩個類不必直接進行通信,那麼兩個類就不必直接相互作用,可以通過第三者轉發這個調用。比如:分佈式開發中的消息隊列。
      第二信息隱藏。這是廣義的迪米特法則,信息的隱藏也可以使各子系統之間脫耦。從而使各子系統能夠獨立開發、優化、使用。類比:如計算機網絡體系結構,各層之間信息隱藏,互不干涉。
    原則意義:降低耦合。

6. 設計模式

  6.1 面向對象編程的本質(和多態)

    面向對象的本質是什麼?
      答:多態
    多態是什麼?
      答:子類實現父類或者接口的抽象方法,程序使用抽象父類或者接口編程,運行期注入不同的子類,程序就表現出不同的形態,是爲多態。
    面向對象編程和此前的面向過程編程的核心區別是什麼?
      答:常說面向對象編程有三大特性,其中封裝在面向過程c語言中可以較容易實現,繼承也可以較容易實現(結構體中定義結構體),但是在c語言中,多態非常難以實現。所以,多態是面向對象和麪向過程語言的非常重要的一個區別。正是多態,使得面向對象編程和以往的編程方式有了巨大的不同。
    爲什麼多態是面向對象編程的本質
      最大的好處就是程序運行時的具體實現無關性,程序針對接口和抽象類編程,而不需要關心具體實現是什麼。我們在3.2中講到的加減法案例,如果輸入的數值a、b與操作類型耦合在一起。那麼當我們需要其他操作時,就不得不修改代碼,之後會越來越難以理解,複用困難。
      而通過使用接口,程序裏只需針對接口編程,而無需關心具體實現操作(通過XML配置項目包路徑,反射)。當需要更換操作時,修改配置文件,使得程序穩定,易於複用。這就像現實中的插座一樣,可以隨時更替,隨時插拔。
      第二個好處是可以將依賴關係倒置。我們在講依賴倒置時,正是通過面向接口編程,利用多態的特性去實現依賴的倒置的。
    正是這兩個好處,決定了多態就是面向對象編程的本質!
    我們如何用好多態呢?

  6.2 本質的具體體現

    但是就算知道了面向對象編程的多態特性,也很難利用好多態的特性,開發出強大的面向對象程序。到底如何利用好多態特性呢?人們通過不斷的編程實踐,總結了一系列的設計原則設計模式
    設計原則大部分都是和多態有關的,不過這些設計原則更多時候是具有指導性。編程的時候還需要依賴更具體的編程設計方法,這些方法就是設計模式
    模式是可重複的解決方案,人們在編程實踐中發現,有些問題是重複出現的,雖然場景各有不同,但是問題的本質是一樣的,而解決這些問題的方法也是可以重複使用的。人們把這些可以重複使用的編程方法稱爲設計模式。經典的設計模式一共有23種。而現今不論是框架還是一些庫,幾乎都大量使用了設計模式。大多數設計模式都是對多態的靈活運用,少部分專注於特殊的功能。所以,設計模式的精髓就是對多態的靈活應用

7. 框架中的設計模式

  7.1 什麼是框架

    框架是對某一類架構方案可複用的設計與實現。比如 Tomcat、Spring、Mybatis、Junit 等,這些框架會調用我們編寫的代碼,而我們編寫的代碼則會調用工具完成某些特定的功能,比如輸出日誌,進行正則表達式匹配等。
    框架應該滿足開閉原則,即面對不同應用場景,框架本身是不需要修改的。
    同時框架還應該滿足依賴倒置原則,即框架不應該依賴應用程序,因爲開發框架的時候,應用程序還沒有呢。應用程序也不應該依賴框架,這樣應用程序可以靈活更換框架。框架和應用程序應該都依賴抽象。

  7.2 Web容器中的設計模式

    前面我們提到 Tomcat 是一個框架,那麼是代碼如何被 Web 容器執行的?
    Web 容器主要使用了策略模式的設計模式,多個策略實現同一個策略接口。編程的時候 Tomcat 依賴策略接口,而在運行期根據不同上下文,Tomcat 裝載不同的策略實現。
    這裏的策略接口就是 Servlet 接口,而我們開發的代碼就是實現這個 Servlet 接口,處理HTTP 請求。其實我們前面的加減法案例就是一種簡單的策略模式,Tomcat策略模式類圖如下:
在這裏插入圖片描述
    Web 容器完成了 HTTP 請求處理的主要流程,指定了 Servlet 接口規範,實現了 Web 開發的主要架構,開發者只要在這個架構下開發具體 Servlet 就可以了。因此我們可以稱Tomcat、Jetty 這類 Web 容器爲框架。

8. 總結

    首先,我們從軟件的問題出發,根據軟件的問題提出軟件設計需要建模。
    其次,又說到了統一建模語言-UML。
    然後,我們從一個實際例子出發,描述了程序員一開始就應該考慮到程序的可拓展、可維護、可複用等問題。
    之後,這些問題又可以統一爲面向對象的設計目標——高內聚、低耦合。
    ,面向對象的目標又可細化爲面向對象的7大設計原則。
    進而,提出了設計原則的運用——設計模式
    最後,我們舉例了框架中設計模式的例子,瞭解到框架就是設計原則和例子的最具體實現方案。引用導讀中的圖:
在這裏插入圖片描述

    謝謝閱讀!

    參考資料
    博文:
    我曾想深入瞭解的:依賴倒置、控制反轉、依賴注入
    封裝的基本概念-java核心技術卷1
    教程:
    後端技術面試38講——李智慧
    書籍:
    設計模式實訓教程(第2版)-合成複用、迪米特

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章