現在很多框架都內建active record pattern來幫助開發者建立application。
乍看之下很自然,實際上它卻違反了一大堆傳統的軟體開發知識。
這會讓工程師在學習時陷入下列困惑:
為甚麼一堆書上的原則(principles)、模式(patterns)、手法(practices),都無法在實務上應用?
接著導致一個更嚴重的認知失調:
是我的開發方法不對嗎?還是這些傳統知識全部過時了?
事實上,active record pattern還真的跟傳統開發知識充滿衝突。
為了點出那些衝突,這篇文章會從傳統開發知識的角度,對active record pattern提出多項批評。
什麼是Active record pattern?
從Rails的ActiveRecord,到Laravel的Eloquent,許多框架都內建active record pattern。
各框架對active record pattern的實作都很華麗。除了直接操作資料庫、充當domain object的功能之外,還提供relationships、自動更新created_at/updated_at欄位等等多種功能。
一般認為active record pattern這個名字由Martin Fowler所發明。
根據他的定義,一旦類別同時處理兩件事情:
* data access logic(處理資料庫相關操作)
* domain logic(處理app的商業邏輯)
就算是實作了active record pattern(下文簡稱AR)。其他華麗的功能都是框架額外提供的。
AR會讓物件同時「提供資料給別人用」與「提供行為給別人呼叫」。
正是因為這點,就傳統開發知識來看,至少可以對AR做出13種批評。
下面會將這些批評分成3種層面來看,然後一一列舉它們:
* 原則層面(Principles)
* 模式層面(Patterns)
* 手法層面(Practices)
(注意:許多批評其實是指同一件事,只是換個層面,或是換個方法描述而已。)
原則層面 (Principles)
1. 違反物件導向(OOP)精神
物件應該將資料對外隱藏,將行為對外開放(hidden data, expose behavior)。
而承載資料的資料結構會將資料對外開放,並且不具有行為(expose data, no behavior)。
AR物件因為同時身兼兩者,導致你從根本上面臨矛盾。
你永遠抓不清分際何在:該讓它所有properties都對外隱藏嗎?還是讓它一個method都沒有?分別該做到什麼程度呢?
// Should I do it this way? $totalPrice = $order->price + $order->tax; // Should I do it this way? $totalPrice = $order->getTotalPrice();
2. 違反Single Responsibility Principle
AR負責了data access logic,導致資料庫schema一改,程式碼必須跟著修改。
AR負責了domain logic,導致app的商業邏輯一改,程式碼必須跟著修改。
OOP知名五大原則SOLID中的SRP被AR直接打破:一個類別應該因為且只因為一個理由而修改。
3. 違反「Tell, don’t ask.」原則
「Tell, don’t ask.」原則指引大家OOP開發應該「告訴物件去做什麼」而不應該「跟物件拿資料出來操作」。
偏偏AR物件是如此方便易用,你幾乎一定會從外部直接取用它的資料。
// This may appear in controllers or service objects. $totalPrice = $oder->price + $order->tax; if ($totalPrice > 1000) { //... }
4. 導致Database Oriented Development
應該圍繞著產品本身領域知識去開發的domain driven/business oriented開發方法,變成以database為中心在開發。
開發者在開發過程中,不再對領域知識念茲在茲,而是滿腦子想著database schema。
如果使用的Active Record Pattern甚至實作了toJson這類的函式來幫你把entity內容轉成JSON格式的話,下場就更誇張了:連front-end的開發人員都得跟著滿腦子database schema。前後端分離程度更低。
5. 導致測試時碰到資料庫
傳統上會說測試時應該只測商業邏輯,別碰資料庫,因為碰到資料庫會跑很慢。而且兩者應該分開來測。
但是用了AR會讓測試很難不碰到資料庫。
框架甚至直接提供fixtures、Model Factories來協助這件事,毫不避諱。
模式層面(Patterns)
6. 無法用Constructor Injection
你無法實作這個dependency injection模式來設計類別:
// You can't do this with AR. $order= new Order(new ComponentA(), new ComponentB());
因為你的建構式被拿來處理data access logic,因而放棄domain logic了。
7. 導致God object
建出來的entity,會很容易把code一直往裡面放,最後變成上千行、負責一堆功能的God object:
$order->iCanDoThis(); $order->iCanDoThat(); $order->iCanDoEverything();
也就是Rails社群流傳的「Fat model, skinny controller」。
8. 導致「貧血的領域模型」(Anemic Domain Model )
如果你為了避免entity內含過多商業邏輯而將code抽離到別的地方(例如:做了一堆service objects),那很容易變成相反的狀況:entity本身幾乎不含什麼code。
例如這樣:
// The entity now can't do anything. // So anemic, so poor. /* $order->iCanDoThis(); $order->iCanDoThat(); $order->iCanDoEverything(); */ // They are all in the services instead. $service = new DoThisService(); $service->handle($order);
9. 做不出Rich Domain Model
理想狀況是隨著app的商業邏輯越變越複雜,你的domain model會因為各種分工(由多個物件來對外負責各種behavior)而越變越豐富,最後成為漂亮的rich domain model。
非常遺憾的,因為使用了AR的關係,類別的設計被綁死在schema上,因此你很難做出rich domain model。
你要嘛會做出God object,要嘛會做出Anemic domain model。
也許會有人嘗試將data access logic與domain logic分離來克服…
奉勸千萬別這麼做:根本是放棄AR的優點去硬搞,最後變成四不像。
10. 質變的Repository Pattern
將CRUD操作(persistence logic、query logic)封裝的經典模式repository pattern,因為AR而產生質變。
舉例來說,你可能看過有人這樣設計repository class:
class OrderRepository { public function save($order) { $order->save(); } // blah blah }
接著將repository用dependency injection放進controller或是service object內。
乍看之下沒問題:確實因此可以在測試時抽換repository object成測試專用的object。
但應該「負責persistence logic」的repository,實際上居然只是叫參數物件自己save自己。
這可稱不上優雅。
質變後的repository退化成只負責查詢功能的query object。
11. 質變的Domain Driven Design
Eric Evans寫的Domain-Driven Design一書教導了一種圍繞領域知識的軟體開發方法。
很悲慘的,如果你用了AR,這本書你將很難看得懂,也很難應用。
DDD從最基本的Repository Pattern就已因AR導致質變(參考10.),再加上entity間的關係通常會被AR直接根據schema建好,Aggregate也變得無法封住內部物件,更不用說domain model根本建不好了(參考9.)。
手法層面(Practices)
12. 濫用public properties
OOP課本翻沒幾頁就會提到getter/setter的觀念,並且反對直接把properties設成public。
AR因為同時負責data access logic,很自然會讓人直接對properties操作:
$order->price = 1000; $order->is_sold = true;
13. Atomic operation困境
為了確保Database內的資料正確性,有時你需要用到lock機制或是transaction機制。
試問下列這兩行code該放哪呢?
LOCK TABLES orders WRITE, items WRITE, users WRITE
與
UNLOCK TABLES
放在entity的話,代表Order/Item/User其中一個類別會直接操作其餘兩個類別 => 容易導致god object。
不放在entity的話,就必須放在外部(例如service object或controller) => SQL語法不再被封裝在底層,也不被封裝在entity內,而外洩到更上層了。
結論
Active Record Pattern方便、快速開發的特性,讓它具有非常高的商業價值。
這也是Rails之後一直到Laravel等等各大框架都實作它的原因。
愛用Active record pattern跟反對active record pattern的人常常有所爭論,幾乎成了現代開發vs傳統開發方法的戰爭(例一、例二)。
使用它來開發沒什麼問題,但千萬要留心它與哪些傳統開發知識有衝突。
享受它的優點,理解它的缺點,才能在現代和傳統開發知識的衝突上,找到屬於自己的平衡點。
(完)
下面是我在公司新開發的一個討論plugin。歡迎用它來反映您對AR的看法。
順帶一提,server-side是用Laravel 5所寫成:背後的Eloquent,正是實作active record pattern。
(Photo via Don McCullough, CC licensed.)