談談面向物件程式設計
何為面向物件程式設計
面向物件程式設計簡介
面向物件程式設計(Object-oriented Programming,縮寫:OOP)是軟體工程中一種具有 物件
概念的程式設計正規化(Programming Paradigm),同時也是一種程式開發的抽象方針,與之對應的程式設計正規化還有:函數語言程式設計(Functional Programming)、程序式程式設計(Procedural Programming)、響應式程式設計(Reactive Programming)等。
面向物件程式設計特點
一切皆為物件
在面向物件程式設計世界裡,一切皆為物件,物件是程式的基本單元,物件把程式與資料封裝起來提供對外訪問的能力,提高軟體的重用性,靈活性和擴充套件性。例如,Java中的 java.lang.Object
物件,可以表示Java中的一切物件(注意區分8種基本資料型別)。
在面向物件程式設計中,通常把物件的資料(欄位)稱為 屬性
,把物件的行為稱為 方法
。

image.png
物件與類
在面向物件程式設計中,最常見的表現就是基於 類(Class)
來表現的,每一個物件例項都有具體的類,即物件的型別。使用類的面向物件程式設計也稱為 基於類的程式設計(Class-based programming)
,如常見的Java,C++;而與之類似的有 基於原型的程式設計(Prototype-based programming)
,如JavaScript。
- 類:定義物件的資料格式(屬性型別)和可用過程(方法),同時也可能包含類成員的資料(如,常量)和過程(如,靜態方法),類其實就是物件的型別/原型(prototype)。
- 物件:類的例項,通過類例項化出來的具體例項。
如,Java中 Object obj = new Object();
,其中 Object
就是類,而 obj
就是具體物件例項。

