iT邦幫忙

2

Week34 - 從 JavaScript 到 Golang 的啟發之旅 [Server的終局之戰系列]

本文章同時發佈於:


大家好,這篇文章主要是六角學院鐵人賽與 2020 iT 邦幫忙鐵人賽對於 JavaScript 到 Golang 所理解的心得大彙整。

並且此文章也有在2020-MOPCON-10/24(六)-3:00 的交流議程(UnConf)以演講的方式發表,簡報在此


JavaScript 是一個動態弱型別的語言,起初在知道 Golang 是一個靜態強型別後,身為 JavaScript 工程師的我一直以為學習 Golang 會是個巨大的挑戰,但在學習的過程中,我常常驚訝

Golang 這做法真 JavaScript

在我的經驗上我發現,如果你是從 JavaScript 的許多地雷一路學習到 Golang,學習 Golang 並不太像學一門完全的語言,而是像學習一門解決 JavaScript 眾多雷的語言。

所以,這篇文章將介紹這些經驗,如果你也是 JavaScript 工程師,希望這些經驗可以用 JavaScript 地雷的例子讓你了解到 Golang 為什麼這樣設計此設計的精神為何,使學習 Golang 更加得順暢。

JavaScript 設計的理念

JavaScript 是一個 Object Oriented Programming(OOP) 與 Functional Programming(FP) 混合的 Hybrid 語言,目標就是簡單、易用、彈性

JavaScript 之父 Brendan Eich 在設計此語言時,是個網頁只能瀏覽,無法用程式設計邏輯的時代,

圖片來源: Javascript 继承机制的设计思想

這導致連檢驗輸入字串是否是email格式都無法透過瀏覽器辦到,於是 Brendan Eich 借鑒了Scheme這門 FP 語言可傳遞 function 的First Class概念來設計了 JavaScript,可輕鬆傳遞處理 function 給 EventListener 使得檢驗 email 格式的 code 變得更簡單更短,

document.querySelector('input').addEventListener('input', e => {
  alert(e.target.value)
})

但當時也是 Java 大紅大紫的年代,Brendan Eich 的公司網景希望要像Java,於是 Brendan Eich 考慮是否要加入 Java style 的 Class,但後來又覺得這樣的 code 太複雜,

如果你只寫過 JavaScript,那對 Java 的 Android EventListener 應該感到很是複雜,

public class MainActivity extends ActionBarActivity implements View.OnClickListener {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      setContentView(R.layout.activity_main);
    }

    @Override
    public void onClick(View v) {
      Toast.makeText(this, "clicked", Toast.LENGTH_SHORT).show();
    }
}

所以 Brendan Eich 借鑑Self這門沒有 Class 的原型 OOP 語言,設計出有以下幾種特性的 JavaScript OOP:

  • 新物件可由另一個物件複製出來,並且因為原型鏈指向原本的屬性
  • 物件不受到 Class 規範,可以隨意擴充屬性

如果有一個 a 物件擁有 z function,b 物件由 a 複製出來後,b 物件的 z function 並沒有做任何修改,那呼叫b.z()的時候實際上會因為原型鏈呼叫到a.z()

而物件在設計到後期後,臨時要加屬性在物件上,也可以動態加入。

這使得一切都極為彈性~但...

Object 我真的猜不透你啊

我曾經遇過類似以下的 express.js server code

const controller = function (req, res, next) => {
  try {
    result = a(req)
    result = b(result)
    result = c(result)
    result = d(result)
    result = e(result)
    result = f(result)
    result = g(result)
    result = h(result)
    result = i(result)
    res.json(result.toJSON())
  } catch (err) {
    res.status(500).send('Get error')
  }
}

這時候,同事過來問我,

同事: 嘿 York,`result.toJSON()`最後被送出時怎麼會爆炸了?
我: 我怎麼知道 orz

你會發現,result裡頭的屬性可以被動態增減這道彈性,使得result的變化根本無法預期,你可能會在 debug 兩三個小時後,發現e function裡的一行 code 改了toJSON()的行為,又或者把回傳的 object 誤換成了 string。

所以動態增減屬性在雜亂的 code 中是有害的

除此之外,如果在程式run起來之前,res.json()可以檢查到result.toJSON()丟進來的東西根本不對啊!那程式出錯的機會就會更少,這使得我們必須要規範result到底有哪些 function,如果沒有這些 function 或是 function 的輸入值與回傳值不對,就要在程式run起來之前與開發者說你這段 code 不符合規範啊。

在雜亂的 code 中無法規範 input/output 的介面是難過的

規範過於彈性的 code

首先,我們先以 JavaScript 來舉一個 Call API 的例子,在定義好了caller這個 object 後,我們把他傳入callAPIfunction 中呼叫.get() method。

function callAPI(caller) {
  caller.get("https://api");
}

const caller = {
  header: 'header',
  get(URL) {
    console.log(`${URL} with ${this.header}`)
  }
}
callAPI(caller)
// 印出 "https://api with header"

一切都挺好,直到有新來的同事將 caller 的屬性亂改了一套,

function callAPI(caller) {
  caller.get("https://api");
}

const caller = {
  header: 'header',
  get(URL) {
    console.log(`${URL} with ${this.header}`)
  }
}
caller.header = 123
caller.get = undefined
callAPI(caller)

於是

你可能會想怎麼可能做這種蠢事,這例子的確比較簡單粗暴,但實際上,在龐大的程式碼中誤改了一段 code 的事時常發生。

而如果是 Golang,他會怎麼做呢?

package main

import "fmt"

func callAPI(caller Caller) {
  caller.Get("https://api")
}

type Caller struct {
  header string
}

