築夢角落

致力於用最生活化的例子讓所有人都能懂程式,也喜歡分享動漫、小說心得,以及自己的所見所聞、所思所想。

抽象化是什麼?淺談概念轉換跟降低相依性的程式哲學

最後更新時間 : 2022-10-18 | viewed : 1405

賦予0跟1的組合一個意義,稱它們為檔案。

 

寫程式有過一段時間的人,應該或多或少都聽過抽象化這個詞。

但是要談論程式中的抽象化不是一件容易的事,因為抽象化這個名詞本身帶有歧義,當我們在談論不同層面的問題時,它可以具有不同的意思。

這次我想嘗試從概念轉換降低相依性兩個方面來討論,因為我認為這是程式抽象化的主要目的

 

目次

 

前言


我第一次聽到抽象化這個名詞是在大學修 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 種設計模式,也不必刻意照著設計模式來寫程式,而是應該專注在眼前的問題上,然後用最貼切的方式來描述你解決問題的思路

別忘了程式語言也是語言,它是有表達能力的,不能只把它當作邏輯工具

除此之外,就如同上一節的兩個程式例子,它們之間的優劣只有當自己在開發上遇到問題時才能有所體會,所以在自己遇到瓶頸之前其實不必想太多

只要在程式設計這條路上走得夠遠,有天自然會出現麻煩來教會你一些事情,而在那之前只要盡情享受程式設計的樂趣就好。(即便未來真的遇上麻煩了,也別忘了要樂在其中!)

 

希望有幫助到你!

有任何問題歡迎留言討論~

 

 
我要留言!
 

X
暱稱(選填)
email(選填,僅站長可見。)
留言 To:#