標籤彙整: MVC

框架不應該有「models」資料夾

各大back-end framework幾乎都採用了「MVC」架構。

它們至少會有「views」、「 controllers」、「 models」三個資料夾。

「views」跟「controllers」沒太大問題,但是「models」資料夾根本不該存在。

我要對這些框架提出嚴厲指控:

「models」資料夾的存在是一種錯誤的架構設計。它不但阻礙新手學習,還會傷害scalability。

任何back-end framework都不應該有「models」資料夾。

我會在這篇文章解釋理由,並且提出改善架構設計的幾個方向。

哪些框架有「models」資料夾?

光是我接觸過的Ruby、PHP框架,就至少有:

  • Rails(Ruby)
  • CodeIgniter(PHP)
  • Yii(PHP)

除此之外,像是Java, Python還是什麼語言,

一定也有框架做這種事:放一個「models」資料夾在那邊。

這真是大錯特錯。

你到底要放什麼東西到「models」資料夾裡面?你覺得Model是什麼?

Model是什麼?

撇開我之前提到的MVC正名爭議不談,光是MVC的M該如何解釋就已經是個大哉問。

看看這則出色的Stack Overflow問答:

How should a model be structured in MVC?

Model是layer、它包羅萬象、它涵蓋你全部的business logic。

眾說紛紜中,這是我們唯一能有的共識。

Model難以定義、沒有絕對正確的架構設計。聽起來真令人洩氣,對吧?

不!這樣很好!這樣才對!軟體架構本來就是大哉問,有無限種可能的方法,

這也是我們所有人應該要一起討論和嘗試的地方。

而「models」資料夾卻嚴重妨礙我們討論、阻止我們思考,

它不但阻礙新手學習,還會傷害scalability。

「models」資料夾如何阻礙新手學習?

說明這段之前,我先定義一個名詞:「entity」。

我將entity定義成「代表現實生活中的一種事物」。

以常見的Active Record pattern來說,

// user.rb
// Rails
class User < ActiveRecord::Base
// user.php
// Laravel
class User extends Eloquent
// post.php
// Yii
class Post extends CActiveRecord

類似這樣的東西,你一定看過。

user.rb、user.php、post.php,這些就是我所謂的「entity」。

也就是這些「entity」,讓新手容易誤以為「entity」就是MVC裡面的M。

錯!M是layer,entity只是M裡面的組成元素之一而已。

「models」資料夾的存在本身,會讓新手以為「弄幾個entity類別丟進去就搞定架構了」。

然後entity的行為、對entity做出的行為、關乎兩個以上entity的行為,管他什麼logic,

管他什麼行為,全部想辦法塞進entity類別。

下場通常就是:那些entity類別最後變得超肥胖、難以理解、動輒達到上千行程式碼。

我稱之為「胖胖entity」。

「models」資料夾帶給新手「model == entity」錯覺!

「models」資料夾誘惑新手去做出一堆胖胖entity!

「models」資料夾如何傷害scalability?

看看Rails社群這篇出名的文章:

7 Patterns to Refactor Fat ActiveRecord Models

幹得好!它點出「胖胖entity」的問題,並給出7個patterns去協助你設計軟體架構。

抽出行為邏輯變成Service Objects、抽出表單驗證邏輯變成Form Objects、抽出資料庫查詢邏輯變成Query Objects、抽出呈現邏輯變成View Objects...聽起來真棒,也確實很有幫助,不是嗎?

問題來了:抽出來的這些類別,到底要放哪裡?

我們看看文章下面comments提到的範例:GItLab

它的檔案結構如下:

/lib
    /gitlab
    /tasks
    /...
/app
    /assets	
    /controllers	
    /finders	
    /helpers	
    /mailers	
    /models	
    /services	
    /uploaders	
    /views	
    /workers

新的問題來了:那個「models」資料夾到底代表什麼?它是我們包羅萬象的偉大Model layer嗎?那finders、services、uploaders為什麼跟models在同一層,而不是在models裡面?lib/底下的gitlab/又是怎麼回事?難道GitLab的商業邏輯也出現在lib?

