天天看點

計算機基礎原來可以如此好懂!——「面向抽象程式設計」

“面向抽象程式設計,面向接口程式設計”這句話流傳甚廣,它像一面旗幟插在每個人前進的道路上,引導大家前行。每個程式員都免不了和抽象打交道,差距可能在于能否更好地提煉。

這句話包含兩部分含義:“面向抽象程式設計”本質上是對資料的抽象化,“面向接口程式設計”本質上是對行為的抽象化。

本文我們先談“面向抽象程式設計”,即資料的抽象化。

3.1 抽象最讨厭的敵人:new

因為直接講什麼是抽象不太好講,容易描述的話那就不是抽象了,是以我們換個角度,先聊聊抽象的反面:什麼是具體。在具體裡,有個先鋒人物,就是我們都熟悉的new。大家知道,new是最簡單和最常見的關鍵字,用來建立對象。但被建立出來的一定是具體的對象,是以new代表着具體,它是抽象最讨厭的敵人。

大家要有這種敏感:什麼時機建立對象,在哪裡建立,是很有講究的。為了闡述這個話題,我們先看下面這行代碼:

Animal animal = new Tiger(); // Animal是抽象類           

我曾經對這句簡單的指派語句思考很久:左邊抽象,右邊具體,感覺不對等,這樣寫好不好?答案不簡單啊。

接下來,我們分成兩個方向細細讨論。

假設一:如果它是某個類的成員變量的定義。例如:

private Animal animal = new Tiger();           

先下結論:如果類裡其他地方沒有對animal這個變量的指派操作,此後再沒有更改它的邏輯了,那麼它基本不是好寫法(有少許例外,後面會講)。那麼,什麼是好寫法?

哈,這裡先賣個關子。

這裡需要注意的是,我們讨論的是左邊是抽象,右邊是具體的new。如果new的兩邊是平級概念的類,例如:

Tiger tiger = new Tiger();            

它左右兩邊沒有抽象之分,那麼不在本文讨論範圍之内。

假設二:如果它是某個函數内部的變量定義語句。示例如下:

void Show() {
Animal animal = new Tiger();
    ...... // 出場前的準備活動
    ShowAnimal(animal);
}           

我曾經疑惑:為何不直接定義成子類類型?就這樣寫:

Tiger tiger = new Tiger();           

根據繼承原理,子類能調用抽象類的方法。是以也不會影響接下來的函數調用。例如:所有的animal.Eat替換為 tiger.Eat一定成立。

同時根據裡氏替換原則,但凡出現animal的地方,都可以把tiger代替進去,是以也不會影響我的參數傳遞。例如:ShowAnimal(animal)替換為ShowAnimal(tiger)也一定成立。

可一旦把Tiger類型上溯轉為抽象的Animal類型,那麼Tiger自身的特殊能力(例如Hunt)在“出場前的準備活動”那部分就用不了,例如:

tiger.Hunt(); // 老虎進行狩獵
animal.Hunt(); // 不能通過編譯           

也就是說,Animal animal = new Tiger();裡Animal的抽象定義,隻有限制我自由的作用,而沒有帶來任何實質的好處!這種寫法不是很糟糕嗎?

你會有一天頓悟:這種對自由的限制,恰恰是最珍貴的!大部分時候,我們缺的不是自由,而是自律。任何人的自由,都不能以損害别人的利益為代價。

ShowAnimal(animal);之前的那段“出場前的準備活動”代碼,将來很有可能是别人來維護的。在架構設計上,一定要考慮“時間”這個變量帶來的不确定性。如果你定義成:

Tiger tiger = new Tiger();            

這看起來更靈活,但你沒法阻止這隻老虎被别人将來使用Hunt函數濫殺無辜。

一旦定義為:

Animal animal = new Tiger();           

那麼,這隻老虎将會是一隻溫順的老虎,隻遵循普通的動物準則。

是以如果“出場前的準備活動”這部分的業務需求裡隻用到Animal的基本功能函數,那麼:

Animal animal = new Tiger();           

要優于

Tiger tiger = new Tiger();           

