關於Model的問題
去年,我還在用cakephp的時候,我注意到這篇文章
http://mk.netgenes.org/archives/230/
當然那個時候,我相當贊同這篇文章的論點,也有回應他。
接著我就開始進行大概半年的專案,當然最後也有成果,我是使用cakephp,所以那個時候也有寫了這一篇。
其中一張圖,我也有提到MVC的寫法,很明顯地我那時候還是認為Model是形同於表格的快取。之所以會這樣認為,也是跟上面的文章一樣。不能否認現在大多數做Web的都還是偏向做做網站,簡單的資料CRUD,將資料庫的資料撈進Controller,簡單地處理就可以達成需求,根本不需要啥複雜的商業邏輯。
那個時候確實知道這跟傳統的MVC觀念根本不一樣,但硬要去說不一樣不如說是一種適應在Web Framework上的變形。畢竟現在cakephp還是用這樣的方式。
我的專案雖然看起來是簡單的管理系統,但確有複雜的程序控制,資料檢查產生的商業邏輯。隨著專案進行,我逐漸發現這樣是不能解決問題的。Cakephp的Model傳回來的是Array,並不是物件,這樣讓我完全無法將商業邏輯寫在Model,在這種情況之下,Model就真的只剩下table快取的功能。
從The Cake Blog Tutorial我們可以看見
-
class PostsController extends AppController
-
{
-
var $name = 'Posts';
-
function index()
-
{
-
$this->set('posts', $this->Post->findAll());
-
}
-
}
[code]
// print_r($posts) output:
Array
(
[0] => Array
(
[Post] => Array
(
[id] => 1
[title] => The title
[body] => This is the post body.
[created] => 2006-03-08 14:42:22
[modified] =>
)
)
...
)
[/code]
這樣子的情況下,是無法撰寫出$posts[0]->submitToGoogleBlog()這樣的事情的。我只好乖乖地將這些功能全部寫在controller裡。
此外,我再仔細觀察了Cakephp對於Model Behavior的支援,發現他也只是將一些常見的Model CRUD的支援function提取到Behavior,讓設計師可以彈性的自訂。不過這樣是不夠的,因為這麼做就好像是將Model 1的時候一直線的程式撰寫,進行重構,將重複使用的function利用framework及class分群組而已。
而在重新看了三個php Framework的程式碼後,我大概重新整理了一下他們在model上的差異。
| Cakephp | Symfony | PHP on Trax | |
| 受...技術影響 | 基本上是Ruby on Rails 不過也參考了許多MVC的實做 |
基本上是Java Struts 不過也參考了許多MVC的實做 |
與Ruby on Rails幾乎一樣 |
| Model對資料庫存取方式 | 自行撰寫各個資料庫抽象層函示庫 | 使用Propel ORM函示庫 | MDB2資料庫抽象層函示庫加上一些修正 |
| Model對資料庫設定方式 | 簡單設定 | 複雜的XML | 簡單設定 |
| Model是否能進行 非資料庫的外部資料來源存取 |
是 | 否 | 否 |
| Model是否能即時取得 資料庫Schema |
是 | 否 | 是 |
| Model回傳型態 | Array | Object | Object |
| Model物件取回效率 (依照執行的程式碼數量) |
中 | 低 | 高 |
| Model物件快取 | 是 | 是 | 否 |
MVC觀念釐清
接下來我會從軟體工程的Waterfall Model探討,為啥需要這些東西。
我們先看看Wikipedia怎樣講。
http://en.wikipedia.org/wiki/Model-view-controller