這就是我想說的:「models」資料夾的存在從一開始就污染了架構設計。

它引誘人們把entity全丟進去。結果除了entity以外的東西,像是前面的Service Objects、 Form Objects、Query Objects、View Objects,還有後面的finders、services、uploaders、gitlab全都不知道放哪了。只好隨便亂放。

GItLab原始碼中的models有代表MVC的M嗎?怎麼不改名叫entities?

就算改了又如何?Model layer到底在哪裡?四分五裂、結構鬆散。

一團混亂的設計、難以理解的命名、與MVC的M不相容的檔案結構。

所以我說,「models」資料夾傷害scalability!

那該怎麼辦?

要解決這個問題,首先得要了解MVC是三個極度不對稱的存在。

V: 負責呈現UI
C: 負責接受request、請M處理、回傳response
M: 負責全部的business logic

M幾乎是你的整個application。

你可以在框架底下,找地方建一個空資料夾,用公司名稱或是專案名稱替它命名,

然後開始煩惱軟體架構這件事。

好好煩惱entity要放在哪裡、Service Objects、 Form Objects、Query Objects、View Objects、finders、services、uploaders這些要放哪裡,彼此又要怎麼分門別類。

MVC不是萬靈丹,只是軟體架構的入門磚。

架構設計本來就是這麼難,OOP本來就是這麼難。

恭喜你,至少你跨出第一步了:

你不再把一堆胖胖entity丟進「models」資料夾,然後覺得設計完軟體架構了。


Q&A

Q1: 「models」資料夾毫無優點嗎?

「models」資料夾還是有少數優點。
它是一種quick and dirty作法,鼓勵你眼中只看見entity,然後把所有business logic全塞進裡面。
換句話說,它在小型的專案可以幫你節省時間。但它的優點也僅此而已。

Q2: 你的結論好空泛,什麼建一個公司名稱空資料夾啊。拜託給點方向?

沒問題,我給你兩個架構設計的參考方向。

第一個來自這篇文章:

Rails is Not Your Application

引用作者的話,核心精神如下:

Rails不是你的application。它可以是你的views還有資料來源,但不是你的application。把你的application放在Gem裡面或是lib/資料夾底下。

我不覺得這樣有很優雅,但至少點出一個可能方向,並且至少不再有models資料夾。

我第二個要給你的,是Laravel官方論壇原始碼

這個Laravel.io專案簡稱為Lio,結構如下:

/app
    /controllers
    /views
    /Lio
        /Core
            /...
        /Accounts
            /User.php
            /UserPresenter.php
            /UserRepository.php
            /UserCreator.php
            /....
        /Articles
            /Article.php
            /ArticlePresenter.php
            /ArticleRepository.php
            /ArticleCreator.php
            /....
        /Comments
        /...

光看檔案結構就很優雅。也正是我前面所說的:建一個空資料夾,用公司名稱或是專案名稱替它命名,然後開始煩惱架構設計這件事。

想想看Laravel官方論壇的原始碼為什麼長這樣吧。

Q3: 講得好像多有道理!我覺得你只是在鬼扯!框架的製作團隊都是業界大神,既然他們決定要有「models」資料夾,必定有它的正當性!

不,你錯了。那些業界大神只是背負了行銷框架的壓力。

他們為了滿足用戶的錯誤期待而委屈地放了個「models」資料夾在那。

但還是有高尚的人存在。PHP最被推崇的框架Laravel就沒有「models」資料夾。

向Laravel的Taylor Otwell致敬吧!

他不願成為殘害新手的幫凶,硬是把「models」拿掉了,

強迫你去思考:「軟體架構到底該長怎樣」。

你要自己在Laravel裡面做一個「models」資料夾,然後把那些entity class全丟進去嗎?

那你是自願把entity當成整個Model,真遺憾,