好了,等号左邊的抽象問題解決了,但等号右邊的new呢?這個場景裡,Animal animal = new Tiger();是函數的局部變量,也沒有傳導到全局變量中。到目前為止,這個new是完全可以接受的。面向抽象,是要在關鍵且合适的地方去抽象,如果處處都抽象,代價會非常大,得不償失。如果滿分是100分的話,目前能得95分,已經很好了,這也是我們大多數時候的寫法。

但你還是要知道:一旦接受了這個new,好比是和魔鬼做了契約,會付出潛在代價的。此處的代價是這段代碼不能再更新成架構性的抽象代碼了。想要完美得到100分,則需要消滅這個new,怎麼辦呢?

3.2 消滅new的兩件武器

前一節站在理論高度“批判”了new,其實并不是說new真的不好,而是說很多人會濫用。就好比火是人類文明的起源,好東西,但是濫用就會造成火災。把火源限定在特定工具才能點火,隔離開,用起來才安全。new其實也一樣,本節講的本質上不是消滅new,而是隔離new的兩件武器。

3.2.1 控制反轉——髒活讓别人去幹

還記得前面賣的關子嗎?如果animal是類成員變量:

private Animal animal = new Tiger();            

這并不是好寫法,那麼什麼是好寫法呢?這種情況下,比較簡單的是對它進行參數化改造:

void setAnimal(Animal animal) {
    this.animal = animal;
}           

然後讓客戶去調用注入:

Tiger tiger = new Tiger();
obj.setAnimal(tiger);           

有了上面的注入代碼,private Animal animal = new Tiger();這句話反而變得可以接受了。因為等号右邊的Tiger僅僅是預設值,預設值當然是具體的。

上面的參數化改造手法,我們可以稱為“依賴注入”,其核心思想是:不要調我,我會去調你!依賴注入分為屬性注入、構造函數注入和普通函數注入。很明顯,上面的例子是屬性注入。依賴注入和标題的“控制反轉”還不能完全劃等号。确切地說,“依賴注入”是實作“控制反轉”的方式之一。

這種幹脆把建立對象的任務甩手不幹的事情,反而是個好寫法,境界高!這樣,你不知不覺把自己的代碼完全變成了隻負責資料流轉的架構性代碼,具備了通用性。

在通往架構師的道路上,你要培養出一種感覺:要建立一個跨作用域的實體對象(不是值對象)是一件很謹慎的事情(越接觸大型項目,你對這點的體會就越深),不要随便建立。最好不要自己建立,讓别人去建立,傳給你去調用。那麼問題來了:都不願意去建立,誰去建立?這個丢手絹的遊戲最終到底要丢給誰呢?

先把問題揣着,我們接着往下看。

3.2.2 工廠模式——抽象的基礎設施

我們回到這段Show代碼:

void Show() {
    Animal animal = new Tiger(); // 上面說過,這裡的new目前是可以接受的
    ...... // 出場前的準備活動
    ShowAnimal(animal);
}           

但如果Show方法裡建立動物的需求變得複雜,new會變得猖狂起來:

void Show(string name) {
    Animal animal;
    if(name == "Tiger")
        animal = new Tiger();
    else if(name == "Lion")
        animal = new Lion();
    ...... // 其他種類
    ShowAnimal(animal);
}           

此時将變得不可接受了。對付這麼多同質的new(都是建立Animal),一般會将它們封裝進專門生産animal的工廠裡:

Animal ProvideAnimal(string name) {
    Animal animal;
    if(name == "Tiger")
        animal = new Tiger();
    else if(name == "Lion")
        animal = new Lion();
        ...... // 其他種類
}           

進而優化了Show代碼:

void Show(string name) {
    Animal animal = ProvideAnimal(name); // 等号兩邊都是同級别的抽象,這下徹底舒服了
    ShowAnimal(animal);
}           

是以,依賴注入和工廠模式是消滅new的兩種武器。此外,它們也經常結合使用。

