iT邦幫忙

2018 iT 邦幫忙鐵人賽
DAY 22
1
Software Development

函數式編程: 從 Elixir & Phoenix 入門。系列 第 22

再會, model

之前我們不斷的明示暗示 Phoenix 已經不太算是個 MVC framework 了 (雖然某些官方文件還是會用 MVC 這個字) 。最主要的原因,在於 2017 釋出的 1.3 版移除了 model 這個資料夾,而且透過各種方式 (演講、文件、blog) 告訴大家:在 Phoenix 裡,已經沒有 model 這個概念了。~~這看起來就是個絕佳的碎念機會,~~本篇將說明 Phoenix 中 Context 及 Schema 的設計哲學。

如果那個叫玫瑰的其實不只是玫瑰…

之所以要如此大費周章的提醒大家別再提 model 了這件事,理由在於Elixir 是個函數式的語言,Phoenix 採用了與其它 MVC 框架 (特別是 Rails) 相當不同的概念,在看待商業邏輯及資料庫存取相關的事務。

在網頁應用程式裡,我們基本上有四件與資料庫有關的事得做。分別是:

  1. 定義資料表,及如何把資料庫變成定義裡的樣子 。
  2. 定義資料庫的 raw data 與語言裡資料型別的對應,及各種資料彼此間的關係。
  3. 寫入時的驗證規則
  4. 將一系列相關的資料存取包裝成高階的商業邏輯

針對第 1 點,自 Rails 後多數的現代網頁框架,都使用 migration file,資料庫遷移檔來處理。但剩下三件事的責任,在 Rails 中都一概交由 model 承擔。這樣的好處是直覺、容易上手,但是根據沒有免費午餐定理, Rails 的預設值至少付出了這些代價:

  • 藏起來的的資料型別對應

如果你開發過夠大的 Rails application,在部份 model 上方,你會看到這種註釋:

## Ruby

# == Schema Information
#
# Table name: sites
#
#  id                 :integer          not null, primary key
#  content            :json             not null
#  css                :text
#  pages              :json             not null
#  created_at         :datetime         not null
#  updated_at         :datetime         not null

class Site < ActiveRecord::Base
  # ...

甚至還有一個 annotate_models gem 專門在做這件事。基於 Elixir 的明示優於隱含原則,Phoenix 會認為不如就明白的寫下來吧。如果用註釋的話,資料庫變動時忘記更新它就變誤導了。

  • 較難處理複雜的情況

Rails 的 ORM 叫做 ActiveRecord,這名字來自 Patterns of Enterprise Application Architecture 裡的 Active Record pattern。這本書裡,作者在該 pattern 的使用時機是這麼寫的:

Active Record is a good choice for domain logic that isn’t too complex… If your business logic is complex, you’ll soon want to use your object’s direct relationships, collections, inheritance, and so forth. These don’t map easily onto Active Record, and adding them piecemeal gets very messy.

Active Record 在領域邏輯不太複雜時是個好選擇…如果你的商業邏輯較複雜,你很快就會想要使用物件的直接關連、集合、繼承等等。這些都不容易對應到 Active Record 上,如果把這些東西零碎的拼裝上去,事情就會變得很混亂。

諸如著名的 N + 1 問題,null 發生的時機點等等,都算是這個代價的後果。

  • 基於 class 的寫入驗證

由於每個 model 只有一份驗證的定義,如果想要依情況改變驗證的規則時,有時你得要想出各種方式來組合出複雜的驗證條件。而這些 workaround 有時就會回來咬你一口。


負能量差不多快要滿出來,該是要回到主題的時候了。為了要能處理各種簡單到複雜的情況,Phoenix 的策略是善用 SQL都要自稱 full stack 了,不懂基本資料庫概念似乎說不太過去。 它使用的 Ecto 套件,是個設計的很優雅的 SQL 包裝層,與 Rails ActiveRecord 非常不同,概念上反而比較像是 ActiveRecord 的底層: arel