但別說是Laravel鼓勵你這麼做。Laravel盡力了。

Q4: 少自以為了解Taylor Otwell了!你憑什麼代替他發言!

Reddit上有一則 Why would anyone choose Laravel over Symfony or Silex?

Taylor Otwell本人親自做出回答。下文擷取自第四段:

我個人在開發Laravel 4的早期階段就想把「models」資料夾整個移除了。因為我不覺得它有用,也不覺得它能協助你設計軟體架構。而且它還會引誘人們掉入「model == database」的陷阱裡。所以,我希望你不要覺得我對架構設計很無知。我花了點時間才想清楚我到底想在PHP世界打造什麼。

Laravel實作Active Record Pattern,資料表映對到entity class。他指的「model == database」陷阱就是我說的「model == entity」錯覺。我並沒有代替他發言。

Q5: 我還是覺得,你沒有資格批評那麼多框架。「models」資料夾就是有某種正當性。除了Taylor Otwell,我看也沒有其他權威支持你的說法!

前面提到的出色Stack Overflow問答:

How should a model be structured in MVC?

作者是tereško。

Stack Overflow上關於MVC的幾個最高分討論,全都是由tereško解答

下文擷取自那篇出色問答的段落「What a model is NOT」:

model不是一個class,也不是任何一個單一物件。這是一個超級常見錯誤,因為大部分的框架都在助長這種誤解。

他選擇這樣帶過。我選擇正面指控。

然後我建議你討論事情的時候,不要太在乎權威還是前輩怎麼講。不如專注於討論事情本身。

Q6: 好啊!那來啊!照你的說法,「models」資料夾底下多放個「entities」資料夾不就搞定一切問題了?你果然是不切實際的理想主義者!最好是有框架幹這麼囉唆的事情!

有!它就是Cake(PHP)框架!

看看Cake在Model底下放了什麼:

/View
/Controller
/Model
    /Behavior
    /Entity
    /Table

看到了嗎?

Cake怕你把entity當成整個model,直接擺好幾個資料夾,逼你去思考entity跟model是什麼。

替這些用心良苦的框架歡呼吧!

Q7: 專注於討論事情本身是不是!那Cake的「models」資料夾就沒問題啊!你還說任何框架都不能有!

你看錯了,Cake沒有「models」資料夾,也沒有「Models」資料夾。它只有「Model」資料夾。

資料夾、package、資料庫table命名,都有一個關於單數/複數的原則可以參考:異質性與同質性。

你的「models」資料夾底下不再是同屬entity的class了,而是分為behavior、entity、以及其他你設計的分類,也就是異質,所以應該用單數命名。

參考這個連結:

Should package names be singular or plural?

簡單地說,既然model代表的是layer而非多個entity,資料夾命名上就應該用單數而非複數。

好吧,我這樣說有點太嚴苛了。

如果你知道自己在幹嘛的話,就繼續用你的「models」資料夾吧,我勉強可以接受。

Q8: 等等,不對勁...你整篇文章流露一股氣息...我覺得你不但反對「models」,你幾乎在否定MVC的價值?你怎麼可以覺得偉大的MVC沒有價值?

我前面提過,Taylor Otwell在Laravel 4移除了「models」資料夾,逼迫大家去思考「軟體架構」到底應該是什麼。

我告訴你第二件事。

你去逛Laravel官網,翻遍官網你都找不到「MVC」三個字。

MVC名氣多麼響亮!哪個framework不想打著MVC當作賣點?但Laravel拒絕這麼做。

我再告訴你第三件事。

2015年最新出爐的Laravel 5,它的views在resources/底下,controllers在app/Http/底下。一樣沒有models。

所以你神聖的MVC在Laravel 5底下長這樣:

/resources
    /views
    /lang
    /assets
/app
    /Commands	
    /Console	
    /Events	
    /Exceptions	
    /Handlers	
    /Http	
    /Providers	
    /Services	
    /Https
        /Controllers
        /Middleware	
        /Requests