上面的ProvideAnimal函數采用的是簡單工廠模式。由于工廠模式是每個人都會遇到的基本設計模式,是以這裡會對它進行更深入的闡述,讓大家能更深入地了解它。工廠模式嚴格說來有簡單工廠模式和抽象工廠模式之分,但真正算得上設計模式的,是抽象工廠模式。簡單工廠模式僅僅是比較自然的簡單封裝,有點配不上一種設計模式的稱呼。是以,很多教科書會大篇幅地介紹抽象工廠,而有意無意地忽略了簡單工廠。但實際情況正好相反,抽象工廠大部分人一輩子都用不上一次(它的出現要依賴于對多個相關類族建立對象的複雜需求場景),而簡單工廠幾乎每個人都用得上。

和一般的設計模式不一樣,有些設計模式的代碼結構哪怕你已經爛熟于心,卻依然很難想象它們的具體使用場景。工廠模式是面向抽象程式設計,資料的建立需求變複雜之後很自然的産物,很多人都能無師自通地去使用它。将面向抽象程式設計堅持到底,會自然地把建立對象的任務外包出去,丢給專門的工廠去建立。

可見,工廠模式在整個可擴充的架構中扮演的不是先鋒隊角色,而是強有力的支援“面向抽象程式設計”的基礎設施之一。

最後調侃一下,我面試候選人的時候,很喜歡問他們一個問題:“你最常用的設計模式有哪些?”

排第一的是“單例模式”,而“工廠模式”是當之無愧的第二名,排第三的是“觀察者模式”。這側面說明這三種模式應該是廣大程式員最容易用到的設計模式。大家學習設計模式時,首先應該仔細研究這三種模式及其變種。在其他章節中,還會詳細介紹另外兩種模式。

3.2.3 new去哪裡了呢

這裡回到最開始也是最關鍵的問題:如果大家都不去建立,那麼誰去建立呢?把髒活丢給别人,那别人是誰呢?下面我們從兩個方面闡述。

 局部變量。局部變量是指在函數内部生産又在函數内部消失的變量,外部并不知曉它的存在。在函數内部建立它們就好,這也是我們遇到的大多數情況。例如:

void Show() {
    Animal animal = new Tiger();
    ...... // 出場前的準備活動
    ShowAnimal(animal);
}           

前面說過,這段代碼裡的new能得95分,沒有問題。

 跨作用域變量。對這類對象的建立,總是要小心一些的。

 如果是零散的建立,就讓各個用戶端自己去建立。這裡的用戶端是泛指的概念,不是伺服器對應的用戶端。凡是調用核心子產品的發起方,均屬于用戶端。每個用戶端是知道自身具體細節的,在它内部建立無可厚非。

 如果寫的是架構性代碼,是基于總體規則的建立,那就在核心子產品裡采用專門的工廠去建立。

3.3 抽象到什麼程度

前面說過,完全具體肯定不行,缺乏彈性。但緊接着另一個問題來了:越抽象就越好嗎?不見得。我們對抽象的态度沒必要過分崇拜,本節就專門讨論一下抽象和具體之間如何平衡。

比如Java語言,根上的Object類最抽象了,但Object定義滿天飛顯然不是我們想要的,例如:

Object obj = new Tiger();           

那樣你會被迫不停地進行下溯轉換:

Animal animal = (Animal)obj;           

是以不是越抽象越好。抽象是有等級之分的,要抽象到什麼程度呢?有一句描述美女魔鬼身材的語句是“該瘦的地方瘦,該肥的地方肥”。那麼,這句話可改編一下,即可成為抽象程式設計的原則,即“該實的地方實,該虛的地方虛”。也就是說,抽象和具體之間一定有個平衡點,這個平衡點正是應該時刻存在程式員大腦裡的一件東西:使用者需求!

你需要做的是精确把握使用者需求,提供給使用者的是滿足使用者需求的最根上的那層資料。什麼意思呢?本節通過下面這個例子詳細闡述。

村裡的家家戶戶都要提供一種動物去參加跑步比賽,于是每家都要實作一個ProvideAnimal函數。你家裡今年養了一隻老虎,老虎屬于貓科。三層繼承關系如下:

public abstract class Animal {
    public void Run();
}
public class Cat : Animal {
    public int Jump();
}
public class Tiger : Cat {
    public void Hunt(Animal animal);
}           

現在有個問題:ProvideAnimal函數的傳回類型定義為什麼好呢?Animal、Cat還是Tiger?這就要看使用者需求了。

