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