你推崇的V跟C不再佔據檔案結構的核心位置了。你最愛的MVC現在看起來是如此渺小,

小到沒有討論價值,小到毫無意義可言。

「MVC是三個極度不對稱的存在」,這是個太過客氣的說法。

MVC這個觀念已經無法協助我們討論和思考了。放下它,往前走吧。

我來自Laravel社群。我們不聲稱自己擁戴MVC。

來把時間花在真正值得討論的概念上吧:你正在用的框架,架構合理嗎?框架有沒有擋住你的路?你在框架之下設計出的專案架構漂亮嗎?大中小型專案通用的架構存在嗎?如何分辨使用時機?怎麼做會最彈性?該怎麼描述某個框架才不會對新手揠苗助長?

放下你凡事都要套進MVC的執著,請直接思考「軟體架構」的本質。

啊,我看到MVC粉絲對Laravel 5的分析了:


MVC依然發揮重要的討論價值!我看到Controllers資料夾了!我看到views資料夾了!
剩下的十幾個資料夾全部統稱為Model!果然是豐富又厚重的layer!
我們來討論Model是什麼吧!MVC萬歲!

朋友,祝福你能得出有意義的結論。


小朱針對本文的延伸討論(2015-4-19補充)

[經驗談] 無招勝有招

(Photo via Alyssa L. Miller, CC licensed.)

MVC是一個巨大誤會

我是web工程師,從剛開始學MVC就深感困惑:

  • 怎麼每個地方說的MVC都不太一樣?
  • 有些文章講的MVC,跟我正在用的MVC,怎麼像完全不同的東西?

Model、Controller、View三者到底如何互動?真是一個定義不明、含糊不清的名詞。

這讓我研究了很久。最後,發覺它是一個嚴重的誤會。

這個誤會導致了學習和溝通上的代價,請聽我娓娓道來。

哪些是MVC?

web領域,不論前端(client-side)、後端(server-side)、不論什麼程式語言,幾乎所有framework都自稱、或被認為是「MVC」。

有哪些呢?

前端:Backbone.js、AngularJS、Ember.js…

後端:Ruby on Rails、CodeIgniter、Laravel、Django…

真的是這樣嗎?它們全都是MVC嗎?

MVC是什麼?

該怎麼定義MVC呢?

我們先來看看維基百科怎麼說:

MVC模式(Model-View-Controller)是軟體工程中的一種軟體架構模式,把軟體系統分為三個基本部分:模型(Model)、檢視(View)和控制器(Controller)。

嗯,跟大家說的一樣。我們繼續往下看:

模型(Model) 用於封裝與應用程式的業務邏輯相關的資料以及對資料的處理方法。「 Model 」有對資料直接存取的權力,例如對資料庫的存取。「Model」不依賴「View」和「Controller」,也就是說, Model 不關心它會被如何顯示或是如何被操作。但是 Model 中資料的變化一般會通過一種重新整理機制被公布。為了實作這種機制,那些用於監視此 Model 的 View 必須事先在此 Model 上註冊,從而,View 可以了解在資料 Model 上發生的改變。(比較:觀察者模式(軟體設計模式))

看起來有些陌生,整段描述跟你的web開發經驗完全不同,對嗎?

最大的疑問來自這句:

那些用於監視此 Model 的 View 必須事先在此 Model 上註冊,從而,View 可以了解在資料 Model 上發生的改變。(比較:觀察者模式(軟體設計模式))

後面直接叫你去看觀察者模式(observer pattern)。

問題來了:你有在view跟model之間實作observer pattern嗎?

也就是說,你的Model在資料改變之後,能主動通知View嗎?

沒有的話,就根本不符合MVC的定義。

全都不是MVC?

我們現在發現MVC有observer pattern這個必要條件了。

事情嚴重了起來。

client-side framework或許能夠符合這個條件。

Backbone.js官網範例來說,我們可以這樣在Model上註冊:

