分類彙整:programming

我對Rails之父的觀察

Rails之父,網路代號DHH,本名David Heinemeier Hansson。
我不認識他本人,但是從他自己的blog、他的合夥人Jason Fried的描述、媒體的採訪,我發現幾件事和想像中的不一樣。撰文和各位分享。


他本來是個有商業天賦的PHP工程師

DHH的合夥人Jason Fried在Hacker News親自寫道

我遇見DHH時他是個PHP工程師。我寫過一些PHP,能夠稍微評估他的能力。除此之外,我還喜歡他的商業素養以及平常做事的方法—都跟我很合。(*1)

他本來不覺得自己適合當程式設計師

他接受Big Think訪問時,其中這段提到:

說來有點好笑。我以前寫PHP跟Java的時候,常常花時間去摸其他程式語言。到處摸看看其他程式語言…隨便什麼都好。寫PHP跟Java實在太悶了,我需要用這種方式讓自己暫時抽離。(*2)

還有這段:

我以前寫PHP跟Java的時候,完全不覺得自己之後會當程式設計師。(*3)

閱讀程式設計的教材、範本給他很多苦悶,他無法那樣學習

他在網誌中透漏,自己沒有對電腦熱愛到能夠純粹為了學習而學習。
請參閱我之前的翻譯:Rails之父:我怎麼學會寫程式的?

他不是資工系畢業的

看他在signalvnoise部落格下方的評論,網友Ben提到:

@DHH
你沒有資訊工程學位。這說明了超多事情。

DHH在下面回應:

我其實有半個資工學位(我們系是一半商業管理、一半電腦科學)。但我沒在學校真的學到怎麼寫程式。(*4)

翻閱維基百科的介紹

從Copenhagen商學院拿到商管與電腦科學學位後,他從丹麥搬到美國。(*5)

他拿的學位等於台灣的「資訊管理學系」。

他一開始只是接案,不是決心創業

DHH以Basecamp(以前叫37signals)公司合夥人的身份跟Jason Fried合出過幾本書。
大家都知道Rails是開發Basecamp的過程中,從code中抽離出來而成的框架。
那麼一開始他是如何定位自己呢?
我寫信給Jason Fried本人,問他股權的事情。他的答覆如下:

Hi Tony-

我們最一開始沒有談股權的事。

我一開始是發案給他,之後僱用成員工。我們一起工作了好幾年才談到公司所有權的事。

-Jason(*6)


*1
I’ve done some PHP programming in the past, and I met DHH when he was a PHP programmer, so I was able to evaluate his talent at a very basic level. Beyond that, however, I liked his business mind and general approach to things – they were closely aligned with my own.

*2
It’s kind of funny; when I used to develop in PHP or the stuff I did in Java, I was always looking for something else. I was always looking for another programming language, another… just something else, in part just to distract me from being bored in the languages I was in.

*3
I was absolutely not convinced that I was going to be a programmer when I was working with PHP and Java.

*4
I actually got half a comp sci degree (my bachelor was in biz admin/comp sci), but that wasn’t where I truly learned to program.

*5
After graduating from the Copenhagen Business School and receiving his bachelor’s degree in Computer Science and Business Administration, he moved from Denmark to Chicago, Illinois, U.S. in November 2005.

*6
Hi Tony-

We didn’t talk about stock at the very beginning.

I hired David as a contractor first, then an employee. We worked
together for a few years before there was any talk about ownership.

-Jason

DDD實戰:建立訂單的多種方式

需求

設計一套專案管理軟體,有三種身份的使用者能建立專案。分別是「管理員」、「已註冊顧客」、「未註冊顧客」。

原解法

在Project類別底下,實作三個靜態函式:
* createForAdmin
* createForClient
* createForGuest
分別代表三種建立專案的方法。

在建立時只要呼叫:

$project = Project::createForClient();

即可。

問題

Project類別的函式代表了「一個專案擁有的方法、屬性」,現又加入了靜態函式去代表「關於專案的一切函式」。
在Project類別還小時,這是一個快速又可接受的作法。
但當Project類別肥大起來,這增加了理解的困難(Project代表的含意太過廣泛)。

DDD觀點

負責建立domain objects的任務可以獨立成一個Factory類別。
在建立時只要呼叫:

$factory = new ProjectFactory();
$project = $factory->createForClient();

即可。

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

DDD實戰:一段金流程序

最近開始研究DDD(Domain Driven Design),發現它確實是組織大型軟體架構出色又實際的方法。
可惜網路上只找得到一堆理論與說明,實際的範例code很少,我只好自己摸索了。
我會陸續將成果分享出來,希望對需要的人有所幫助。


需求

我公司串接由A公司提供之金流服務(第三方支付),A公司在確認收到顧客付款(信用卡、ATM轉帳)之後,會以HTTP POST通知我公司某設定好之URL。
由於可能有粗心大意之顧客,針對單筆訂單送出多筆交易、重複付款,因此需要在發現重複付款之時通知A公司放棄之前相關授權交易。

