1. 程式人生 > 其它 >內功修煉系列(壹):面向物件思想

內功修煉系列(壹):面向物件思想

面向物件是一種程式設計思想,而不是一種語言,更不是Java本身。Java也同樣可以寫出面向過程的程式,C語言也能使用面向物件的思想去程式設計。
本篇是《程式設計的邏輯:用面向物件方法實現複雜業務需求》一書的前兩章的讀書筆記,原作是李運華,內容摻雜原書作者的內容和自己的思考+總結。

OOP的發展歷程

程式設計思想的演變是從無到面向過程,再從面向過程當中發現不足進而發展出來面向物件OOP。從無到面向過程,突破的點是——人們編寫程式不需要關注具體的機器、指令集、儲存介質等硬體因素。從這裡開始有了所謂的高階語言。相比原來,程式更好編寫和維護。

那麼從上面時候開始發現面向過程思想的不足呢?上世紀中後期人們發現軟體質量和按時交付率隨著軟體規模的不斷變大、複雜度的不斷變高

而變得不盡人意,屢次出現了很多嚴重的事故。這被人們稱為“軟體危機”。

軟體危機促進了軟體工程的發展,針對軟體危機,人們也作出了很多批判(比如“goto是有害的”/“我們需要模組化來控制程式複雜度”...)。大家解決這個危機的初步嘗試的產物是結構化程式設計思想的出現,特徵是"自頂向下、逐步細化和模組化",典型的程式語言有Pascal。

初步的嘗試其實也很快面臨問題,因為Pascal本質上還是面向過程,雖然結構化一定程度上控制了複雜性的問題,但是隨著變化的增加,人們開始要求軟體要具有可擴充套件、可維護性。人們第二次解決軟體危機的行動產物,就是發明了諸如CPP、Java等著名的面向物件程式語言,這種支援OOP的程式語言結合OOP一直髮展到現在。

面向過程和麵向物件的比較

什麼是面向過程,具有面向過程的程式設計思維是怎麼樣的?考慮一件事,使用面向過程的思維就是在思考“做完這件事需要什麼步驟?”。類似一個流水線一樣去劃分、銜接工序,傳遞著處理產物。可以這樣說,在面向過程的思想當中,程式=演算法+資料結構。演算法就是我的流水線,資料結構則是我上下游工序的輸入、產出的物件的描述。

面向物件則不一樣,如果使用OOP的思維方式,會考慮“做這件事情都有誰參與?他們長什麼樣?”。

就好像打籃球,想要元件王朝球隊,我需要一個體重輕盈(最好不要超過80KG)的、跑動靈活的控衛,還需要一個穩如泰山(體重100KG以上),精通擋拆戰術的中鋒...。至於為了拿到季後賽的冠軍這件事情或者是贏得下一場比賽如何打、使用什麼戰術、什麼時候換人這些事情等到具體的變化、場景出現的時候再來考慮。

可以這樣說,OOP的思想當中,程式=物件+互動。我定義他是什麼樣的,再程式設計的時候讓他們之間互動。

面向物件的長短處

面向物件的好處就是擁抱需求的變更,打籃球和流水線生產東西不一樣,流水線生產東西有明確的約定,如果上下游工序修改,則影響整體的執行。打籃球這件事本身就是擁抱變化的,如果說打籃球的戰術等價於演算法,那麼我物件可以支援的演算法就五花八門。這就叫擴充套件性。

可擴充套件性帶來的好處主要迎合了不斷變化的需求,經常變化的場景就適合使用面向物件思想。這是OOP思想的長處.

其實在一些基礎、穩定不變的基礎軟體上,比如資料庫、OS。OOP的長處“擁抱變化”其實就不那麼明顯。甚至有時候會因為遠離底層導致效能方面略遜一籌。

衡量一個軟體質量好有很多維度,比如:可擴充套件性、成本、效能、可靠、安全、可維護、可移植、可伸縮。OOP並不是所謂的“六角戰士”,除了可擴充套件性,OOP這種程式設計思想基本上都拿不到很高的分數。

說在開篇的一些解釋

如何使用C語言來運用OOP思想去寫出OOP的程式碼呢?典型例子就是Redis,它的原始碼中的事件處理,在不同的OS有不同的實現,Linux使用epoll核心系統呼叫,Unix使用select核心系統呼叫... 。如果是面向過程,那麼他可能會這樣實現:

if(osname=='UNIX'){
  select();
}else if(osname=='LINUX'){
  epoll();
}

