蒼時弦也
蒼時弦也
資深軟體工程師
發表於

寫好程式的方法 - 加入程式的脈絡

對脈絡(Context)比較有清晰的概念是在今年研究 Domain-Driven Design 的時候,這幾天剛好因為工作需要修正 Golang 的靜態分析警告,想到這其實也跟脈絡有些關係,因此這篇文章會來跟大家分享幾種常見的「程式脈絡」

什麼是脈絡

脈絡,在許多技術文章中我們也會用「上下文」來稱呼,簡單來說就是「背景資訊」例如一段「加入購物車」的程式碼,我們會有著「購物車存在」這樣的前提。

要對程式的脈絡有所理解,就需要知道「資料(Data)」和「資訊(Information)」的差異,在英文字典中,資訊的解釋是「the data with context(有脈絡的資料)」這樣的定義,那麼當我告訴你「繼續」的時候因為缺乏上下文你會不清楚要「繼續什麼?」然而有了脈絡,像是「在紅燈停下來,接下來繼續前進」就會知道可能是在路上行駛中。

程式語言的脈絡

其實程式語言的設計中,也會使用 Context 這個單字。像是作業系統中的 Context Switching 或者程式語言的原始碼都很常看到這個單字,對於使用這些程式語言開發的工程師,最常見的應該就是函式(或者方法)本身。

 1def context_a
 2  val = true
 3end
 4
 5def context_b
 6  val = false
 7end
 8
 9context_a
10context_b
11# val => nil

因為兩個函式有著各自的上下文,因此變數並不會互相影響,而呼叫他們的最外層(主程式)也有自己的上下文,因此無法存取到他們。

這也是為什麼我們需要利用參數(Parameter)來傳遞資訊,或者使用全域變數(Global Variable)來突破上下文的限制。

程式碼區塊的脈絡

除了每一個能夠成「上下文」的區段之外,一整段的程式碼也是有其脈絡存在的,這也是我在修改 Golang 靜態分析警告遇到最多的狀況。其實就是經常被爭辯的「空行」和「變數聲明」的使用,雖然在不同語言會有細微的差異,然而整體上來說還是具備其特性的。

 1def pay_to(order:, credit_card:)
 2  payment = Payment.new(order: order)
 3  payment.use(:credit_card)
 4
 5  service = CreditCardService.new(credit_card)
 6  service.authorize!(payment)
 7  service.capture!(payment)
 8
 9  service.update(order) # => order.paid
10end

以上面的例子,我們可以看到三個以上的脈絡資訊。

  • pay_to() 表示了跟付款相關的邏輯,參數 ordercredit_card 表示了可能跟電商有關,而且使用信用卡付款
  • payment.use(:credit_card) 的段落明確的聲明「使用信用卡」
  • CreditCardService.new 的段落描述了透過某張信用卡,對某筆付款進行扣款的情報
  • service.update(order) 的段落則是根據付款的結果,對訂單進行更新

假如我們沒有留下空行在這幾個段落之間,那麼要理解這裡有著「三階段的小步驟」就會變得困難。另一方面,如果把 paymentservice 都放到方法的開頭一次性的聲明,也會讓理解後續的「這個變數從何而來」的難度增加。

物件的脈絡

小到一行程式碼,大到一整個系統都會有脈絡存在。也因此在 Domain-Driven Design 之中定義了「領域模型」跟「分層架構」的觀念,就是要讓我們專注於「物件的脈絡」這一件事情上。

以 Value Object(值物件)來說,他是一種「資訊」也就是具備脈絡的資料,因此我們在談論「金錢」的時候,會用「幣種」加上「總數」作為一個「情報單位」

 1class Money
 2  attr_reader :currency, :amount
 3
 4  def initialize(amount, currency:)
 5    @amount = amount
 6    @currency = currency
 7  end
 8
 9  def +(other)
10    raise ArgumentError, 'invalid currency' unless other.currency == currency
11
12    Money.new(amount + other.amount, currency: currency)
13  end
14
15  # ...
16end

因此我們就可以很清楚的知道處理的是金錢,而且在「幣種錯誤」的狀況下無法進行換算,近一步延伸出去,還可以討論到像是 Clean Architecture 的單一職責、MVC 框架的補充 DCI(Data Context Interaction)等等不同的議題,而一個系統的「架構」也是基於脈絡所構成,因此當我們在實作一個功能時沒有考慮到脈絡,就會讓整個系統變得難以理解。

用脈絡設計系統

假設我們用「電商網站」作為一個系統的開發目標,在沒有脈絡的狀況下我們只會將各種「電商」要素加入到這個系統中,然而有脈絡之後,可能會更清楚的知道「這是一站式網站」「使用者只會購買一種商品」等等資訊,那麼在設計的過程中就會更加清晰。

這也是實踐 Domain-Driven Design 過程中會發現的一個面向,因為我們想要畫分出 Bounded Context(上下文邊界)其實就是在整個大的系統中,繼續細分到一個「恰當的」資訊群組來一起處理這些問題。

從產品的角度 UX(使用者體驗)的使用者研究,也是在尋找「行為」上的脈絡,我們從不同的來源搜集情報並且加以彙整,最後就能找到一條符合產品需要的脈絡,進而實現更加「正確」的應用軟體。

這樣來看,整個程式的設計跟撰寫「小說」其實是一樣的,我們需要定義角色、場景、物件等等不同的資訊資訊,然後讓他們在舞台上互動,只不過換成軟體後變成用「物件」去定義出整個故事的要素,然後讓他動起來。