原解法

我公司有代表交易之trades資料表與代表訂單之orders資料表。
商業邏輯如下:
* 使用A公司SDK計算與驗證參數
* 從trades找到對應之交易並設為「已授權」
* 從orders找到對應之訂單並設為「已付款」

在MVC架構之下,我將其撰寫於某專責於金流交易之controller:

fetchParams();

        // 驗證失敗,參數並非A公司加密傳來
        if (!$params) {
            return '0|Fail';        
        }
         
        // 更新交易狀態
        $t = Trade::where('trade_no', $params['trade_no'])->first();
        $t->recieved_datetime = date('c');            
        $t->status = 1;
        $t->save();
        
        // 重複付款了,發送API通知A公司取消前幾筆交易
        if ($t->order->status == ORDER::PAID_STATUS){
            // blah blah
            // ...
            return '1|OK';            
        }
        
        // 更新訂單狀態
        $t->order->status = ORDER::PAID_STATUS;
        $t->order->save();
        
        return '1|OK';            
    }

}

問題:
* code可讀性不夠高
* 此controller action一次處理多件事情、分工不佳

反省

也許可將這串程序封裝成Trade類別底下的一個動作?


class SomeController extends BaseController {

    public function postPaymentReceive(){
        // A公司提供之SDK
        $sdk = new SDK();
        $params = $sdk->fetchParams();

        // 驗證失敗,參數並非A公司加密傳來
        if (!$params) {
            return '0|Fail';        
        }
        
        // 授權此筆交易
        // 內含交易狀態更新、訂單狀態更新、重複扣款通知
        $t = Trade::where('trade_no', $params['trade_no'])->first();
        $t->authenticate($params);
                
        return '1|OK';            
    }

}

問題:
* 只是將code由controller移進model,所有問題都依舊存在

在MVC架構之下,只追求’thin controller, fat model’會很容易將軟體架構只改善到這個階段。

DDD觀點

一個「交易」實體本身不會「授權」自己,應該由「某個程序」去進行「授權」一個「交易」實體的動作。
也就是說這行code不合理:

$t->authenticate();

它將原本Trade類別的意義由「一筆交易擁有的屬性、行為」擴大去包含了「會對一筆交易施加的行為」。
就算改寫為靜態函式依然擴大了原Trade類別的意義。
Trade類別的意義過廣會導致可讀性降低、不好理解、難維護。

DDD中關於一個service的判斷如下:
* 涉及到domain中的其他object
* stateless
* 涉及一個domain,通常不屬於一個entity或value object

我決定撰寫一個service類別專職處理這段接收通知的金流程序,並在controller中使用這個類別:


class SomeController extends BaseController {

    public function postPaymentReceive(){
        // A公司提供之SDK
        $sdk = new SDK();
        $params = $sdk->fetchParams();

        $service = new ReceivePaymentNotification();

        return $service->handle($params);        
    }

}
processParameters($params);

        return '1|OK';
    }
    
    public function processParameters($params)
    {        
        $t = Trade::where('trade_no', $params['trade_no'])->first();

        $this->processTransaction($t, $params);        

        $this->processOrder($t->order);        
    }

    public function processTransaction($t, $params)
    {
        $t->recieved_datetime = date('c');            
        $t->status = 1;
        $t->save();
    }
    
    public function processOrder($o)
    {

        // 重複付款了,發送API通知A公司取消前幾筆交易
        if ($o->status == ORDER::PAID_STATUS){
            // blah blah
            // ...
            return '1|OK';            
        }
        
        // 更新訂單狀態
        $o->status = ORDER::PAID_STATUS;
        $o->save();
    }
    
}

幾乎只是將原本的code剪下、貼上而已,卻花了我整個下午。

您覺得可讀性、可維護性有改善嗎?

期待您的寶貴意見,歡迎在下方給我feedback。

軟體架構:Domain Driven Design

從事軟體開發,特別是從Web領域開始的developers,一定因為各框架普遍提供MVC架構的關係而大致了解MVC的功能區分。

然而,當專案大了起來,很快就會發現如何讓MVC分工,比想像中困難:

* 某些code到底該放在C還是M?

* 修改V內的某些code會直接影響到business logic的運作與安全…這不應該吧?

很顯然地,對軟體架構的知識只擁有MVC是不夠的。

我舉一個更殘酷的例子。我在Reddit曾經和國外的工程師討論MVC的問題:

What exactly is ‘model’ in MVC?

最高分的回答如下,謝謝Nephand

