CHU PDLab Cluster服務建置 換個角度看事情
1月 08

關於Model的問題

去年,我還在用cakephp的時候,我注意到這篇文章

http://mk.netgenes.org/archives/230/

當然那個時候,我相當贊同這篇文章的論點,也有回應他。
接著我就開始進行大概半年的專案,當然最後也有成果,我是使用cakephp,所以那個時候也有寫了這一篇。

進階php程式設計-介紹MVC與Cakephp

其中一張圖,我也有提到MVC的寫法,很明顯地我那時候還是認為Model是形同於表格的快取。之所以會這樣認為,也是跟上面的文章一樣。不能否認現在大多數做Web的都還是偏向做做網站,簡單的資料CRUD,將資料庫的資料撈進Controller,簡單地處理就可以達成需求,根本不需要啥複雜的商業邏輯。

那個時候確實知道這跟傳統的MVC觀念根本不一樣,但硬要去說不一樣不如說是一種適應在Web Framework上的變形。畢竟現在cakephp還是用這樣的方式。

我的專案雖然看起來是簡單的管理系統,但確有複雜的程序控制,資料檢查產生的商業邏輯。隨著專案進行,我逐漸發現這樣是不能解決問題的。Cakephp的Model傳回來的是Array,並不是物件,這樣讓我完全無法將商業邏輯寫在Model,在這種情況之下,Model就真的只剩下table快取的功能。

The Cake Blog Tutorial我們可以看見

PHP:
  1. class PostsController extends AppController
  2. {
  3.     var $name = 'Posts';
  4.     function index()
  5.     {
  6.         $this->set('posts', $this->Post->findAll());
  7.     }
  8. }

[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,一旦頁面變多,就會發現過多重複混亂的程式碼。

依照軟體工程的方式,大概會是以下的流程

  1. 需求分析
  2. 設計
  3. 實做

權限系統的實例

在介紹實例之前,要先提到幾個前提。通常小的應用程式都不會發生以下我說的事情,除非真的寫的亂七八糟了。但是如果是中大型的應用程式,例如說三人以上共同開發,或是有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裡)。

當然你說,為啥不能夠每個頁面自己做檢查?當然是可以。
一旦頁面多達上百個,而權限的原則改變,要改寫程式碼時,就會掉淚了。

那我只好乖乖地繼承了。

RUBY:
  1. class ApplicationController
  2.   model :user,:role
  3.   private
  4.   def check_login
  5.     if(!session[:user])
  6.       redirect_to :controller=>'user',:action=>'login'
  7.     end
  8.   def check_admin_role
  9.     if(!check_login) return false;
  10.     if(session[:user].role.id==0)
  11.       return true
  12.     end
  13.     #顯示類似「權限不足」的訊息
  14.     false
  15.   end
  16. end

RUBY:
  1. class RoleController <AppliactionController
  2.   def list
  3.     check_login
  4.     check_admin_role
  5.     #載入roles並且列出他們,做該做的工作
  6.     ...
  7.   end
  8. end

既然將權限控制繼承了,也就是說一旦開始有每個頁面對權限控制有不同的需要,就完蛋了。

RUBY:
  1. def check_admin_role
  2.     if(!check_login) return false;
  3.     if(session[:user].role.id==0)
  4.       return true
  5.     end
  6.     if(session[:user].role.id<=1 && controller_name=='ctl1' && action_name=='act1') #另一個頁面
  7.       #嗯...
  8.     end
  9.     if(session[:user].role.id<=2 && controller_name=='ctl2' && action_name=='act2') #再一個頁面
  10.       #挖咧...
  11.     end
  12.     #接下去的還是別寫好了...
  13.     false
  14.   end

另外我們來看看使用者,根據上述的前提,又因為認證功能在很多頁面都有可能使用,我還是得繼承。

