嵌入式文件

2013 年 6 月 4 日

這些日子,我愈來愈常看到 JSON 資料結構流經伺服器。JSON 文件可以直接儲存,方法是使用 AggregateOrientedDatabase 或關係資料庫中的 序列化 LOB。JSON 文件也可以直接提供給網路瀏覽器,或用於將資料傳輸至伺服器端頁面渲染器。當 JSON 以這種方式使用時,我聽見人們說,使用物件導向語言會造成阻礙,因為 JSON 需要轉譯成物件,然後再重新渲染出來,這浪費了程式設計的功夫 [1]。我同意浪費這一點,但我認為這不是物件的問題,而是不了解封裝所致。

想像一下,我們將訂單儲存為 JSON 文件,並透過輕微的伺服器端處理提供服務,同樣以 JSON 呈現。範例文件可能是這樣。

{ "id": 1234,
  "customer": "martin",
  "items": [
    {"product": "talisker", "quantity": 500},
    {"product": "macallan", "quantity": 800},
    {"product": "ledaig",   "quantity": 1100}
  ],
  "deliveries": [
    { "id": 7722,
      "shipDate": "2013-04-19",
      "items": [
        {"product": "talisker", "quantity": 300},
        {"product": "ledaig",   "quantity": 500}
      ]
    },
    { "id": 6533,
      "shipDate": "2013-04-18",
      "items": [
        {"product": "talisker", "quantity": 200},
        {"product": "ledaig",   "quantity": 300},
        {"product": "macallan", "quantity": 300}
      ]
    }
  ]
}

我們假設沒有太多伺服器端處理工作要做,但還是有一些。我們也假設我們使用的是 OO 語言。一個天真的做法可能是讀取 JSON 文件,將資料轉換成適當的物件圖形(包含訂單、品項和交貨),套用任何處理,然後將物件圖形序列化為 JSON 提供給客戶端。

在許多這種情況下,更好的做法是將資料保留在 JSONish 形式,但仍用物件包裝起來,以協調操作。大多數程式設計環境提供通用函式庫,可以取得文件並將其反序列化為通用資料結構。因此,JSON 文件會反序列化為清單和字典的結構,XML 文件會反序列化為 XML 節點的樹狀結構。然後,我們可以取得這個通用資料結構,並將其放入訂單物件的欄位中,以下是一個使用 Ruby 和 JSON 的範例。

class Order...

  def initialize jsonDocument
    @data = JSON.parse(jsonDocument)
  end

當我們想要操作資料時,我們可以像往常一樣在物件上定義方法,並透過存取這個資料結構來實作它們。

class Order...

  def customer
    @data['customer']
  end
  def quantity_for aProduct
    item = @data['items'].detect{|i| aProduct == i['product']}
    return item ? item['quantity'] : 0
  end

這包括具有更複雜邏輯的情況。 [2]

class Order...

  def outstanding_delivery_for aProduct
    delivered_amount = @data['deliveries'].
      map{|d| d['items']}.
      flatten.
      select{|d| aProduct == d['product']}.
      inject(0){|res, d| res += d['quantity']}
    return quantity_for(aProduct) - delivered_amount
  end

內嵌文件可以在傳送給客戶端之前進行豐富化。

class Order...

  def enrich
    @data['earliestShipDate'] = 
      @data['deliveries'].
      map{|d| Date.parse(d['shipDate'])}.
      min.
      to_s
  end

如果需要,您可以在內嵌文件的子樹上建立類似的物件。

class Order...

  def deliveries
    @data['deliveries'].map{|d| Delivery.new(d)}
  end

類別 Delivery

  def initialize hash
    @data = hash
  end
  def ship_date
    Date.parse(@data['shipDate'])
  end

這裡要注意的一點是,此類物件封裝器與一般物件並不完全相同。上述程式碼片段中傳回的傳送物件,其相等語意與您從以更常見結構排列的物件中預期的不同。

儘管比較少見,內嵌文件非常適合物件導向。封裝資料的重點在於隱藏資料結構,因此物件使用者並不知道或不關心訂單的內部結構。

熟悉函式程式設計的人會認出將通用資料結構傳遞至一系列函式的風格 - 您可將物件視為提供一個用於處理通用資料結構的命名空間。

內嵌文件的最佳應用時機是,當您以從資料儲存取得的相同格式提供文件,但仍想對該資料進行一些處理時。如果您不需要存取 JSON 文件的內容,則甚至不需要將其反序列化成通用資料結構。訂單物件只需要一個建構函式和一個用於傳回其 JSON 表示形式的方法。另一方面,當您對資料進行更多工作時 - 更多伺服器端邏輯,轉換成不同的表示形式 - 那麼值得考慮是否將資料轉換成物件圖表會比較容易。

備註

1: 有些人可能會認為這也是一種浪費運算能力 - 儘管我對此感到驚訝,如果這很重要。除非有測量結果,否則我肯定不會接受反對轉換成物件圖表的效能論點 - 就像任何效能論點一樣。

2: 請注意此方法中集合管線的串連。我個人最討厭聽到一些函式程式設計愛好者說這種程式碼風格不是物件導向。儘管這可能對具有 C++/Java 背景的人來說很陌生,但這種風格對 Smalltalk 使用者來說完全是自然的。