MVC在1979年就由Trygve Reenskaug發明,當時他還在Xerox寫smalltalk。接著這個pattern就影響了Microsoft的MFC,他的Document/View架構就是MVC。而實際上如果嚴格地去看,如果你發現某一個語言有類似畫面,程式碼,資料來源拆開的作法,多半都是MVC的觀念。例如說C#的視窗程式使用了partial class,利用這來讓你盡量不要看見Form的程式碼。但實際上form的程式碼你還是可以改,IDE也還是自動產生C#程式碼。有些人認為MVC一定要使用三種不同的語言或技術,也是不對的觀念。
就以前Model 1 的寫法,是將邏輯資料與畫面混雜在一起。而在MVC(Model 2),就利用控制器(controller)分開畫面(view)及資料(model)。Controller有幾項工作
- 將Model物件取回,傳遞給View來產生實際頁面
- View的驗證
- Model的例外狀況
- 處理View的輸入
- 新增:建立一個Model的實體
- 修改:修改特定ID的Model物件
- 刪除:刪除設定ID的Model物件
- 最後都是得儲存
Model在MVC裡有一個重要的工作,本來就不是拿來做table快取用。Model的全名可以說是「應用程式領域物件模型」(Application Domain Object Model),通常都是代表你的應用程式所使用的類別。如果採用ORM(Object Relational Mapping)的觀念,也就是物件的屬性會對應到資料庫的欄位,而每一筆記錄就是一個物件了。
但是物件光有屬性是不夠的,物件的方法要從哪來呢?實際上當你在設計的時候,就應該要考慮到你的Model類別應該要進行怎樣的程式行為。這些行為我們通常都叫做商業邏輯(Business Logic),也叫做領域邏輯(Domain Logic)。換句話說就是專為你的Model類別所寫的方法。在三層架構(3-tier architecture)中,通常會提到表現層,邏輯層,資料層。資料層就是熟知的資料庫,表現層大家也都會認為就是View,可是有些時候邏輯層大家會誤以為就是Controller,而將Business Logic寫在Controller,一旦頁面變多,就會發現過多重複混亂的程式碼。
依照軟體工程的方式,大概會是以下的流程
- 需求分析
- 設計
- 實做
權限系統的實例
在介紹實例之前,要先提到幾個前提。通常小的應用程式都不會發生以下我說的事情,除非真的寫的亂七八糟了。但是如果是中大型的應用程式,例如說三人以上共同開發,或是有20個table,或是上百個頁面...當然這個準則見仁見智,只是當你覺得你的應用程式越寫越複雜的時候,就要注意了。而且通常這個情況也會發生在許多快速開發的專案上,如果沒有經常重構,也會很容易陷入這種泥沼中。
需求分析
假設我要做一個系統,裡面必須包含權限控制,這個系統有100個頁面。有關檢查頁面是否已登入的情況,幾乎全部的頁面都一定需要。有關使用者認證,我必須有選擇性第三方認證的功能。
實做的部分,我打算採用Ruby on Rails。
DB範例
雖然資料庫實做不應該先討論,但為了後面程式實做可以對照,我先放在這。
根據需求,我應該要有以下table:users,roles。
users table:
| id | username | password | name | role_id |
| 1 | admin | $5$dhdsf43d4f3as8d4f3 | Kiwi | 1 |
| 2 | guest | $1$a6s4df86asd4f8asd4 | Guest | 2 |
roles table:
| id | name |
| 1 | Administrator |
| 2 | User |
| 3 | Guest |
沒有商業邏輯的實做
先講講這個情況,是模擬在Model沒有商業邏輯的情況下,意思就是我們不在Model裡寫方法。
我先思考兩個方向:每個頁面撰寫自己的權限控制,不然所有頁面繼承同樣的權限控制方法(也就是寫在ApplicationController裡)。
當然你說,為啥不能夠每個頁面自己做檢查?當然是可以。
一旦頁面多達上百個,而權限的原則改變,要改寫程式碼時,就會掉淚了。
那我只好乖乖地繼承了。
-
class ApplicationController
-
model :user,:role
-
private
-
def check_login
-
if(!session[:user])
-
redirect_to :controller=>'user',:action=>'login'
-
end
-
def check_admin_role
-
if(!check_login) return false;
-
if(session[:user].role.id==0)
-
return true
-
end
-
#顯示類似「權限不足」的訊息
-
false
-
end
-
end
-
class RoleController <AppliactionController
-
def list
-
check_login
-
check_admin_role
-
#載入roles並且列出他們,做該做的工作
-
...
-
end
-
end
既然將權限控制繼承了,也就是說一旦開始有每個頁面對權限控制有不同的需要,就完蛋了。
-
def check_admin_role
-
if(!check_login) return false;
-
if(session[:user].role.id==0)
-
return true
-
end
-
if(session[:user].role.id<=1 && controller_name=='ctl1' && action_name=='act1') #另一個頁面
-
#嗯...
-
end
-
if(session[:user].role.id<=2 && controller_name=='ctl2' && action_name=='act2') #再一個頁面
-
#挖咧...
-
end
-
#接下去的還是別寫好了...
-
false
-
end
另外我們來看看使用者,根據上述的前提,又因為認證功能在很多頁面都有可能使用,我還是得繼承。
-
class ApplicationController
-
def user_authenticate(username,password)
-
auth_passed=true
-
if(!User.find(:first,["username=?,password=?",username,password]))
-
auth_passed=false
-
end
-
#選擇性的第三方認證,假設是google
-
if(params[:google_auth_checked])
-
#開始google auth
-
##你會把一堆程序寫在這##
-
#google auth結束
-
end
-
#如果還有更多第三方認證方式...會一直往下加
-
auth_passed
-
end
-
end
我曾經用這種方式撰寫了約500多行複雜的商業程序。
不管你怎樣寫結果一樣複雜,看起來我在挖個陷阱讓大家跳。不過這種類似的情況普遍地存在在許多的專案中,尤其是一剛開始快速開發,然後時間一久又發現需求開始變更的這種專案。而典型解決的方法,也就是各位程式設計師們的苦工。此外,現在有了MVC的寫法,如果沒有仔細思考Model的設計,也會自然而然地將這些方法往Controller那邊寫,久了也會造成這種情況。
有設計並有商業邏輯的實做
設計
根據上面的需求,我就開始思考,User到底該幹啥?Role到底該幹啥?不是與該類別相關的方法就不要加入,並將這些想法化成實際的圖。
變成類別圖看起來會像這樣。在UML中,有加上底線的是static method。

