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。

参考


<<:  38.vue.config.js

>>:  【图解演算法教学】【Tree】二元树遍历 vs QuickSort

DAY29 欸你Git来Hub一下

昨天提到先将本机的档案列为版控,但是光在本机这样操作还是不太够,其他人要一起共同开发的时候,还是一样...

Youtube Reports API 教学 - 告一个段落

「鲑鱼均,因为一场鲑鱼之乱被主管称为鲑鱼世代,广义来说以年龄和脸蛋分类的话这应该算是一种 KNN 的...

Day17 - 帮蛇多加了暂停与继续

class Game{ startGame () { this.snake = new Snake(...

[Day 18] Leetcode 1328. Break a Palindrome (C++)

前言 今天来做九月每日挑战的今天这题1328. Break a Palindrome。这题不是考验程...

Ubuntu巡航记(4) -- Rust 安装

前言 Rust 是一个现代版的 C/C++ 程序语言,它加入物件导向、套件安装(cargo)、函数式...