book.on({
  "change:title": titleView.update,
  "change:author": authorPane.update,
  "destroy": bookView.remove
});

它的確實作了observer pattern。

但server-side framework呢?

你的Model如何能在發生改變之後去「主動通知」View?

你平常開發web哪有用到server push的技術?

所有server-side framework,從Ruby的Rails;PHP的CodeIgniter、Laravel;到Python的Django,他們全都不是MVC。

它們實作的,是昇陽電腦在1998年提出的「Model 2」。

什麼是Model 2?

Model 2名氣不大,在維基百科連中文條目都沒有。我們看看英文條目怎麼講

Model 2 is a complex design pattern used in the design of Java Web applications which separates the display of content from the logic used to obtain and manipulate the content.

In a Model 2 application, requests from the client browser are passed to the controller. The controller performs any logic necessary to obtain the correct content for display.

它針對web而設計,讓controller進行必要的程序之後,將資料塞進view去呈現。

正是我們server-side框架在做的事情。

也就是說,server-side目前只能實作Model 2;client-side可以實作Model 2,也可以實作MVC。

巨大的代價

web工程師最常碰的就是client-side跟server-side框架。結果整個業界把MVC跟Model 2混為一談,都說成MVC。

這帶來了什麼後果呢?

MVC變成一個從初學者到業界工程師,永遠說不清楚、下不了定義的名詞。

這件事對於學習和討論,造成了非常巨大的成本。(參考下方的Q1和Q2)

那該怎麼辦?

下次有初學者詢問「什麼是MVC」的時候,怎麼回答才不會害他回家之後「越查資料越混亂」?

Rails is not MVC的作者提出了三種解決方法:

第一個方法是聲稱MVC已經從原始意義改變了,Model 2也可以稱為MVC。如此一來,我們可以用「傳統MVC」或「真MVC」來描述原始的MVC。這是現在普遍的作法,但我不認為改變定義是一個好主意。這幾乎是越搞越亂。

第二個方法是到處推廣Rails其實是Model 2,MVC就留給…MVC吧。這很困難,但至少能保持定義不變。

第三個是直接忽略這些混亂。管它那麼多?

我個人覺得MVC這個詞已經沒救了,不管怎麼解釋都會帶給別人混亂。

當對方同時學習client-side跟server-side時,混亂更強烈。

我選擇這樣回答:


MVC有分很多種喔!網路上全部沒寫清楚,你一定看不懂。
沒關係,你只要知道View可以抽出來就好。
C跟M先別管,你先隨便瞎搞吧。


Q&A

Q1: 怎麼可能各大server-side framework都搞錯?

確實有人腦袋清醒得很,它就是Python的Django。

Django的官方文件內根本沒有「Controller」這個名詞。

看看Django官網的常見問題

Q: Django似乎是一個MVC框架,但你們把Controller命名為「View」,把View命名為「Template」。你們幹嘛不用標準命名啊?

A: (前略)…如果你真的很想要一個縮寫,你就說Django是一個MTV框架吧。Model、Template、View。這樣分比較有道理些。

Django不想變成搞亂MVC的幫凶,只好委屈地又發明了一個名詞「MTV」。

Q2: 那client-side框架有受影響嗎?

有。client-side框架也必須為MVC巨大誤會浪費一堆時間解釋。

看看Backbone.js官網的常見問答

Backbone跟「傳統MVC」的關聯何在?

(上略)
…我們來比較一下Backbone跟像是Rails這種server-side MVC框架的差別…
(下略)

Backbone實作了「傳統MVC」,卻被迫用「傳統MVC」來描述server-side的Model 2,然後花一堆篇幅解釋。

Q3: 知道Model 2的存在又如何?我現在依然一片混亂!

沒錯,Model 2跟MVC都用到Model、Controller、View三個名詞,所以看起來類似。

但是,我們不應該再把時間花在思考「MVC怎麼如此難懂」。