我們把方才說的三件事情拆成三塊,寫在兩個檔案中。 分別是 Context 及 Schema:

Context

Context 負責描述高階的商業邏輯,把一系列的資料庫操作及驗證,包裝成有脈絡的函式,像是 Blog.create_postAccount.login_user 等等。並將各個緊密相關的資料型別的操作放在同一個 Context 裡。

這個做法的目標,是希望你從最初就開始思考,在你的商業邏輯中哪些資料是會放在一起操作的,應該怎麼設計它們之間的關聯。Context ,就只是把一系列獨立但相關的函式放在同一個模組裡,就像是我們在操作 Enum.max/1Logger.info/1 一樣。

但既然命名是電腦科學裡最難的兩件事之一,Phoenix 官方文件也這樣建議:

如果當系統結構還不明確,你一直想不到好的 context 名稱時,就直接把 context 取名為 resource 的複數,例如 Users context 用來管理 users。等到你的系統逐漸成長,有些概念變得愈來愈明顯時,再來把 context 改成更精確的名稱。

而每個 context 裡的函式,就是操作一系列的 schema 函式,依商業邏輯來組合查詢、驗證及寫入的流程。Context 的內容大概像是這樣:

defmodule HelloPhx.Blog do
  import Ecto.Query, warn: false
  #...
  def list_posts do
    Repo.all(Post)
  end

  def get_post!(id), do: Repo.get!(Post, id)

  def create_post(attrs \\ %{}) do
    %Post{}
    |> Post.changeset(attrs)
    |> Repo.insert()
  end
  #...

Schema

Schema 這個名字來自資料庫的 table schema。正如之前所說,這跟 Rails 裡的 schema.rb 完全不是同一個東西。在這張檔案裡會有兩個區塊,第一塊是資料表欄位與資料型別的對應,接著是一些小函式,例如寫入時的各種不同驗證方式。

defmodule HelloPhx.Blog.Post do
  use Ecto.Schema
  # ...

	# 資料表欄位對應區
  schema "posts" do
    field :content, :string
    field :draft, :boolean, default: false
    field :title, :string

    timestamps()
  end

  # 函式區,這個是預設的驗證方式
  def changeset(%Post{} = post, attrs) do
    post
    |> cast(attrs, [:title, :content, :draft])
    |> validate_required([:title, :content, :draft])
  end
end

由於每張 schema 可以定義多個不同的驗證方式,並在 Context 裡決定什麼時候用什麼方式來驗證,在情況複雜時也可以很優雅的處理。

ScreamingArchitecture

當然概念上更重要的就是我們在之前的章節提到 Uncle Bob 所寫的 Screaming Architecture。把商業邏輯拆出來放在 lib/專案名稱 下,而非 _web 區塊裡,並用 Context 將各個脈胳下的區塊拆分開來,你可以很容易掌握這個應用程式的核心組件及整體概念。而獨立在 _web 資料夾以外,代表你可以很容易的把這些商業邏輯獨立出去,變成一個 Elixir project,接著用 Erlang Umbrella project 的方式 (包含多個 Application 的 Application) 或其它方式來使用這個組件。

只想生成一部份

如果你只想要生成 View 及 Controller,也可以在生成模版時加上 --no-context 或是 --no-schema,只生成 View、Controller 等等,再自行實作資料庫存取的部份。

總結

  • Phoenix 自 1.3 起鼓勵不要用 model 的方式思考
  • 而是用 Context 來將系統劃分成幾個會互動的大區塊
  • Schema 則是用來定義資料型別、驗證方式及其它小函式。

今天幾乎都只在概念的部份談設計的哲學。下一篇將要來深入看一下讓這些想法成真的函式庫: Ecto。

Happy hacking! 明天見。


上一篇
沒有很 thin 的 Controller
下一篇
不是 ORM,但是更好用: Ecto
系列文
函數式編程: 從 Elixir & Phoenix 入門。31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

0
taiansu
iT邦新手 4 級 ‧ 2018-01-11 18:49:55

已更新

我要留言

立即登入留言