因為是WebApplication加上我想要使用Framework,所以我也預定要有這些controller,並將他們的view(method)標上。

實做
假設我已經將User,Role兩個寫好了,其中User
-
class User
-
has_many :role
-
...
-
end
接著我就可以:
-
class RoleController
-
model :user
-
def list
-
if(!session[:user])
-
redirect_to :controller=>'user',:action=>'login'
-
return
-
end
-
if(!session[:user].role.is_registered_user?)
-
#顯示"權限不足"
-
end
-
if(session[:user].role.is_admin?)
-
#顯示管理員功能
-
end
-
end
-
end
看起來,寫在role.is_admin?的程式我當然還是簡單地去用role.id去判斷,但在沒花多久時間簡單思考的設計中,做了幾件很重要的事情。
一是將前面例子的check_admin_role中,真正和Role有關係的程式碼重構到Role類別之中。就算是快速開發的專案,發現一樣的問題,要進行重構,也還是會做這件事情。
二是role.is_admin?只會做跟role和admin有關係的事情,不會再牽扯到controller或action。一個類別的一個方法只做好他該做的事,這個是在大系統中撰寫物件導向的目的,也就是進行鬆散整合(loosely coupling)。
但畢竟靜態方法,也是有點像進行重構,再看看另一個例子。
-
class User
-
def authenticate(password)
-
return self.password==Digest::MD5.hexdigest(password)
-
end
-
def google_auth(password)
-
#開始google auth
-
##你會把一堆程序寫在這##
-
#google auth結束
-
end
-
end
那麼,不管是啥頁面,你都可以方便使用,也不用在繼承的方法裡一直地加下去。
-
class UserController
-
model :user #avoid session restore error
-
def login
-
user=User.find(:first,["username=?",params[:username]])
-
if(!user)
-
#顯示未註冊錯誤
-
end
-
if(!user.authenticate(params[:password]))
-
#顯示密碼錯誤
-
end
-
if(params[:google_auth_checked] && !user.google_auth(params[:password]))
-
#顯示google認證錯誤
-
end
-
end
-
end
結論
雖然在這篇http://lightyror.thegiive.net/2007/01/active-record.html
文章有有闡述到,user.mail.read這樣的寫法寫起來比較順,但是其實我認為問題是設計上去影響到實做,並不是實做起來比較好寫而已。
當然並不是每樣東西都要套上MVC才肯罷休,MVC只是提出一個觀念:畫面,控制邏輯,商業邏輯與資料的分離。為何需要鬆散整合?為何需要物件導向?為何需要軟體工程或設計樣式?現在很多新一代的程式設計師都有很好的編程能力,但卻對以上一無所知,只是因為大家都這樣寫,文章都這樣介紹。加上許多快速開發的Framework,都強調讓設計師好維護,快速撰寫,所見即所得。難不成這些觀念,會因為這些快速的Framework而就變得不需要了嗎?一旦系統變大,數十個table,上百個頁面,就算再快速開發的Framework也不見得解決問題,還是全部都會回歸到設計的問題去。
經過之前的專案後,我發現,設計觀念更是重要,因為這些Framework本來就是依照設計樣式的觀念撰寫的。觀念一錯,就會造成誤用,誤用多的結果,就會效能低落。你會花上很多重複的程式碼去完成一件結構上很簡單的事,或是因為想要很高度的彈性而過度結構化(或者是過度鬆散)。其實在上述的例子,與其說是重構,或是改變寫法,不如說好好地思考並撰寫商業邏輯就可以解決了。
在這裡當然也要為Ruby on Rails美言幾句,實際上在比較過許多的Framework後,我發現Rails對於兩種Web程式設計師能夠帶來好處。
1. 如果你是新手,Rails的Generator,Scaffold能夠為你帶來快速開發的好處。
2. 如果你是老手,Ruby語法的快捷,以及Rails開發團隊為你撰寫許多彈性的功能,讓你反而不會被Framework所限制住。
參考連結
http://www.ccw.com.cn/server/jssc/htm2006/20061226_232112.shtml