I would say think of the ‘model’ more as your application – it’s not a single thing. The ‘view’ and ‘controller’ are just a channel of communication into your application’s behaviours.
Another word to replace ‘model’ might be ‘domain’ – see Domain Driven Design. Your application domain might need to deal with emails, or SMS but such things aren’t specific to your domain – generally speaking – so they’re services that your model is aware of (even if it’s just dispatching an event that your services listen for). They’re channels of communication into and out of your domain.
Your web interface talks to the application domain through a view and controller. Yet imagine you want to offer an API – this has no need of the View [simplistically put]. Also, your application might talk to the outside world through an SMS Service, or an Email Service and so on.
Personally, I like Hexagonal architecture as a way of conceptualising this.
Your ‘model’/’domain’/’core application’ then might have, for example, a persistence/database layer which creates a ‘seam’ between your application’s logic and the actual data storage format. This can be great, it translates out complex objects into a simple data format (SQL Rows…Mongo Documents…Redis)
You mention the idea of ‘one model’ and ‘one table’ – and you’re right to say this part is confusing. The ‘model == table’ approach has plenty of benefits, in that it’s pretty simple and easy to follow – but it can be limiting.
Having a ‘seam’ between your model and the data store becomes useful as it grows more complex. Within your ‘model’ you might have one ‘god object’ that serves as a root for multiple sub-objects. Each part can encapsulate behaviours that its containing object doesn’t need to know about – but all of that complexity still constitutes just one table row.
Domain Driven Design (as a primary voice but not the only one) differentiates Entity and Value Object [the classic Value Object being Money ].
The Entity might represent what you’re thinking of as a ‘row’ in your table (in the most general sense) – it has something that makes you want to be able to track its lifespan and differentiate it from others inside your application (e.g. a unique primary key ‘ID’).
Quick and dirty example: A User. A User would/could be an Entity, with a unique identifier. A User might have an email address. The concept of an ‘Email Address’ is not unique to a User, and so it might be a good candidate for being a Value Object that can be used by anything in your application domain/model. So when you later need to give another Entity (say a Company) an email address, you can re-use that same value object and all of the behaviours you might want for all EmailAddresses in your domain.
Lets say your business requires a hatred of hotmail users, and will never consider @hotmail.com a valid email address. You can then encapsulate such a rule into your EmailAddress object’s constructor [other approaches are preferred perhaps but I did say quick and dirty].
Whether you use EmailAddress in the context of a User or a Company, it will always throw an Exception / fail elegantly.
This is not [typically] intended to validate user input, but to ensure that your model/domain is meeting your hotmail hatred requirement by not allowing an EmailAddress value object for “*@hotmail.com” to be created. Whether that is passed in from a web form, over an API, or parsed in from an SMS interface shouldn’t matter to your core logic – you’re just saying ‘never allow this to happen even if it makes it past input validation somehow’.
This comes back to your concern about having a complex model separated into different models and in turn tables. That is where different strategies for mapping your ‘model’ to your data store come in. The Data Mapper is arguably the most flexible – and in turn potentially complicated – to implement. Patterns of Enterprise Application Architecture in combination with the Evans book Domain Driven Design can go a long way to help you understand these ideas, even if you water them down to ‘keep it simple [stupid]’.
Basically your User table (VALUES user_id, email_address) would be updated with the $user->id and $user->emailAddress->getAddress() values to store. A Company would look similar, $company->id, $company->emailAddress->getAddress().
The great part about all of this is that you can start to break up and refactor your god objects into smaller parts, that can be persisted separately or not. While your god object might still serve as a ‘root’ object, you will hopefully have been able to split its behaviours off into smaller chunks that can be tested easily, persisted easily, and even re-used where needed by other parts of your model.
This is all well and good, but don’t over-engineer things. If it’s just a string value, then just let it be a string value. Until you arrive at a behaviour requirement that makes it useful to implement an abstraction then it’s probably just bloat.
Obligatory warning This is a massive over-simplification of some more interesting concepts than I’d be able to explain – both due to lack of experience and time. Apologies to the knowledgeable if it’s a bad explanation – and to everyone else if it’s especially rambling, I don’t have time to proof read.
Quick Links for getting to grips with the model: – Unbreakable Domain Models – good quick intro. – Domain Driven Design – Patterns of Enterprise Application Architecture – Command Query Request Separation

為了回答我關於MVC的問題,他引用了大量的專有名詞…我完全看不懂,但大致知道是關於Domain Driven Design的東西。看起來這些概念已經是國外討論軟體架構的必備知識了。

而這些,在台灣很少聽人提到。


關於DDD其實我早有耳聞。Rails之父DHH在

The five programming books that meant most to me

曾經提到一本關於Domain Driven Design的書:由Eric Evans撰寫的Domain-Driven Design: Tackling Complexity in the Heart of Software

我翻過之後實在是..很厚重,再加上又是原文,唸不下去。

今天在網路上找到此書由其他作家精簡過之後,接著翻譯為簡體中文的版本

领域驱动设计精简版

我還沒有心得可以分享,因為我還沒有消化完。

但是目前翻閱了三分之一,確實非常精彩。

開卷有益啊朋友,有空翻一翻吧。