但是Redis並不使用面向過程的方式實現它,它定義了一些介面:

  1. 建立事件
  2. 新增事件
  3. 刪除事件

使用不同的實現方式去實現這個介面,再在事件處理的流程當中統一的去處理。

也許我們最早學Java寫出來的程式碼就是面向過程的,在最早學習Java的時候,main方法並不是整個程式的一個入口,main方法是整個程式的全部。我們會定義這個類一些屬性和方法,再以main方法為所有動作的施展之地,一行一行的呼叫我們的方法去實現我們的流程,這就是面向過程的思想的實踐。可見思想的選擇和施展並不囿於語言本身。

什麼組成了我們的面向物件理論?

有人說類是一組物件的抽象,這個解釋毛病在用不確定的概念解釋不確定的概念,有人說類是屬性和方法的集合,這個解釋的毛病在於過度的淺顯,將程式碼和我們的"類"這個概念劃等號。

類的定義是:一組相似事物的統稱。 站在你自己的角度,具有相似點的事物就可以稱為一類。

類的組成有哪些:

類=屬性+方法。

如何去抽象一個事物為一個類,首先要對事物要素識別,將其相似點抽取出來稱為屬性,屬性的抽取的要求為“屬性不可以再被分割”。這裡很像我們設計資料庫表字段的時候,你的老師會要求你設計的欄位是不可以再分一樣。

這叫做屬性最小化原則。

事物有他自己的動作,描述事物的時候看到描述裡面的動詞,這個動詞就可以被識別為一個方法。設計類的方法的時候,注意一個基本原則——方法單一化原則。也就是一個方法只幹一件事情。

物件

物件是具體的類,是真實世界的模擬,是一個個體,存在於軟體執行的過程中。

小總結一下:現實有相似點的事物歸納為現實意義上的類,現實意義上的一類事物抽象為軟體中的類,軟體中的類例項化稱為物件,程式在執行的時候就使用這些物件。

軟體中的類一般都能對應上現實意義上的一類事物,但是有的軟體類是對應不上的,比如“策略”一詞,我就可以把策略當做一個類。

介面

歸咎於介面這個中文翻譯的晦澀性。日常開發當中介面一次是最被濫用的一個詞語,一個interface是一個介面,一個API也是一個介面。從英語詞彙的角度來看,interface可以被拆開兩半來理解。

  • inter是互動的意思,常見的詞彙有International,Internet等,International是nation的互動,Internet是net網路的互動。
  • face比較簡單,就是面部,面的的意思。

互動其實好理解,但是face的話,你可以認為一個face就是一個展現、一個傳達。你可以認為這是一個功能。

我多個face進行互動,是我想要告訴外界,我又很多資訊要傳達。這個功能是很多樣的。

總結一下介面的定義:介面是一組相關的互動功能點的定義的集合。結合例項,一個介面,包含一種類(比如UserDo)的CRUD,那麼C或者是R或者是UD也好,其實都是一個個功能點,由於介面是不會實現的,那麼其實就是功能點的定義,我有四個(even more)這種功能點的定義,那麼其實它就是一個定義的集合。

話說回來,面對被濫用的介面一次,我們要學會分語境來判別它使用的是否正確,比如前後端聯調,前端呼叫查詢介面,那麼這個“查詢介面”只能是一個功能點,多個功能點才會組成一個真真的interface。

