接下來要這篇文章要來談談很常聽到的『 Active Record 』。
根據 《 Patterns of Enterprise Application Architecture - Martin Fowler 》書中的定義如下 :
An object carries both data and behavior.
對,基本上就是在 DataSource 層裡,然後每個實體代表一個 table row,且實體內包含資料與行為 ( domain 邏輯 ) ,這和上一章的 RowDataGateway 很像,都是它缺了行為。下圖就是書中的範例物件 UML 圖。沒看錯。
圖片來源: 《 Patterns of Enterprise Application Architecture - Martin Fowler 》
我覺得不是,我覺得作者本意不是,不然為什麼還要有 Domain 層呢 ? 並且還將 Active Record 放在 DataSource 層,我覺得他應該是指將一些簡單的 domain logic 放在 active record 中,例如下述範例中的 isPass。
class StudentTest{
id: string
stuentId: string
score: number
isPass(): boolean{
return score >= 60;
}
}
作者只建議在以下的情境下使用 :
並且他還要說到一句話,它事實上沒有以上的情況不要用 :
Another argument against Active Record is the fact that it couples the object design to the database design. This makes it more difficult to refactor either design as a project goes forward
簡單的說就是會讓物件設計與資料庫設計綁在一起。
目前我常聽到的,基本上有使用 Active Record Pattern 來實作的有以下幾個 :
還記不記得上一章節的 RowDataGateway,它每個實體都代表資料庫中的一行,而 query 因為有時會回傳多個,所以他拉出一個叫『 Finder 』的東東,但在 Active Record 範例中我到沒看到這個東西呢… 那他多筆資料的 query 也是寫在 Active Record 嗎 ?
然後看一下實作的 framework 例如 Laravel ,看起來就是丟在裡面。
$flight = App\Flight::where('active', 1)->first();
$flights = App\Flight::find([1, 2, 3]);
事實上有不說少了在抱怨這個模式,請參閱以下的文章。
這個範例與 RowDataGateway 相同,都是以 Person 為範例,然後這裡有個重點要注意一下 :
這個地方 Active Record 的類別,我是以 Model 為命名,主要的原因是不少實作 Active Record 的語言,都是以 Model 為命名。
所以我現在暫時的將我對 Model 這個詞的定義為 :
以 Active Record 為概念的類別稱為 Model
// DataSouree Layer
const mockDbData = [
{
id: 1,
username: 'mark',
age: 19,
company: 'hahow'
},
{
id: 2,
username: 'ian',
age: 19,
company: 'amazon'
}
]
function executeSql(sql: string){
return mockDbData
}
class PersonModel{
id: number
username: string
age: number
company: string
constructor(id: string,username: string, age: number, company: string)
constructor(username: string, age: number, company: string)
update(): void{
console.log(`UPDATE person SET username=${this.username}, age=${this.age}, company=${this.company} WHERE id=${this.id}`)
}
insert(): void{
console.log(`INSERT INTO person (username, age, company) VALUES(${this.username}, ${this.age}, ${this.company})`)
}
// Domain Logical
isAdult(): boolean{
return this.age >= 18
}
// Domain Logical
isVIP(): boolean{
return ['HAHOW','GOOGLE','KKBOX'].includes(this.company)
}
static findByCompany(company: string): PersonModel[]{
const result: PersonModel[] = []
const sql = `SELECT * FROM person WHERE company=${company}`
const resultSet = executeSql(sql)
for (const data of resultSet) {
result.push(new PersonModel(data.id, data,username, data.age, data.company))
}
return result
}
}
// Domain Layer
class PersonTableModule{
register(username: string, age: number , company: string){
// 假設有個業務需求為公司有限定人數 limit = 10
const personsInCompany: PersonModel[] = PersonModel.findByCompany(company)
if(personsInCompany.length >= 10) throw Error('公司人數已達限制')
const person: PersonModel = new PersonModel(username, age, company)
if(!person.isAdult()) throw Error('要成年才能註冊喔')
person.insert()
}
}
這個模式基本上就是以 row 為單位,來進行資料操作與業務,這個在簡單的業務專案使用真的快速放便,但是我覺得這應該是架構師最難抓的,要如何判斷未來會不會複雜,會不會 overdesign,不知道有沒有什麼標準呢 ?
首先來談談 Domain Model,它是一個 Domain Layer 的模式,一個實體就是一筆資料,然後他裡面有很多業務邏輯與資料,只是他與 Active Record 的差別我覺得在於,Domain Model 應該是沒有限定一個 table,而是以 domain 為主,這我個人的看法 ~ 不一定對。
而 RowDataGateway 就是一個實體代表一個 row,但與 Active Record 不同處在於,它裡面沒有業務邏輯,而且好像有讀寫分離,整理上我個人是比較喜歡 RowDataGateway。
最後還有一個連結,那就是 ORM,ORM 本質上是與資料庫的對應實體,並且應該是不包含業務邏輯,所以兩者應該是不相同,但我覺得在 Active Record 中可以使用 ORM。
Domain Model 應該是沒有限定一個 table,而是以 domain 為主,這我個人的看法 ~ 不一定對
ddd 相關書籍也是這樣說的
domain model 以 aggregate 作為 修改狀態的最小邊界
與 repo (data layer) 互動
一個 aggregate 可能是多張 table 所組合起來的
讀取的話, 因為不會修改系統的狀態
可以忽略 domain model
根據不同需求, 定義不同的 read model
嗯 ~ 我自已也是這樣理解 ~ 只是 PoEAAA 那本有時總是沒說的很明確 ……