抽象化是什麼?淺談概念轉換跟降低相依性的程式哲學
寫程式有過一段時間的人,應該或多或少都聽過抽象化這個詞。
但是要談論程式中的抽象化不是一件容易的事,因為抽象化這個名詞本身帶有歧義,當我們在談論不同層面的問題時,它可以具有不同的意思。
這次我想嘗試從概念轉換跟降低相依性兩個方面來討論,因為我認為這是程式抽象化的主要目的。
目次
前言
我第一次聽到抽象化這個名詞是在大學修 Java 的時候,Java 裡有一個保留字 abstract,我們可以用 abstract 來實作抽象類別(abstract class),抽象類別的特徵就是裡面可以包含抽象方法(abstract method),你必須再寫一個 Class 來繼承它,然後完成所有抽象方法的實作。
下面來看個例子:
public abstract class Animal {
abstract void run();
}
public class Dog extends Animal {
public void run () {
System.out.println("I am running!");
}
}
當時,我完全無法理解這是在做什麼,而且也看不出來抽象在哪裡,明明一切看起來都很具體呀?
在我真正弄懂之前,我問過很多不同的人,每個人的說法都不一樣,但結論都偏向這是一種規範或習慣,無法解釋做與不做的差別在哪裡,也不乏有人覺得這麼做很多此一舉。
進入職場工作之後,有次我問了一位好像什麼都懂的同事:「什麼是抽象化?」
他沒有馬上回答我,而是反問我另一個問題:「為什麼在 3ds max 裡做好的模型,匯入 Maya 或 Blender 一樣可以用?」
他的這個問題,是讓我開始瞭解抽象機制為何的一個契機。
什麼是抽象?
如果去問不同的人「抽象是什麼意思?」,我相信會得到很多不同的答案,就像莎士比亞說的「一千個讀者,一千個哈姆雷特。」
但可以肯定的是,抽象並不意味著模糊,這是兩種完全不同的概念。(模糊理論是另一門學問,也被運用於人工智慧。)
經過抽象的事物依然是可以具體描述的,抽象化大多時候只是想轉換事物的概念,或是將一句話的意思變得更廣義(或者說不指涉具體細節)。
比如我說「我要去吃飯」,這時候你不需要知道我是要用筷子吃還是用湯匙吃,也不需要知道我是要吃飯、吃麵,還是吃水餃。
雖然具體細節你都不知道,但是「我要去吃飯」這件事依然能讓你理解我具體要做什麼,這就是一種抽象表達,用較廣義的文字來傳達一件事。
這其實也是一種降低相依性的表現,因為吃飯這個詞彙被抽象化了,所以吃飯可以不指涉具體細節(吃什麼、怎麼吃),只單純表示吃飯的這個概念或事件。
又比如我們會將一群生長於同一個區域的「樹」稱為「森林」,而不會用「很多樹」來形容,這就是一種概念轉換。
因為到底有多少樹才算森林?到底需要有哪些植物才算森林?這個問題或許很難回答,但是當我們說到「森林」的時候,每個人腦中都會浮現相近的畫面。
所以「森林」對我們而言已經是一種抽象概念,它具有另一層意義,不能直接跟「很多樹」畫上等號。
降低相依性
說到降低相依性的例子,我首先會想到 OSI 模型。
OSI 模型是現代網路的基礎架構,它將網路世界依功能性來分類,一共劃分成七層。
之所以要有這種設計,是因為我們希望不要牽一髮動全身。
如果瀏覽器更新了,我們不想為此換一張新的網路卡,如果家裡的無線路由器壞了,我們也不想為此換一條新的網路線。
而對於開發商而言,他們也不想在設計瀏覽器的時候,還需要考慮你用的是什麼網路卡。反過來也是一樣。
這種降低相依性的設計方式,有時候也被稱作解耦合(decoupling)。
將一個巨大的東西拆分成數個部份,並且每個部份都按照約定好的規範來實作,就能讓部件與部件之間不過度相依,而是能像螺絲一樣具有可替換性。
解耦合是實現抽象機制的一種方法,但如果說抽象=解耦合就有點不正確了,因為抽象不一定需要解耦合,解耦合也不一定是為了抽象化。
再者,降低相依性也不是抽象化的唯一目的。
概念轉換
這裡所說的概念轉換,是指不要只關注事物的本質,而是要適時地用自己的認知賦予它另一層意義。(這裡所說的本質,其實也能理解成表相,這取決於你站在什麼維度去看它。)
最容易理解的例子,我想應該是非對稱式加密中的公鑰與私鑰這個概念。
資料加解密其實是一個數學問題,而在加解密過程中所使用的公鑰,其實只是一個數字,私鑰也是。
我們將這兩個數字命名為公鑰與私鑰,將加解密描述成用鑰匙上鎖與解鎖這種貼近日常的概念,於是不懂數學的人也能直觀地理解加解密的運作原理。
進一步說,當我們在討論數位簽章的時候,也可以直接利用公鑰與私鑰的概念來思考,而不用回到底層去重新思考數學問題。
因為有了這樣的概念轉換,我們才能以抽象概念來思考跟管理更複雜的事情,雖然我們眼前看到的是兩個數字,但在我們的認知中它們是兩把鑰匙。
必須要提的一點是,並不是有了抽象概念就表示去瞭解事物的本質變得不重要。
高階概念跟底層邏輯,這兩種觀點都很重要,尤其是當我們面對一個足夠複雜的問題時,一定要適時在兩種觀點間轉換,才能幫助我們學習跟解決問題。
退一步說,隨著計算機的運算速度越來越快,過去使用的加解密方式可能會在短時間內被暴力破解,如果不去理解加解密的本質(數學原理),就有可能誤以為隨便用一種加密方式都是安全的。
這種只理解抽象概念、卻不知其本質的現象也被稱為抽象泄漏。
從高處俯瞰,會看見概念,在低處環視,會發現本質。
我們需要適時地轉換視野。
程式抽象化怎麼做?
抽象化並沒有一定的做法,並不是一定要怎麼寫才叫抽象化,也並非用了 abstract 或 interface 之類的關鍵字,就代表運用了抽象化。
因為與其說抽象化是一種程式寫法,或許說它是一種認知事物的看法會更為合適。
一個人看待一件事物的眼光,會影響他對抽象機制的理解,而他的理解會決定他將用什麼方式進行抽象化的工作。
雖說抽象化沒有一定的做法,但這裡我還是試著分享一個例子。
假設我們要設計一個 SPA網站(Single-page application,中譯單頁式應用),它同時還需要是 CSR(Client-Side Rendering,中譯客戶端渲染),並且我們不使用前端框架(例如 Vue.js、React、Angular),那我們的第一個任務就會是根據當前 URL 的不同,顯示不同的頁面。這在前端框架一般被稱為路由(routing)。
我對路由功能的理解如下:
(1) 當用戶點擊網頁內的連結時,URL 會改變,但網頁不會重導向。
(2) 事先寫好的 javascript 程式監聽到 URL 發生變化,便根據當前 URL 向 Server 請求相應的資源,然後重新渲染畫面。
這是經過簡化的說明,實際上還有一些問題被忽略了,有機會的話再另寫一篇來介紹。
假設 (1) 的部份我們已經做好了,現在來處理 (2) 的部份。(因為 (1) 不是本次重點,若有機會一樣會再另寫一篇詳述。)
我的想法是,我需要有很多員工來幫我完成各類工作,有的人負責請求並渲染文章列表、有的人負責請求並渲染文章內容、有的人負責請求並渲染留言板。
同時,我還需要一位領導者來指揮這些人工作。於是,我寫出了如下的程式片段:
let factoryM = new FactoryM(); // 工廠管理者。
function registerFactorys () {
// 想工作的工廠,須在工廠管理者這裡註冊登記。
factoryM.registerFactory(new ArticleLayoutFactory()); // 文章內容工廠。
factoryM.registerFactory(new ArticleListLayoutFactory()); // 文章列表工廠。
factoryM.registerFactory(new ArticleCommentListLayoutFactory()); // 留言板工廠。
}
// ..............(中略)..............
// 當URL發生變化時觸發此函式。
function changeUrl () {
// 取出URL的path。
let currentUrl = location.pathname;
// 工廠管理者負責通知所有已註冊的工廠,當前頁面的URL。
factoryM.getFactorys().forEach( function (layoutFactory) {
// 每間工廠都根據當前的URL,判斷自己是否需要為它工作,如果要就執行work(),如果不要就略過。
if (layoutFactory.matchUrl(path)) {
layoutFactory.work(path);
}
});
}
熟悉設計模式的人,應該會覺得這其實就工廠模式跟觀察者模式的結合應用。
只不過,我並不是從設計模式的角度出發,只是單純思考要如何用抽象概念來分析問題而已,結果剛好跟設計模式裡提到的概念十分類似。
同一個問題,如果我們不進行抽象思考,一樣可以用簡單的邏輯達成。
// 當URL發生變化時觸發此函式。
function changeUrl () {
let currentUrl = location.pathname;
if (currentUrl == "/articleA") { /* 在畫面上顯示A文章。*/ }
if (currentUrl == "/articleB") { /* 在畫面上顯示B文章。*/ }
if (/^article/.test(currentUrl)) { /* 運用正則表達式,只要URL是以article開頭就執行此程式區段。*/ }
// ......(下略)
}
相信也會有人覺得這樣寫最淺顯易懂。
事實上,這些判斷式在上一個範例裡,是分別寫在每間工廠的 matchUrl 方法中,等於是將這些單純的邏輯進行封裝,換成另一種概念來描述而已。
總結
雖然抽象化帶給我們很多好處,但也不必為了抽象而抽象,如果能在不進行抽象化的情況下就完美解決問題,那會比過度抽象化(過度設計)來得好。
比如你熟悉典型的 23 種設計模式,也不必刻意照著設計模式來寫程式,而是應該專注在眼前的問題上,然後用最貼切的方式來描述你解決問題的思路。
別忘了程式語言也是語言,它是有表達能力的,不能只把它當作邏輯工具。
除此之外,就如同上一節的兩個程式例子,它們之間的優劣只有當自己在開發上遇到問題時才能有所體會,所以在自己遇到瓶頸之前其實不必想太多。
只要在程式設計這條路上走得夠遠,有天自然會出現麻煩來教會你一些事情,而在那之前只要盡情享受程式設計的樂趣就好。(即便未來真的遇上麻煩了,也別忘了要樂在其中!)
希望有幫助到你!
有任何問題歡迎留言討論~