如果此時是舉行跑步比賽,那麼隻需要你的動物有跑步能力即可,此時傳回Animal類型是最好的:

public Animal ProvideAnimal() {
    return new Tiger();
}           

如果要舉辦跳高比賽,是Cat層級才有的功能,那麼傳回Cat類型是最好的:

public Cat ProvideAnimal() {
    return new Tiger();
}           

切記,你傳回的類型,是客戶需求對應的最根上的那個類型節點。這是雙赢!

如果函數傳回值是最底下的Tiger子類型:

public Tiger ProvideAnimal() {
    return new Tiger();
}           

這會帶來如下兩個潛在的問題。

問題1:給别人造成濫用的可能

這給了組織者額外的雜亂資訊。本來呢,對于跑步比賽,每一個參賽者隻有一個Run函數便清晰明了,但在老虎身上,有Run的同時,還附帶了跳高Jump和捕獵Hunt的功能。這樣組織者需要思考一下到底應該用哪個功能。是以提供太多無用功能,反而給别人造成了困擾。

同時也給了組織者犯錯誤的機會。萬一,他一旦好奇,或者錯誤操作,比賽時調用了Hunt方法,那這隻老虎就不是去參加跑步比賽,而是追捕别的小動物吃了。

問題2:喪失了解耦子對象的機會

一旦對方在等号兩邊傻傻地按照你的子類型去定義,例如:

Tiger tiger = ProvideAnimal();           

從此組織者就指名道姓地要你家的老虎了。如果比賽當天,你的老虎生病了,你本可以換一頭獵豹去參加比賽,但因為别人預定了看你家的老虎,是以非去不可。結果便喪失了寶貴的解耦機會。

如果是Animal類型,那麼你并不知道是哪一種動物會出現,但你知道它一定會動起來,跑成什麼樣子,你并不知道。這樣的交流,是比較進階的交流。繪畫藝術上有個進階術語叫“留白”,咱們程式設計玩“抽象”也算是“留白”。我先保留一些東西,一開始沒必要先确定的細節就不先确定了。那這個“留白”留多少呢?根據使用者需求而定!

3.4 總結

多态這門特技,成就了人們大量采用抽象去溝通,用接口去溝通。而抽象也不負衆望地讓溝通變得更加簡潔、高效;抽象也讓互相間依賴更少,架構更靈活。

參數化和工廠模式是消滅或隔離new的兩種武器。

使用者需求是決定抽象到何種程度的決定因素。

——本文選自

《代碼裡的世界觀:通往架構師之路》
計算機基礎原來可以如此好懂!——「面向抽象程式設計」

程式設計中有很多通用的知識點,它們是10年甚至20年都不會淘汰的程式設計技術,市面上也極少有将它們綜合起來并講得有意思的書。

上面這本書是一位IBM架構師結合了自己13年程式設計經驗,結合自己的了解和領悟,把許多知識點彙入到了這本書裡。它們并不是潮流的知識點,而是厚重的基礎知識。

圖書目錄

第1章 程式世界的兩個基本元素

1.1 資料和代碼的互相僞裝

1.2 資料和代碼的關系

第2章 用面向對象的方式去了解世界

2.1 好的程式員是安徒生

2.2 封裝——招兵買馬,等級森嚴

2.3 繼承——快速進化

2.4 多态——抽象的基石

2.5 總結

第3章 面向抽象程式設計——玩玩虛的更健康

3.1 抽象最讨厭的敵人:new

3.2 消滅new 的兩件武器

3.3 抽象到什麼程度

3.4 總結

第4章 耦合其實無處不在

4.1 耦合的種類

4.2 耦合中既有敵人也有朋友

4.3 壞耦合的原因

4.4 解耦的原則

4.5 總結

第5章 資料的種類——生命如此多嬌

5.1 常用資料類型

5.2 按生命周期劃分資料

5.3 兩個重要的資料容器

5.4 對象的種類

5.5 描述資料的資料

5.6 總結

第6章 資料驅動——把變化抽象成資料

6.1 三個案例

6.2 資料驅動的好幫手:反射

6.3 總結