func (c Caller) Get(URL string) {
  fmt.Println(URL + " with " + c.header)
}

func main() {
  callAPI(Caller{header: "header"})
}

嗯?怎麼好像新增的 code 不多,Golang struct 就像 JavaScript 被規範成員的 object,而 struct 中的 function 則是用 Golang 的receiver function來實作,receiver function一旦設定好後,就無法在動態修改。

不過,如果callAPI()要傳入其他具有.Get()的 struct 時,我們就需要某些方案來解決,而這個方案就是多型

今晚我想來點多型

多型是純 JavaScript 開發者很少聽到的詞,主要是因為 JavaScript 沒有物件型態,所以也不用特別去找出不同 object 相似的 function,

以剛剛的例子來說我們又做出了三個 caller object 時,JavaScript 根本不管這幾個 object 是否擁有.get(),總而言之 object 都讓你傳進來,要爆炸的話那是runtime時的事情,

callAPI(caller)
callAPI(callerB)
callAPI(callerC)

但 Golang 再加以規範 code 時,限制了callAPI()只能傳入Caller struct,這時就無法傳入CallerB structCallerC struct

我們不妨回頭想想,如果要傳入callAPI()要符合什麼規範,有了!就是擁有.Get()function,

所以 Golang 提供了interface這項功能,你可以把 struct 所擁有的共同 function 定義出來,只要傳入的 struct 有此 function 就可以傳入,

package main

import "fmt"

type GetHandler interface {
  Get(string)
}

func callAPI(getHandler GetHandler) {
  getHandler.Get("https://api")
}

type Caller struct {
  header string
}

func (c Caller) Get(URL string) {
  fmt.Println(URL + " with " + c.header)
}

type CallerB struct {
  header string
}

func (c CallerB) Get(URL string) {
  fmt.Println(URL + " with " + c.header)
}

func main() {
  callAPI(Caller{header: "header"})
  callAPI(CallerB{header: "header"})
}

透過type GetHandler interface所定義的介面,使得傳入(getHandler GetHandler)的 struct 都要擁有.Get(string)function,而CallerCallerB都符合,所以都可以傳入使用。

至此我們發現,Golang 跟 JavaScript 一樣都在追求簡單、易用、彈性,Golang 的 function 也擁有First Class的特性,使得傳遞邏輯不需要再包裝成一個物件,讓 code 更短更簡單,為了單純 Golang 也不使用經典 OOP 常出現的 Class。

為什麼 Golang 不想要 Class

Golang 算是 OOP 嗎?官網的回答Yes and no,官網認為 interface 這種行為判斷的多型,會比 class 以階層判斷的多型來得輕量許多,行為階層是什麼意思呢?以 Java 來說:

interface Caller {
    public void call();
}
interface OtherCaller {
    public void call();
}

class ACaller implements Caller {
    public ACaller() {
    }

    public void call() {
        System.out.printf("Call API A");
    }
}

class BCaller implements Caller {
    public BCaller() {
    }

    public void call() {
        System.out.printf("Call API B");
    }
}

class CCaller implements OtherCaller {
    public CCaller() {
    }

    public void call() {
        System.out.printf("Call API C");
    }
}

public class Main {
    public static void main(String[] args) {
        doRequest(new ACaller());
        doRequest(new BCaller());
        // 爆炸!雖然行為相同的介面不同!
        doRequest(new CCaller());
    }

    public static void doRequest(Caller caller) {
        caller.call();
    }
}

雖然ACallerBCallerCCaller都有.call()function,但由於CCaller來自OtherCallerinterface 實作,所以會無法傳入doRequest(),要傳入doRequest()一定要是來自Callerinterface 實作,

換句話說,CCaller的實作的上層不是Callerinterface 就無法傳入,

在 Uncle Bob 的 Clean Architecture 一書中,他認為 OOP 中的封裝繼承多型中,多型是最具代表性與實戰效果,封裝繼承事實上不限定於在 OOP 語言出現前就可以做到,

Java class 在實現上述三個特性很方便,但因為 class 嚴謹的規範造成如上功能實現的麻煩,而 Golang 將多型設定為整體 OOP 重點考量之一,而不侷限在利用 class封裝繼承的思維中。

多型的下一步,控制反轉(DIP)

如果將整個系統呼叫每層的 interface 定義出來,並將每層以此 interface 注入至下一層,那系統將不再被底層綁架

聽起來有點抽象,但實際上就是在描述剛剛 Golang 的範例:

package main

import "fmt"

type GetHandler interface {
  Get(string)
}

func callAPI(getHandler GetHandler) {
  getHandler.Get("https://api")
}

type Caller struct {
  header string
}

func (c Caller) Get(URL string) {
  fmt.Println(URL + " with " + c.header)
}

type CallerB struct {
  header string
}

func (c CallerB) Get(URL string) {
  fmt.Println(URL + " with " + c.header)
}

func main() {
  callAPI(Caller{header: "header"})
  callAPI(CallerB{header: "header"})
}

我們將 GetHandler 的 interface 定義出來,並將每個 Caller 以此 interface 注入至 callAPI(),只不過在系統架構上我們習慣把這些 code 稱為

假設Caller都是一個 library,當有天Caller停止更新了,必須換成CallerB,這時我們只要確保CallerB也符合 interface 即可,並不會直接更改callAPI()的實作。

JavaScript 沒有 interface 的幫忙,開發者要有自知的將不同層分離,由於彈性與時程的關係,常常會忽略分層的重要性,導致某些專案我一打開:


控制反轉的實作 Clean Architecture

了解了以上概念後,最後可以了解我寫的這三篇文章,我竭盡所能地將 Clean Architecture 以簡單的方式表達 XD。

參考


圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言