RUBY:
  1. class ApplicationController
  2.   def user_authenticate(username,password)
  3.     auth_passed=true
  4.     if(!User.find(:first,["username=?,password=?",username,password]))
  5.       auth_passed=false
  6.     end
  7.     #選擇性的第三方認證,假設是google
  8.     if(params[:google_auth_checked])
  9.       #開始google auth
  10.       ##你會把一堆程序寫在這##
  11.       #google auth結束
  12.     end
  13.     #如果還有更多第三方認證方式...會一直往下加
  14.     auth_passed
  15.   end
  16. end

我曾經用這種方式撰寫了約500多行複雜的商業程序。

不管你怎樣寫結果一樣複雜,看起來我在挖個陷阱讓大家跳。不過這種類似的情況普遍地存在在許多的專案中,尤其是一剛開始快速開發,然後時間一久又發現需求開始變更的這種專案。而典型解決的方法,也就是各位程式設計師們的苦工。此外,現在有了MVC的寫法,如果沒有仔細思考Model的設計,也會自然而然地將這些方法往Controller那邊寫,久了也會造成這種情況。

有設計並有商業邏輯的實做

設計

根據上面的需求,我就開始思考,User到底該幹啥?Role到底該幹啥?不是與該類別相關的方法就不要加入,並將這些想法化成實際的圖。
變成類別圖看起來會像這樣。在UML中,有加上底線的是static method。
webmvc_1.png
因為是WebApplication加上我想要使用Framework,所以我也預定要有這些controller,並將他們的view(method)標上。

webmvc_2.png

實做

假設我已經將User,Role兩個寫好了,其中User

RUBY:
  1. class User
  2.   has_many :role
  3.   ...
  4. end

接著我就可以:

RUBY:
  1. class RoleController
  2.   model :user
  3.   def list
  4.     if(!session[:user])
  5.       redirect_to :controller=>'user',:action=>'login'
  6.       return
  7.     end
  8.     if(!session[:user].role.is_registered_user?)
  9.       #顯示"權限不足"
  10.     end
  11.     if(session[:user].role.is_admin?)
  12.       #顯示管理員功能
  13.     end
  14.   end
  15. end

看起來,寫在role.is_admin?的程式我當然還是簡單地去用role.id去判斷,但在沒花多久時間簡單思考的設計中,做了幾件很重要的事情。

一是將前面例子的check_admin_role中,真正和Role有關係的程式碼重構到Role類別之中。就算是快速開發的專案,發現一樣的問題,要進行重構,也還是會做這件事情。

二是role.is_admin?只會做跟role和admin有關係的事情,不會再牽扯到controller或action。一個類別的一個方法只做好他該做的事,這個是在大系統中撰寫物件導向的目的,也就是進行鬆散整合(loosely coupling)。

但畢竟靜態方法,也是有點像進行重構,再看看另一個例子。

RUBY:
  1. class User
  2.   def authenticate(password)
  3.     return self.password==Digest::MD5.hexdigest(password)
  4.   end
  5.   def google_auth(password)
  6.       #開始google auth
  7.       ##你會把一堆程序寫在這##
  8.       #google auth結束
  9.   end
  10. end

那麼,不管是啥頁面,你都可以方便使用,也不用在繼承的方法裡一直地加下去。

RUBY:
  1. class UserController
  2.   model :user #avoid session restore error
  3.   def login
  4.     user=User.find(:first,["username=?",params[:username]])
  5.     if(!user)
  6.       #顯示未註冊錯誤
  7.     end
  8.     if(!user.authenticate(params[:password]))
  9.       #顯示密碼錯誤
  10.     end
  11.     if(params[:google_auth_checked] && !user.google_auth(params[:password]))
  12.       #顯示google認證錯誤
  13.     end
  14.   end
  15. 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