第7章 對象之間的關系——父子、朋友或情人

7.1 繼承——父子關系

7.2 組合——朋友關系

7.3 依賴——情人關系

7.4 總結

第8章 函數的種類——迷宮的結構

8.1 面向對象的函數叫方法

8.2 參數是函數的原材料

8.3 傳回值對函數的意義

8.4 值傳遞、引用傳遞和指針傳遞

8.5 有狀态函數和無狀态函數

8.6 靜态函數和普通函數

8.7 能駕馭其他函數的函數

8.8 編譯器做過手腳的函數

8.9 總結

第9章 面向接口程式設計——遵循契約辦事

9.1 接口和抽象類——分工其實挺明确

9.2 接口的應用場景

9.3 接口和函數指針

9.4 函數指針的應用場景

9.5 總結

第10章 if...else 的多面性

10.1 兩條兄弟語句

10.2 if...else 的黑暗面

10.3 開閉原則——if...else 的天敵

10.4 化解if...else 黑暗面

10.5 總結

第11章 挖掘一件神秘武器——static

11.1 static 神秘在哪裡

11.2 static 的特性

11.3 static 的應用場景

11.4 總結

第12章 把容易變化的邏輯,放在容易修改的地方

12.1 一個和使用者的故事

12.2 一個和銷售的故事

12.3 一個和産品經理的故事

12.4 一個和運維的故事

12.5 總結

第13章 隐式約定——猶抱琵琶半遮面

13.1 撥開隐式約定的神秘面紗

13.2 調料包資料

13.3 越簡單的功夫越厲害

13.4 總結

第14章 異常,天使還是魔鬼

14.1 三個江湖派别

14.2 異常的種類

14.3 異常的throw:手榴彈什麼時候扔

14.4 異常的catch——能收炸彈的垃圾筐

14.5 異常的使用技巧

14.6 總結

第15章 多線程程式設計——在混沌中永生

15.1 幾個基礎概念

15.2 互斥——互相競争

15.3 同步——互相協作

15.4 異步——各忙各的

15.5 阻塞與非阻塞

15.6 總結

第16章 單元測試——對代碼庖丁解牛

16.1 單元測試的誕生

16.2 單元測試的進化

16.3 編寫單元測試的基本原則

16.4 如何讓代碼面向單元測試

16.5 最後的忠告:無招勝有招

16.6 總結

第17章 代碼評審——給身體排排毒

17.1 排毒要養成習慣

17.2 磨刀不誤砍柴工

17.3 經驗點滴——關鍵是流程化

17.5 總結

第18章 程式設計就是用代碼來寫作

18.1 程式員與作家的差別

18.2 如何提高寫作水準

18.3 案例解析——咬文嚼字很重要

18.4 謹慎對待注釋

18.5 總結

第19章 程式員的精神分裂——扮演上帝與木匠

19.1 一個腦袋,兩種身份

19.2 上帝模式:開天辟地,指點江山

19.3 木匠模式:緻富隻有勤勞一條路

19.4 總結

第20章 程式員的技術成長——打怪更新之路

20.1 技術成長三部曲

20.2 碼農都是好老師

20.3 重視程式設計效率

20.4 盡量通過工作去鍛煉

20.5 三分之一的工匠精神

20.6 明白架構師的含義

20.7 總結

第21章 語言到底哪種好——究竟誰是屠龍刀

21.1 軍隊的背後是國家實力的較量

21.2 專一和多情哪個好

21.3 如何快速學習一門新語言

21.4 總結

第22章 程式員的組織生産——讓大家更高效和親密

22.1 靈活開發:及時回報,小步快跑

22.2 雙人程式設計:雙人搭配,幹活超累

22.3 封閉開發:并不是蹲大獄

22.4 總結

第23章 程式員的職業生涯——選擇比努力更重要

23.1 程式員到底能幹多久

23.2 程式員的中年危機

23.3 自問一:你适不适合當程式員

23.4 自問二:程式員是否最适合你

23.5 自問三:問問自己有沒有雙門檻

23.6 自問四:程式員最适合轉什麼行

23.7 總結

更多IT圖書盡在

iTuring

繼續閱讀