我們討論的重點,應該是「如何分辨MVC與Model 2」、「在server-side如何實作Model 2才漂亮」、「在client-side實作MVC跟Model 2的優劣分別何在」。

Q4: 好,那你現在回答我,「如何分辨MVC與Model 2」?

OK,就讓我拋磚引玉一下。
分別談談Model、View、Controller吧:

View

  • Model 2: 不具有行為,只是等別人塞資料進去的模板(template)。
  • MVC: 具有監視Model的行為,並以此去改變呈現(presentation)。

兩種View有沒有很像?跟張飛、岳飛一樣像。

看看Backbone.js官網的View範例。你server-side的View哪是長這樣?

var DocumentRow = Backbone.View.extend({

  tagName: "li",

  className: "document-row",

  events: {
    "click .icon":          "open",
    "click .button.edit":   "openEditDialog",
    "click .button.delete": "destroy"
  },

  initialize: function() {
    this.listenTo(this.model, "change", this.render);
  },

  render: function() {
    ...
  }

});

Controller

  • Model 2: 接收請求與參數,轉交給Model處理,再把結果(最新的資料)塞進View。
  • MVC: 接收請求與參數,轉交給Model處理。沒其他事了。

兩種Controller有沒有很像?跟小狗、熱狗一樣像。

MVC的View跟Model 2的Controller可能還比較像一點。(隨便說說,千萬別這樣類比。)

Model

  • Model 2: 接收Controller傳來的參數,回傳結果。
  • MVC: 接收Controller傳來的參數,將結果通知View。

Model倒是有些類似。

總之,Model 2跟MVC除了三個部份的名字一樣之外,沒什麼關聯了。

Q5: 我決定徹底鑽研MVC跟Model 2的定義了。給我一些延伸閱讀?

MVC與Model 2的變異與結合

Rails is not MVC

Django appears to be a MVC framework, but you call the Controller the “view”, and the View the “template”. How come you don’t use the standard names?

How does Backbone relate to “traditional” MVC?

Model-View-Confusion part 1: The View gets its own data from the Model(2015-2-28新增。謝謝網友「王兲玐」的分享。)


一些社群的看法(2015-2-28)

附上幾個社群的連結,裡面有許多很棒的討論。

歡迎提供更多連結給我。

JavaScript.tw

PHP 台灣

Python Taiwan

劉依語 (Mosky)


良葛格針對本文的延伸討論(2015-4-19補充)

技術名詞紛爭多

(Photo via  Julia Wolf, CC License)

DDD實戰:一段訂單建立程序-Part 2

我在DDD實戰:一段訂單建立程序-Part 1進行了一次效果不佳的重構。

最後不盡理想的結果如下:

orderRepository = new OrderRepository();
    }

    private function getNewOrderOrCreate()
    {
        if (Auth::check()){

            $order = $this->orderRepository->getNewOrderByUser(Auth::user());
            if ( !$order ){
                $order = Order::createWithUser(Auth::user());
            }
                        
        } else {
            
            $order = $this->orderRepository->getNewOrderBySesssionId(Session::getId());            
            if ( !$order ){
                $order = Order::createWithSession(Session::getId());
            }

        }
        
        return $order;
    }

    public function getOrder()
    {              
        $order = $this->orderRepository->getNewOrderOrCreate();

        return View::make('client/order', ['order' => $order]);
    }

}

我對controller中的getNewOrderOrCreate函式非常不滿意。
controller中的其他部份,都只是初始化某些類別、把某些參數傳來傳去,謹此而已,唯獨這個函式內含複雜邏輯。真醜。

但是那些判斷logic確實是controller該負責的事情,也不能丟進model,怎麼辦呢?

DDD觀點

DDD中區分了Application Layer、Domain Layer。乍看之下getNewOrderOrCreate不能進Domain Layer,只能硬放在Application Layer變成醜controller。

我在DDD實戰:一段金流程序提到過Service的概念。

我研究了一下發現…

其實Service又可分為Application Service、Domain Service以及Infrastructure Service。