10 Responses to “重新探討Cakephp,Ruby on Rails與MVC”

  1. kuni Says:

    記得我有看到有人提到寫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不好,這樣真的還蠻詭異的。

  2. Kiwi Says:

    是的!你說的完全正確!

    好久以前有一篇文章,就是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真正發揮他的功用,我想如果撰寫的系統大到一個程度,就會遇到這個問題了。

  3. kuni Says:

    對於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了。

  4. Kiwi Says:

    你的領悟力相當棒,當然要看你對於寫程式的想法是啥,是不是把寫程式當工作,你想寫到哪種程度。如果你是一直想求新求進,想要看見你以前看不見的東西的話,我會推薦你去學「軟體工程」。

    這四個字算是能夠完全解決你最後一段寫的疑問吧,不過我還是用提問法來回答你的問題。

    請問你覺得自己寫軟體寫起來是啥樣?
    1) 如同歡樂畫室的Bob Rose隨意揮灑,愛寫哪就寫哪,沒了畫筆用手指抹也行
    2) 如同Micro$oft一樣,經常內部召開程式碼審查會議,發現有問題就趕快出補釘讓使用者去打~。
    3) 如同日本Ruby會會長高橋說的「勤勉的」程式設計師,儘管一個方法要寫一千行,但也強調「如企業級軟體般的有擴充性和穩定」
    4) 如同上面那個傢伙說的「輕量的」程式設計師,一千個方法只要寫一行,並且深信「關於穩定度這問題,時間會證明一切」
    5) 如同諸葛亮一樣有孫子兵法+三十六計,不用則已,一用見血
    6) 如同宮本武藏一樣,摳頂(coding)的最終境界是「No Code」
    7) 如同趙括一樣:「反正我一定寫的出來」

    我忽然覺得自己有點朝向7這答案~~嘿嘿~

    我必須要說,人類的歷史中,打從有電腦以來,軟體工業也佔了人類的不少時間。而如果寫軟體沒有一定的法則,那以前的人不知道是混啥吃的?方法不僅是方法,要用對地方才是重要。而我從你問的問題可以發現,你的邏輯相當好沒話說。但似乎在你的心裡,該有啥工具,該瞭解啥觀念,你不並是很清楚。

    而一旦你有了觀念,在加上你的邏輯,寫程式就不是「先寫後寫」(也不會說要把責任推給model了XD),而真的是「程式設計」。
    而上述這問題也不是瞎扯的,分別代表程式開發的幾個階段,真的要解釋的話有一拖拉庫。(好吧我承認最後一個是瞎扯)

    當然我的回答僅供參考,有興趣的話我們可以多討論。

  5. kuni Says:

    其實我的問題很有問題,就是SA混合到寫程式中才想,如果的SA弄好,寫起來就很快了,model當然方法也都先想好了.

    我個人比較喜歡七點中的6,可能跟看浪人劍客有關係吧 XD

    軟體工程,真的是一門很大的學問,即便是現在,還是很多東西在推出演進,從開發模式到很多很多Issue,尚努力吸收中.

  6. Kiwi Says:

    不過說到no code,這真的不是瞎扯
    我記得我小時候我阿公給我的教育,就是寫程式最好是不是要手寫
    那要用啥寫呢~?我後來問他,他跟我想的一樣,就是要用「想的」

    至少現在第一個技術是已經克服了,就是Brain Gate這種侵入式晶片,不過這只是給殘障人士使用,而且可以想像的是還在人體實驗階段
    此外,第二個就是我目前看到的sematic web就是為了要讓未來的AI有理解力的一個很重要的動作,我相信不久之後AI就會懂得怎樣產生程式碼了,人類只要叫他寫怎樣的程式即可(至少那種秀秀圖的網站應該沒啥問題吧?)

  7. kuni Says:

    這真的是合理的,因為以現在來說,設計完成,寫程式真的就有點像是routine的動作。

  8. kuni Says:

    剛剛看了一下,好像這篇的回覆看起來還真壯觀。

  9. R@Ndy Says:

    我喜歡這種壯觀的回覆,看了很有收穫,讓我對軟體開發更有進一步了解。

  10. 介紹Ruby on Rails Says:

    [...] 記得在上次的的主題,重新探討Cakephp, Ruby on Rails與MVC,我大力地闡述能否實做商業邏輯,或是掌握Model的功用是多麼的重要。而老話一句,這個問題比較會出現在中大型的Web應用程式中,如果說你正在寫的是小小的網頁作業,或許本文對你只有參考的價值。 [...]

Leave a Reply