image.png
面向物件三大特徵
面向物件的三大特徵分別是:封裝、繼承、多型,這三者是面向物件程式設計的基本要素
- 封裝(Encapsulation)
通過物件隱藏程式的具體實現細節,將資料與操作包裝在一起,物件與物件之間通過訊息傳遞機制實現互相通訊(方法呼叫),具體的表現就是通過提供訪問介面實現訊息的傳入傳出。
封裝常常會通過控制訪問許可權來控制物件之間的互訪許可權,常見的訪問許可權:公有( public
),私有( private
),保護( protected
)。某些語言可能還會提供更加具體的訪問控制,如,Java的 package
。
封裝的意義:由於封裝隱藏了具體的實現,如果實現的改變或升級對於使用方而言是無感知的,提高程式的可維護性;而且封裝鼓勵程式設計師把特定資料與對資料操作的功能打包在一起,有利於應用程式的 去耦
。
- 繼承(Inheritance)
支援類的語言基本都支援繼承,繼承即類之間可以繼承,通過繼承得到的類稱為子類,被繼承的類為父類,子類相對於父類更加具體化。
子類具有自己特有的屬性和方法,並且子類使用父類的方法也可以覆蓋(重寫)父類方法,在某些語言中還支援多繼承,但是也帶來了覆蓋的複雜性。
繼承的意義:繼承是程式碼複用的基礎機制
- 多型(Polymorphism)
多型發生在執行期間,即子型別多型,指的是子型別是一種多型的形式,不同型別的物件實體有統一介面,相同的訊息給予不同的物件會引發不同的動作。
多型的意義:提供了程式設計的靈活性,簡化了類層次結構外部的程式碼,使程式設計更加註重 關注點分離
(Separation of concerns,SoC)
其他特徵
- 抽象(Abstraction)
能夠把複雜問題通過抽象簡單化,可以為具體問題找到最恰當的類定義,並且可以在最恰當的繼承級別解釋問題。
- 組合(Composition)
物件可以在其例項變數中包含其他物件
思考
隨著電腦科學的發展,面向物件也一直在擴充套件,其實面向物件只是一種程式設計範例,或者是一種程式設計思路,只是編碼解決問題的一種通用思路,不同語言對於面向物件的支援與實現其實也是大同小異,瞭解面向物件的思想更為重要。無需糾結概念上的卻別,例如,Golang認為組合優於繼承,但是從大體來看其實組合和繼承最終的結果都是為了複用。
為什麼面向物件程式設計
面向物件解決的問題
面向物件思想早在20世紀50年代末和60年代初就已經被提出,第一個真正實現面向物件的語言Smalltalk,也就在20世紀70年代出現;面向物件的提出就是為了提高軟體的重用性、靈活性和擴充套件性。
早期的程式設計正規化就是程序式程式設計,因為計算機執行的時候就是一行一行指令執行,所以傳統的程式設計方式就是把程式看成一系列函式的集合,或者直接向機器發出指令(如,組合語言),這就是面向過程的程式設計。而隨著計算機的發展,以及程序式程式設計暴露出來的問題,如無法複用,不靈活,不符合人類的思維方式等等,這就是面向物件思想產生的原因,人們希望程式設計是更加靈活更加符合人類思維方式的,面向物件程式設計本質可以看成是由各種獨立而互相呼叫的物件組成的程式,而且事實證明,面向物件確實比過程式更加靈活,更加容易維護。
由於面向物件的各種特點,使得面向物件程式設計更加容易學習,是複雜的問題簡單化,是程式更加便於分析、設計和理解。
與其他程式設計正規化比較
那麼,既然面向物件如此靈活易用,那麼我們還需要其他的程式設計正規化嗎?
其實不然,使用面向物件解決問題的時候需要明確抽象的層次,也就是說不同的問題其對應的抽象層次是不同的,比如, 起床
這件事情:
- 面向物件
class People { public void getUp(){ } } People.getUp();//起床
- 面向過程
//起床開始 openEye;//睜眼,開始到結束 dressed;//穿衣服,開始到結束 getOutOfBed;//下床,開始到結束 //起床結束
這樣看來,從 起床
這件事情來看,面向物件更加簡潔明瞭,但是面向過程在這過程中就沒用了麼,其實不是,例如, People.getUp
方式可以這麼實現:
class People { public void getUp(){ Eye.open();//睜眼 Body.dressed();//穿衣服 Body.getOutOfBed();//下床 } }
那麼,從 起床
這個問題來看,起床的內部實現其實還是面向過程式的,即使用的還是面向物件程式設計去實現,所以,我個人覺得面向物件是相對的,需要站在解決問題的角度來看待面向物件的抽象層次,與之對於的程序式程式設計是在不同的層次解決不同的問題,其他程式設計正規化也一樣,它們之間可以並存,這並不矛盾。
設計模式(Design Pattern)
談起面向物件程式設計,就不得不得說設計模式, 設計模式
最初來源於建築設計領域,後由GoF(Gang of Four,四人幫,Erich Gamma,Richard Helm,Ralph Johnson,John Vlissides)引入到電腦科學,在他們合作出版的《設計模式:可複用面向物件軟體的基礎》(Design Patterns - Elements of Reusable Object-Oriented Software)一書中介紹了23種設計模式,而隨著電腦科學的發展,設計模式也越來越多,應用也越來越廣泛。
設計模式是一種用於在某個範圍內普遍發生的問題的通用解決方法,設計模式不是程式碼,可以說是一種解決方案或者最佳實踐,它是描述在不同情況下要怎麼解決問題的一種方案或一種模板。
常見的設計模式:

image.png
SOLID和GRASP
S.O.L.I.D設計原則
單一職責原則(Single responsibility principle)
一個類或則一個模組應當只有一種職責,其提供的服務應該與其責任保持一致,如果存在多種責任則應考慮對其拆分。
開閉原則(Open/closed principle, OCP, Open for extension, close for modification)
軟體中的物件(類,模組,函式等等)應該對於擴充套件是開放的,但是對於修改是封閉的
開閉原則主要思想就是對於擴充套件的包含,對於修改的限制,新增功能的同時避免修改已有的實現,儘量做到對外提供的功能不變
里氏替換原則(Liskov substitution principle)
程式中的物件應該是可以在不改變程式正確性的前提下被它的子類所替換的
里氏替換原則認為子類的功能應該可以完全替換父類並且不會影響程式的正確性,簡單理解就是子類在繼承父類的同時不能改變父類已有的功能,加上開閉原則子類只能對父類進行擴充套件而不能對父類的功能進行修改。
介面隔離原則(Interface segregation principle,ISP)
多個特定功能介面要好於一個寬泛用途的介面
介面隔離強調將大而全的介面拆分成小而精的介面,使用方只需關係自己需要的介面,通過介面隔離有利於系統的解耦,增加程式的易用性和拓展性。
依賴反轉原則(Dependency inversion principle,DIP)
1.高層次的模組不應該依賴於低層次的模組,兩者都應該依賴於抽象介面</br>
2.抽象介面不應該依賴於具體實現。而具體實現則應該依賴於抽象介面
依賴反轉原則是指一種特定的解耦(傳統的依賴關係建立在高層次上,而具體的策略設定則應用在低層次的模組上)形式,使得高層次的模組不依賴於低層次的模組的實現細節,依賴關係被顛倒(反轉),從而使得低層次模組依賴於高層次模組的需求抽象。
GRASP面向物件設計
通用職責分配軟體模式(General Responsibility Assignment Software Patterns,GRASP),面向物件的設計原則,與SOLID設計原則無關,與GoF設計模式也不太相同,GRASP更像是一個設計思想,是在面向物件設計過程中起指導的作用,是長期面向物件程式設計過程中經過驗證且標準的最佳實踐,可以說我們通常所說的設計模式是基於GRASP的。
GRASP告訴我們怎樣設計問題空間中的類與分配它們的行為職責,以及明確類之間的相互關係等,下面簡單介紹下GRASP的九個原則:
資訊專家原則(information expert)
資訊專家原則主要用於表明何處委派職責,職責的委派可以是一個方法,欄位等。
分配職責的原則:檢視給定的職責,確定履行職責所需要的資訊,然後確定資訊的位置並將其職責分配給它。也就是,將職責分配給擁有履行一個職責所必需所有信息的類。
創造者原則(creator)
建立物件是面向物件系統中最常見的活動之一。建立者原則表明物件的建立應該由哪個類負責建立的原則,如果A和B之間符合下面的規則,則表明A的建立可以分配給B:
- 例項B包含例項A或者例項B聚合例項A
- 例項B記錄例項A
- 例項B頻繁使用例項A
- 例項B擁有例項A初始化的全部資訊並且在建立的過程中把這些資訊傳遞給A
低耦合原則(low coupling)
耦合是衡量一個元素與其他元素的連線,或依賴其他元素的強弱程度。低耦合是一種評估模式,決定了如何將責任的分配
低耦合設計:
- 類與類之間的依賴儘可能要降到最小
- 修改一個類對其他類的影響應該是無影響或者要把影響降到最小
- 提高系統的複用性
高內聚原則(high cohesion)
高內聚是衡量物件保持適當的集中,可管理和可理解的程度,低耦合通常需要高內聚的支援。
高內聚意味著特定元素的職責是強相關且高度集中的,為了實現高內聚通常做法就是類的劃分和子系統的劃分,若是劃分的元素低內聚也就是職責不明確,那麼使用者將會難以理解,程式也難以複用,難以維護。
控制器原則(controller)
控制器模式是通過控制器(Controller)將系統事件或者一類用例分配給對應職責的物件,這個物件可以是類或模組或子系統,控制器不與UI進行互動,它只負責系統事件的調配。
基於用例的控制器應該負責處理該類別的所有用例,並且是支援多用例的(如,使用者相關的用例,新增使用者和修改使用者等應該統一交給使用者控制器處理)
雖然控制器不與UI互動,但控制器通常用於UI層之外的第一層,也就是我們經常使用的MVC軟體架構種的 C
層(即控制器層),控制器層起組織協調的作用,負責事件的分發委派並返回處理結果。
多型性原則(polymorphism)
多型性原則即面向物件的三大特徵之一,指的是不同的型別實現統一的介面,使在系統執行期間相同訊息傳送給不同型別的例項而會有不同行為。
在具有多型性的場景下應該使用多型性操作,而不應該使用具體某個型別(如表現在Java中就是使用介面程式設計,即IOP)
純虛構(pure Fabrication)
純虛構是指一個不代表處理某個問題領域的類,專門用於實現 高內聚低耦合
,提高複用性,這總類在領域驅動設計中被稱為服務(Service)。
為了實現 高內聚
類通常根據功能被劃分稱為功能集中的類,而這種劃分導致使用方需要更多的類從而提高了耦合度,這與低耦合相矛盾,而通過純虛構可以構造出"虛構類",這種類不是針對某個問題,而是某些能力/功能的抽象劃分。(實際使用中例如我們通常使用的分層系統,資料庫訪問層就屬於純虛構的一種實現)
中介原則(Indirection)
中介模式是指通過一個 中介
來實現兩個物件之間的互動實現低耦合。其目的是為了避免兩個物件之間產生直接耦合,降低物件之間的耦合度。
同樣的,在MVC設計模式中,控制器(Controller)起到的作用就是作為中介連線其資料模型(Model)與檢視(View)
受保護變數原則(protected Variations)
與開閉原則類似,通過使用介面封裝系統中存在的不穩定點,並且使用多型操作來使用此介面,從而避免不穩定點影響其他物件(類,模組,子系統等)
總結
面向物件程式設計其實是一個非常系統非常抽象的話題,這裡只對主要概念的介紹以及一些個人的看法。要理解並使用面向物件程式設計需要通過不斷的實踐和理解,並且需要一定的抽象能力,通過閱讀以後的優秀原始碼也可以增強自己的面向物件程式設計意識。如果在面向物件抽象過程中一團霧水的話可以通過畫圖來整理思路,通過面向物件的設計原則來檢查自己的設計與實現,循序漸進不斷迭代來提高自己的面向物件程式設計能力和思維方式。
參考
- ofollow,noindex">面向物件程式設計-維基百科
- SOLID
- GRASP_(object-oriented_design)