要判斷一個Service隸屬於哪種,常常非常困難(譬如說,您覺得DDD實戰:一段金流程序中的ReceivePaymentNotification算是Application service還是Domain service?)

不過眼前的getNewOrderOrCreate卻非常清楚,應該是屬於Application Layer!

(但這都沒關係。目前我沒打算針對三種Service在程式中加上namespace,也沒打算在檔案結構上分別開資料夾之類的。現階段我就隨便開個Service資料夾,然後管他哪種Service通通丟進去就好了。)

那麼,就讓我來封裝一個Service,接著在Controller使用它吧:

getOrCreate();

        return View::make('client/order', ['order' => $order]);
    }

}
orderRepository = new OrderRepository();
    }
    
    public function getOrCreate()
    {
        if (Auth::check()){

            $order = $this->orderRepository->getNewOrderByUser(Auth::user());
            if ( !$order ){
                $order = Order::createWithUser(Auth::user());
            }
                        
        } else {
            
            $order = $this->orderRepository->getNewOrderBySesssionId(Session::getId());            
            if ( !$order ){
                $order = Order::createWithSession(Session::getId());
            }

        }
        
        return $order;
    }
        
}

終於成功了!終於讓controller內的code看起來非常簡潔、expressive,卻又不影響到Domain Layer!
我還發現,getNewOrderOrCreate光看函式名稱就覺得概念模糊。這種模糊感本身就暗示了抽象化的不足、程式設計想得不夠清楚。
一模一樣的內容,封裝成類別、再重新命名,就成了FetchOrderForClient底下的getOrCreate,真是清楚多了。

DDD實戰:一段訂單建立程序-Part 1

需求

我公司為一電子商務公司,顧客可在網路上下訂單購買商品。
為提供出色顧客消費體驗,故允許顧客可不註冊不登入、直接建立訂單(付款前再要求登入即可)。

原解法

* 將已登入之顧客訂單紀錄顧客id;未登入之顧客訂單紀錄session id。

在MVC架構之下,我個人嚴禁在M內直接操作session,所以我將已登入之顧客訂單建立程序封裝於createWithUser函式,並傳入目前使用者;將未登入之顧客訂單建立程序封裝於createWithSession並傳入當前session id。
以上兩函式屬於Model範疇。
我接著將判斷使用者登入與否、以及判斷顧客是否有「未完成訂單」的code寫在controller,並封裝成getNewOrderOrCreate(拿出顧客之前未完成訂單、讓顧客繼續買東西。不然就建立一筆新訂單。)

inStatus(Order::CREATED_STATUS)->first();
            if ( !$order ){
                $order = Order::createWithUser(Auth::user());
            }        

        } else { // use is not logined

            $order = Order::where('session_id', Session::getId())->inStatus(Order::CREATED_STATUS)->first();
            if ( !$order ){
                $order = Order::createWithSession(Session::getId());
            }

        }
        return $order;        
    }

    public function getOrder()
    {              
        $order = $this->getNewOrderOrCreate();

        return View::make('client/order', ['order' => $order]);
    }

}

問題:
* getNewOrderOrCreate函式負責太多事情,code讀起來十分困難

已經將建立程序封裝在Model了,controller的code還是這麼雜,怎麼辦呢?

DDD觀點

應該定義一個專門負責以各種不同方式、參數取出domain objects的Repository類別。

我決定撰寫一個OrderRepository類別專職處理getNewOrderOrCreate負責的任務,並在controller中使用這個類別:

orderRepository = new OrderRepository();
    }

    public function getOrder()
    {              
        $order = $this->orderRepository->getNewOrderOrCreate();

        return View::make('client/order', ['order' => $order]);
    }

}
inStatus(Order::CREATED_STATUS)->first();
            if ( !$order ){
                $order = Order::createWithUser(Auth::user());
            }        

        } else { // use is not logined

            $order = Order::where('session_id', Session::getId())->inStatus(Order::CREATED_STATUS)->first();
            if ( !$order ){
                $order = Order::createWithSession(Session::getId());
            }

        }
        return $order;        
    }
    
}