Java中的interface是如何體現OOP當中介面的特徵的呢?

  1. 介面可以定義多個功能,從1~N個都可以。
  2. 介面必須為public的修飾符,體現了inter字首該體現的互動性。
  3. 介面沒有實現(這裡不講default和static
  4. 接口裡面的方法一般、標準的情況下適合一類相關的。

那麼什麼時候使用介面?當你不想知道物件所屬的具體的類,但是想要知道這些類有什麼功能的時候,就使用他!

抽象類

抽象類是一種特殊的類,特殊在:

  1. 不能例項化
  2. 只能繼承 extends,不能implements

“存在即合理”,為什麼要存在一種限制性很突兀的東西,一般事物的限制性很強烈、很突兀的時候,往往意味著它有另一方面強烈的好處,只是一般人很少意識到而已。

抽象類是更高層次的抽象,和普通類不同,比如蘋果、草莓和菠蘿都隸屬於水果,那麼水果就是更高層次的抽象。抽象類的特點是可以有自己的抽象方法,即只有宣告和定義,並沒有實現(這裡非常類似介面,作用也異曲同工——“不想知道這個抽象執行的時候具體屬於那個類,但是我想知道你能做什麼”)。

既然抽象類和介面很類似,那為什麼還區分這兩個東西?——因為抽象類本質上還是類,強調的是一組事物共同的屬性、定義的方法,部分方法其實實現出來是可以給子類進行共享的。而介面只強調方法宣告上的相似性(一個介面包含的多個功能點只屬於一類物件的),具體怎麼實現需要實現類一個不落的去實現。

什麼是抽象?

抽象的定義

抽象,抽象,白話的解釋就是抽取比較相似的東西出來,結合之前說過的類和物件的兩個定義:

  1. 類:類是一組相似事務的統稱。
  2. 物件:物件是一個具體的類,是一個真實存在的類。

那麼就可以再次詳細的闡明抽象的詳細定義:抽取多個物件或者類中比較像的部分。

抽象的層次可以很多種,比如把屬性類似的東西抽取為父類,或者將行為類似的抽取為父類。具體應用設計的時候,可以從不同的觀察角度來。

抽象有什麼作用

抽象的作用是劃分類似,其他學科裡面也有類似的說法。

OOP的三大核心特徵

相輔相成的三核心特徵

  1. 封裝
  2. 繼承
  3. 多型

封裝

封裝從詞彙上來看,其實就是不想讓人看見。歹徒給你戴個黑色頭套,其實也就是在封裝自己,不想讓自己的認識的人識破自己。在OOP裡面,封裝屬於類的一個功能,類選擇封裝是為了自己的隱私,不想讓自己的資料結構、屬性方法暴露給別人來肆意修改,避免因為濫用導致出現問題的影響範圍擴大。

這種避免糟糕影響範圍擴大的目的被稱為“隔離複雜度”。A類想要獲取B類的資訊,不需要知道A類的具體內部實現、結構(這叫複雜度),只需要呼叫A類提供的方法(這叫隔離)。

在OOP裡面,封裝的施加物件可以是屬性和方法。比如我將一個類的某屬性設定為private,這其實也就是封裝,我封裝了我這個類的屬性為私有訪問,想要訪問我的屬性必須通過我開放的getXXX方法( 在這個方法裡面我可以校驗你的許可權,可以對你說“No means No” XD )。

其實,再被別人問到封裝的好處的時候,應該從如下兩個方面開始作答:

  1. 將類封閉,控制訪問
  2. 隔離複雜度

繼承

繼承是OOP最基本的特徵,有了繼承這個特徵,隨之而來的就是子類、父類的定義。子類遺傳了父類的部分方法和屬性。而什麼屬性和方法可以遺傳給子類,也取決於父類屬性和方法的開放程度是在protected之上還是之下。

抽象和繼承兩者是什麼關係?

再次回顧一下抽象的定義:抽取多個物件或者類中比較像的部分,是一個發生在設計階段的動作。而繼承是實現階段的動作,根據抽象的思維產物得到類,在使用繼承來表達這個思維產物。

可以說繼承是OOP對抽象的表達。

多型

多型的英文是polymorphoismmorphoism是形態學的意思,poly字首表示的是多的意思。在OOP裡面,多型的定義是:使用父類的應用,能夠呼叫子類的物件。

這種特性起到一個非常重要的作用,就是能使用多型來遮蔽子類物件的差異,通用性的程式碼很容易產出。如果新的子類新增和產生,對於原有的呼叫者的程式碼不需要任何變動。

總結

本章文章從幾個方面闡述和介紹了OOP,首先,OOP是一種程式設計思想,起源是為了解決第二次軟體危機——人們對“擴充套件性”的追求。和造成第二次軟體危機的面向過程設計思想來說,OOP側重於對目標事物的定義,大於完成目標的過程的定義。這種思維的好處是擁抱變化,能夠很快的響應需求,暫時了第二次危機繼續解決的擴充套件性要求。

OOP的理論支撐需要四類事物,類、物件、介面和抽象類。

  1. 類是相似事物的統稱
  2. 物件是一個具體的個體的事物,在程式執行的時候承載資訊。
  3. 介面是一系列功能點的定義,當我們不想知道某類物件在執行時候是誰,只想知道某類物件在執行時候能提供什麼功能點的時候,就使用介面。
  4. 抽象類本質上還是類,包含一些需要子類定義的方法,也共享子類共有的一些方法。

OOP的三大核心特徵也必須要牢記:

  1. 封裝是為了控制對物件資訊的訪問,隔離了複雜性。
  2. 繼承是抽象的表達方式,是OOP最基本的特徵
  3. 多型的含義就是父類應用可以呼叫子類物件的實現,利用這個特性可以寫出擴充套件性強、可修改性好的程式碼。