6月 13th, 2007 at 10:57 am
記得我有看到有人提到寫rails會很容易將企業邏輯寫到Controller,剛看到的時候,我不了解Rails,所以無法理解,只能接受此問題。現在在回來看這篇,會覺得這也就是你上面提到的 -
You Say:….此外,現在有了MVC的寫法,如果沒有仔細思考Model的設計,也會自然而然地將這些方法往Controller那邊寫,久了也會造成這種情況。
最近深入的研究rails之後,在回來看這篇,從上面的
只會做跟role和admin有關係的事情,不會再牽扯到controller或action…
這對我現在來看,真的很有感覺
因為如果深入去看AWD那本Rails聖經,在ActionRecord Relation章節其實提到了很多議題,這些議題如果你沒好好去了解,就會變成認為model不過是個表格的快取,然後在controller中寫入企業邏輯。
rails本來就是認為企業邏輯是寫在model中,而不是Controller,也就是實踐你上文提到MVC的架構。那如果有人因為不熟悉誤用,將企業邏輯寫在Controller上,然後責怪rails不好,這樣真的還蠻詭異的。
6月 13th, 2007 at 4:36 pm
是的!你說的完全正確!
好久以前有一篇文章,就是IBM說明分散式系統的架構,強調表現層,邏輯層,資料層是分開的,就可以看出這些雛形。只是這個觀念在java的手裡逐漸變成龐大複雜的東西,我想這也是催生rails主要的原因之一。
我相信很多人一開始接觸MVC就會聯想到上述的分散式架構,而以為MVC裡的M是資料層,C是邏輯層,V是表現層。實際上MVC與分散式架構雖然相似,但完全是兩回事。M應該是邏輯層加一半資料層吧(另一半是資料庫)!但C這東西,我寫久了發現,真的也如同「控制器」這定義是在處理對V的輸出入。
Rails是一個寫Web的框架,而一個Web應用程式有多少開發時間是花在Model或Schema上呢?相較於View就少太多了,這也是在推廣一個東西時需要考量到其「理想」與「實際」的問題,所以在說下去,就要開始想,MVC真的適用在開發Web上嗎?
認真地思考,只是寫個站,有點殺雞用牛刀啦,這個C看起來除了處理輸出入是沒啥作用,所以才會有快速產生Controller(就是scaffold)這東西的出現。但是我最近在思考著要突破這問題,將M與C真正發揮他的功用,我想如果撰寫的系統大到一個程度,就會遇到這個問題了。
6月 14th, 2007 at 9:58 am
對於Scaffold,我也來說說我的想法
我覺得Scaffold是個很玄的東西,這個東西有兩個作用
1.展現間單的表象
哈,這大概只能拿來拍廣告用,XD。但是其實是剛入門的時候,越專注學某一個部份比較好,也就是說你用scaffold可以封裝C和V,專心的在學習model的一些概念上,馬上可以看到結果。
2.Generate Code的Scaffold
這個是昨天領會到的,我推測這個東西是基於要加速開發速度產生出來的,因為其實就像你說的,Controller很多基本的功能都很相似,要自己一個字一個字從無到有真的不容易,很且學習曲線很高,現在我會採取用scaffold generate出來code,然後在修改成自己要的。另外要附帶講的,就是很多人會覺得我又不是每一個都是CRUD這種操作。
最近漸漸強烈的感受到rails真的是認為CRUD是基本元素,通常反駁的人會說我又不是所有程式都是CRUD,可是事實上通常是基本的CRUD加上換名字與一些運算而以。例如,以user這個來說,Singup就是新增,login就是查詢加上一些邏輯,修改密碼資料的就是U等等。至於名字的切換就是在route.rb動手腳。
其實這引發出來的是我的疑問與想法,也或許會與你回覆的最後一段做結合
我昨天在寫code時候想到個問題,究竟model要先寫,還是controller先寫,model先寫很不合邏輯;所以我想就是在Controller編寫邊設計,就是我照SA邏輯寫,概念是只寫flow control的部份,至於涉及model的部份就是先想這與哪個model相關例如與User相關就是寫user.just_read(article),用比較白話的心態來說,就是在寫controller的時候先抱著想把責任推給model的心態卻寫,那自然就不會把一堆東西擠在controller了。
6月 15th, 2007 at 12:31 pm
你的領悟力相當棒,當然要看你對於寫程式的想法是啥,是不是把寫程式當工作,你想寫到哪種程度。如果你是一直想求新求進,想要看見你以前看不見的東西的話,我會推薦你去學「軟體工程」。
這四個字算是能夠完全解決你最後一段寫的疑問吧,不過我還是用提問法來回答你的問題。
請問你覺得自己寫軟體寫起來是啥樣?
1) 如同歡樂畫室的Bob Rose隨意揮灑,愛寫哪就寫哪,沒了畫筆用手指抹也行
2) 如同Micro$oft一樣,經常內部召開程式碼審查會議,發現有問題就趕快出補釘讓使用者去打~。
3) 如同日本Ruby會會長高橋說的「勤勉的」程式設計師,儘管一個方法要寫一千行,但也強調「如企業級軟體般的有擴充性和穩定」
4) 如同上面那個傢伙說的「輕量的」程式設計師,一千個方法只要寫一行,並且深信「關於穩定度這問題,時間會證明一切」
5) 如同諸葛亮一樣有孫子兵法+三十六計,不用則已,一用見血
6) 如同宮本武藏一樣,摳頂(coding)的最終境界是「No Code」
7) 如同趙括一樣:「反正我一定寫的出來」
我忽然覺得自己有點朝向7這答案~~嘿嘿~
我必須要說,人類的歷史中,打從有電腦以來,軟體工業也佔了人類的不少時間。而如果寫軟體沒有一定的法則,那以前的人不知道是混啥吃的?方法不僅是方法,要用對地方才是重要。而我從你問的問題可以發現,你的邏輯相當好沒話說。但似乎在你的心裡,該有啥工具,該瞭解啥觀念,你不並是很清楚。
而一旦你有了觀念,在加上你的邏輯,寫程式就不是「先寫後寫」(也不會說要把責任推給model了XD),而真的是「程式設計」。
而上述這問題也不是瞎扯的,分別代表程式開發的幾個階段,真的要解釋的話有一拖拉庫。(好吧我承認最後一個是瞎扯)
當然我的回答僅供參考,有興趣的話我們可以多討論。
6月 16th, 2007 at 11:36 am
其實我的問題很有問題,就是SA混合到寫程式中才想,如果的SA弄好,寫起來就很快了,model當然方法也都先想好了.
我個人比較喜歡七點中的6,可能跟看浪人劍客有關係吧 XD
軟體工程,真的是一門很大的學問,即便是現在,還是很多東西在推出演進,從開發模式到很多很多Issue,尚努力吸收中.
6月 16th, 2007 at 2:03 pm
不過說到no code,這真的不是瞎扯
我記得我小時候我阿公給我的教育,就是寫程式最好是不是要手寫
那要用啥寫呢~?我後來問他,他跟我想的一樣,就是要用「想的」
至少現在第一個技術是已經克服了,就是Brain Gate這種侵入式晶片,不過這只是給殘障人士使用,而且可以想像的是還在人體實驗階段
此外,第二個就是我目前看到的sematic web就是為了要讓未來的AI有理解力的一個很重要的動作,我相信不久之後AI就會懂得怎樣產生程式碼了,人類只要叫他寫怎樣的程式即可(至少那種秀秀圖的網站應該沒啥問題吧?)
6月 16th, 2007 at 10:16 pm
這真的是合理的,因為以現在來說,設計完成,寫程式真的就有點像是routine的動作。
6月 16th, 2007 at 10:19 pm
剛剛看了一下,好像這篇的回覆看起來還真壯觀。
7月 14th, 2007 at 2:20 am
我喜歡這種壯觀的回覆,看了很有收穫,讓我對軟體開發更有進一步了解。
7月 16th, 2007 at 10:51 am
[...] 記得在上次的的主題,重新探討Cakephp, Ruby on Rails與MVC,我大力地闡述能否實做商業邏輯,或是掌握Model的功用是多麼的重要。而老話一句,這個問題比較會出現在中大型的Web應用程式中,如果說你正在寫的是小小的網頁作業,或許本文對你只有參考的價值。 [...]