controller內的code終於簡潔多了!
如果說DDD的repository對應到MVC,是M中最靠近C的一層的話…
那麼MVC常要求的’skinny controller, fat model’我終於做到了吧!

問題:
* 只是將code由C移出,getNewOrderOrCreate難以理解的問題依然存在
* 現在repository內操作session了…有點像在M內操作session的感覺,這不對吧?不是說了嚴禁嗎?

反省

getNewOrderOrCreate太過含糊不清,應該讓repository內的函式更expressive,光看函式名就得知會取出什麼樣的domain objects。否則,不但不夠reusable、也失去多寫一層repository的意義。

兩點想法:

* 關於session的操作,還是讓它留在controller內吧。

* controller對應到DDD觀點下,要負責application layer大部分的任務。
application layer本來就要負責知道當前提供的應用走到哪一階段(state)。
因此到底要取出未完成訂單、還是建立一筆新訂單,是controller要負責的事。

orderRepository = new OrderRepository();
    }

    private function getNewOrderOrCreate()
    {
        if (Auth::check()){

            $order = $this->orderRepository->getNewOrderByUser(Auth::user());
            if ( !$order ){
                $order = Order::createWithUser(Auth::user());
            }
                        
        } else {
            
            $order = $this->orderRepository->getNewOrderBySesssionId(Session::getId());            
            if ( !$order ){
                $order = Order::createWithSession(Session::getId());
            }

        }
        
        return $order;
    }

    public function getOrder()
    {              
        $order = $this->getNewOrderOrCreate();

        return View::make('client/order', ['order' => $order]);
    }

}
inStatus(Order::CREATED_STATUS)->first();
    }
    
    public function getNewOrderBySesssionId($sessionId)
    {
        return Order::where('session_id', $sessionId)->inStatus(Order::CREATED_STATUS)->first();        
    }
    
}

問題:
* controller又有點變回一開始那個鳥樣了
* 雖然repository讓controller內的code更可讀一點點了…,但反省半天到最後只有這麼一點benefit,而且repository內的函式在其他地方未必真的會用到,這reusability也許不必要
* 綜合以上兩點,我可不可以說這根本是over design、over engineering?

結論

如您所見,在我的範例之中,增加一層repository,當下得到的benefit並不大。有些人會認為跟最一開始的code根本沒差多少。

根據Martin Fowler的介紹,當query logic龐大起來時,多加上Repository這層專門負責處理這些query logic,不但可以減少duplicate code,還可以讓呼叫方呼叫時更清楚自己在呼叫什麼。

Repository在DDD中被認為是Domain Layer最重要的組成之一。
何時引進這層,就看當下需求了。

2015-01-04 更新

我越看那個controller越覺得很不爽。
研究了一下,發現DDD當然有指引這類問題的方向。
我缺少的是Application Service。
參見:DDD實戰:一段訂單建立程序-Part 2

framework實作 – front controller pattern & template engine

以前在用Python的Django、Rails或是PHP其他framework的時候,總覺得很難想像,要怎麼寫一個framework出來。

連最入門的這兩件事我都覺得難以想像:

  • 網址輸入’user/create’ 要如何執行controller資料夾內user class的create method?
  • 要如何讓controller去讀取view資料夾底下的html 然後render出去?

也就是說,所謂MVC pattern要從何開始實作?

最近試著親自動手做,發現其實沒那麼難,也從中學到不少。上述兩件事,100行內就可以作到。

提供我嘗試實作的source code給各位參考。

那100行程式碼長這樣

https://github.com/howtomakeaturn/PigFramework/blob/super_light/index.php

資料夾結構長這樣
https://github.com/howtomakeaturn/PigFramework/tree/super_light

(Photo via   Glyn Lowe Photoworks, 2 Million Views, Thanks, CC License)