_溫馨提示:此篇章需要比較長的時間才能最終定稿,因為我還要尋找最合適的方式和語言來表述。_
##2.16.1 領域驅動設計##
很多框架關心性能,而不關心人文;很多項目關心技術,而不關注業務。
就這造成了復雜的領域業務在項目中得不到很好地體現和描述,也沒有統一的規則,更沒有釋意的接口。最終導致了在“純面向對象”框架里面凌亂的代碼編寫,為后期的維護擴展、升級優化帶來很大的阻礙。這就變成了,框架只關注性能,項目只關心技術,而項目卻可憐地失去了演進的權利,慢慢地步履維艱,最終牽一發而動全身。
**很多人都不知道該如何真正應對和處理領域的業務** ,盡管領域業務和單元測試都是如此重要并被廣泛推崇。正如同表面上我們都知道單元測試卻沒有具體真實地接觸過,并且一旦到真正需要編寫一行單元測試的代碼時就傻眼了。
這里不是發明一些新技術,也不是提供一些新的模式,而是繼續將前人、大神和頂級大師關于領域驅動設計這方面的思想結合真實后臺接口開發進行分享,進而推廣之。
##2.16.2 講述故事
很多人,都喜歡聽故事。像我以前中學的時候,就很喜歡看《故事會》。
如果,我們能讓代碼也像小說一樣,在講述某個故事時,將會更加吸引“讀者”(也就是其他開發同學),從而易于理解和維護。
最近,我在做一個項目時,再一次發現了這種講述故事的威力。
###(1)一個第三方登錄的寫法
我們先以F項目來命名這個項目,在F項目中,我們跟其他App一樣,需要接入第三方登錄,其中包括:微信登錄、微博登錄和QQ登錄、郵箱登錄等。
以下,則是我根據 **講述故事** 的方式,為微信登錄編寫的代碼:
```javascript
<?php
class Api_User_Login extends PhalApi_Api {
public function getRules() {
return array(
'weixin' => array(
'openId' => array('name' => 'wx_openid', 'require' => true, 'min' => 1, 'max' => 28),
'token' => array('name' => 'wx_token', 'require' => true, 'min' => 1, 'max' => 150),
'expiresIn' => array('name' => 'wx_expires_in', 'require' => true, 'min' => 1),
'nickname' => array('name' => 'name', 'default' => '',),
'avatar' => array('name' => 'avatar', 'default' => '',),
),
);
}
public function weixin()
{
$rs = array('code' => 0, 'info' => array(), 'msg' => '');
$domain = new Domain_User_Login_Weixin();
$isFirstBind = $domain->isFirstBind($this->openId);
$userId = 0;
if ($isFirstBind) {
$userId = Domain_User_Generator::createUserForWeixin(
$this->openId, $this->nickname, $this->avatar);
$domain->bindUser($userId, $this->openId, $this->token, $this->expiresIn);
} else {
$userId = $domain->getUserIdByWxOpenId($this->openId);
}
$token = Domain_User_Session::generate($userId, $this->client);
$rs['info']['user_id'] = $userId;
$rs['info']['token'] = $token;
$rs['info']['is_new'] = $isFirstBind ? 1 : 0;
return $rs;
}
}
```
> 溫馨提示:
> 以下代碼為我正在參與開發的一個項目的源代碼,已征得項目負責人同意。同時出于對項目的尊重,已省去部分代碼。
###(2)登錄場景的故事
細細品讀上面的代碼,其實就是在描述登錄場景的故事:
當用戶進行微信登錄時,先查看用戶是否首次登錄;如果是,則為用戶自動生成了一個帳號并綁定,如果不是,則獲取已綁定的用戶ID;最后,生成一個登錄態的token。
當然,這里為了突出故事的主線,已去除了很多異常情況的處理。
###(3)有趣的開發體驗
更為有趣的是,此次參與F項目開發的還有另外一位同學。
這位同學擁有多年資深的iOS開發經驗,但對PHP開發還是首次接觸,但他在參考微信登錄的寫法后,很快就交付了微博和QQ登錄這兩個接口服務。
但令我為之驚訝和興奮的不是他的速度,而是他所編寫的代碼是如此的優雅美麗,猶如出自資深PHP開發人員之手。
這讓我再一次相信,使用 **講述故事** 的方式來開發接口,不僅能讓代碼更易于傳送業務邏輯,也能為更多的同學乃至新手接受并快速上手。
###(4)與TDD的結合
**講述故事** 有一個很明顯的特點就是,全部的操作都是處于同一抽象級別的,即都是釋意接口下的領域業務規則和操作。
但對于如何引出這個業務場景,很多人用傳統的方式都是寫一個接口,然后在瀏覽器調試。
其實,這并不是最好的開發體驗。因為,使用這種傳統的開發方式,你難免會落入技術纏繞的糾結中,比如在想使用哪些類型的數據庫表字段。也就是說,你在丟失關注點。
而通過測試驅動,則會先引導你做正確的事,再將你的關注引導到領域業務上,最后將自然而然地就知道應用使用什么技術了。
講故事,是針Domain領域層外部使用的說明。下面,我們將走進Domain層內部,闡明我們應該如何為講故事做好準備。
##2.16.3 表達規則
###(1)釋意接口
釋意接口的作用是很大的,這可以使得后來的同學在看待一個接口時,無須深入內部實現即可明白它的用意和產生的影響。
如一個get系列的操作,我們可以推斷出它是無副作用的。但如果當時的開發者不遵守約定,在里面作了一些“手腳”,則會破壞我們這些“望文生義”的推斷。
在我曾經就職的一個游戲公司里面,我常根據接口的命名來推斷它的作用,但往往會倍受傷害。因為以前的開發人員沒有遵守這些約定,當時的team leader還責怪我不能太相信這些接口的命名。然而我想,如果我們都不能相信我們團隊其他人員的接口,我們又能相信誰呢?我們是否應該反思,是否應該遵守約定編程所帶來的好處?
任何一個問題,都不是個人的問題,而是一個團隊的問題。如果我們經常不斷地發生一生項目的問題而要去指責某個人時,我們又為何不從一開始就遵守約定而去避免呢?
簡單來說,釋意接口會將“命令-查詢”分離、會將多個操作分解成更小粒度的操作而保持同一層面的處理。根據《領域驅動設計》一書的說法:
_類型名、方法名和參數名一起構成了一個釋意接口(Intention-Revealing Interface),以解釋設計意圖,避免開發人員需要考慮內部如何實現,或者猜測。_
如下面的家庭組成員領域業務類:
```javascript
<?php
class Domain_Group_Member {
public function joinGroup($userId, $groupId) {
//TODO
}
public function hasJoined($userId, $groupId) {
//TODO
}
}
```
我們可以知道,Domain_Group_Member::joinGroup()用于加入家庭組,會產生副作用,是一個命令操作;Domain_Group_Member::hasJoined()用于檢測用戶是否已加入家庭組,無副作用,則是一個查詢操作。
###(2)業務規則的描述
_規則出現且僅出現一次。_
當代碼出現重復時,我們都知道會面臨維護的高成本。而當規則多次出現時,我們更知道當規則發生變化時所帶來的各種嚴重的問題,這也正是為什么總有一些這樣那樣的BUG的原因。
系統出現問題,大多數上都是業務的問題。而業務的問題在于我們不能把規則收斂起來,匯集于一處。
在以往的開發中,我都很注意對這些規則統一的重構工作。這使得我可以非常相信我所提供業務的穩定性,以及在給別人解講時的信心。
如有一次,我們有一個大型的系統中的一個頁面跳轉鏈接的生成規則,后來系統進行了調整,需要對URL生成規則作出調整。我跟另一位新來的同事說只需改一處時,他仍然很驚訝地問我怎么可能?!因為他看到是這么多場景,如此多的頁面,怕會有所遺漏。然而,事實證明,我們確實只需要改動一處就可以了。
類似這樣的URL拼接規則,我們可以這樣表示:
```javascript
<?php
class Domain_Page_Helper {
public static function createUrl($userId) {
return DI()->config->get('app.web.host') . '/u/' . $userId;
}
}
```
正如你看到了,我們使用了static靜態方法,是因為這個規則生成可以當作一個工具方法來使用。我們不反對使用static方法,但推薦只在合適的時候使用。
規則出現且僅出現一次,可以說是一個知易行難的做法,因為我們總會有不經意間重復實現規則。有時我們會忽略已有的規則,有時我們會出于當前緊張開發進度的考慮,有時我們可能懶得去統一。
但把規則的實現統一起來,再重復調用,會讓你在今后的項目開發中,長期收益。沒錯,真的會長期收益。
##2.16.4 不可變值與無狀態操作
###(1)在開源中國上翻譯的兩點收獲
首先,讓我們簡單來了解一下PHP語言的運行機制。
PHP是一個運行于服務端的腳本解析語言,每一個HTTP請求都會觸發一個php-fpm進程來響應,所以不同于其他長時間運行的語言或者系統,不用過多地考慮內存的回收或者對實體的管理和共享。
這樣是有明顯的好處,作為PHP開發人員,由于每一次請求所消耗的內存都會在本次釋放,即使運行錯誤也不會影響其他的調用。從而,我們可以放心快速地開發。
但我們也應該看到這樣的便利給很多開發同學所帶來的誤導。正因為不用再擔心一些傳統的問題(如內存管理),他們變得更無限制。當這種無限制日積月累而引發諸多項目的問題時,他們會開始責怪PHP這門語言。
其實,語言本身沒有對錯,關鍵在于我們怎么使用。
先前,在開源中國進行翻譯時,我從翻譯的文章中收獲了兩點。
+ 第一點是,學習不同的語言,你將會獲得不同的靈感。比如,你是OC的開發人員,可以從GO語言中獲得OC的開發靈感;反之亦然。
+ 第二點是,不可變值的使用。
這里,我將嘗試說明如何在PhalApi現有的分層機制基礎上,結合不可變值和無狀態,應對復雜的領域業務開發。
###(2)不可變值
通常,我們在程序中處理的變量可以分為:值和實體。簡單來說,值是一些基本的類型,如整數、布爾值、字符串;實體則是類對象,有自己內部的狀態。當一個實體表示一個值的概念時(如坐標、金額、日期等),我們可以稱之為值對象。
明顯地,系統的復雜性不在于對值的處理,而在于對一系列實體以及與其關聯的另一系列實體間的處理。
如同其他語言一樣,如果我們也在PHP遵循 **不可變值** 與 **無狀態** 這兩個用法,我們的系統乃至業務都可以從中獲益。
**不可變值** 是指一個實體在創建后,其內部的狀態是不可變更的,這樣就能在系統內放心地流通使用,而無須擔心有副作用。
舉個簡單的例子,在我們國際交易系統中有一個金額為100RMB的對象,表示用戶此次轉賬的金額。如果此對象是不可變值,那么我們在系統內,無論是計算手費、日志紀錄,還是轉賬事務或其他,我們都能信任此對象放心使用,不用擔心哪里作了篡改而導致一個隱藏的致使BUG。
也就說,當你需要修改此類對象時,你需要復制一個再改之。有人會擔心new所帶來的內存消耗,但實際上,new一個只有一些屬性的對象消耗很少很少。
要明白為什么在修改前需要再創建新的對象,也是很容易理解的。首先,我們保持了和基本類型一致的處理方式;其次,我們保持了概念的一致性,如坐標A(1,2)和坐標B(1,3)是兩個不同的坐標。
當坐標A發生改變,坐標A就不再是原來的坐標A,而是一個新的坐標。從哲學角度上看,這是兩個不同的概念。
在PhalApi中,我們可以看到不可變值在Query對象中的應用:
```javascript
$query1 = new PhalApi_ModelQuery();
$query1->id = 1;
$query2 = new PhalApi_ModelQuery($query1->toArray());
$query2->id = 2;
```
這樣以后,我們就不再需要小心翼翼維護“漂洋過海”的值對象了,而是可以輕松地逐層傳遞,這有點像網絡協議的逐層組裝。
這又讓我想起了《領域驅動設計》一書中較為中肯的說法:
_把值對象看成是不可變的。不要給它任何標識,這樣可以避免實體的維護工作,降低設計的復雜性。_
###(3)無狀態操作
前面提到了PHP的運行機制,不同于長時間運行的語言或系統,PHP很少會在不同的php-fpm進程中共享實體,最多也只是在同一次請求中共享。
這樣,當我們在一次請求中需要處理兩個或兩個以上的用戶實體時,可以怎么應對呢?
關于對實體的追蹤和識別,可以使用ORM進行實體與關系數據庫映射,但PhalApi弱化了這種映射,取而代之的是更明朗的處理方式,即: **無狀態操作** 。
因為PhalApi都是通過“空洞”的實體來獲得數據,即實體無內部屬性,對數據庫的處理采用了 **表數據入口模式** 。
當我們需要獲取兩個用戶的信息時,可以這樣:
```javascript
$model = new Model_User();
$user1 = $model->get(1); //$user1是一個數組
$user2 = $model->get(2);
//而不是
$user1 = new Model_User(1); //$user1是一個對象
$user2 = new Model_User(2);
//或者可以這樣批量獲取
$users = $model->multiGet(array(1, 2)); //$users是一個二維數組,下標是用戶的ID
```
這樣做,沒有絕對的對錯,可以根據你的項目應用場景作出調整。但我覺得無狀態在PhalApi應用,可以更簡單便捷地處理各種數據以及規則的統一,以實現操作的無狀態。因為:
+ 1、可以按需取得不同的字段,多個獲取時可以使用批量獲取
+ 2、在單次請求處理中,簡化對實體的追蹤和維護
+ 3、換種方式來獲得不可變值性的好處,因為既然沒有內部狀態,就沒有改變了
###(4)引申到Domain層
Domain層作為ADM(Api-Domain-Model)分層中的橋梁,主要負責處理業務規則。
將值對象與無狀態操作引申到Domain層,同樣有處于簡化我們對數據和業務規則的處理。
我們可以根據上述的家庭組成員領域類來完成類似下面功能場景的業務需求:
```javascript
$domain = new Domain_Group_Member();
if (!$domain->hasJoined(1, 100)) {
$domain->joinGroup(1, 100);
}
if (!$domain->hasJoined(2, 100)) {
$domain->joinGroup(2, 100);
}
if (!$domain->hasJoined(3, 100)) {
$domain->joinGroup(3, 100);
}
```
即:如果用戶1還沒加入過組100,那么就允許他加入。用戶2、用戶3也以此類推。
當我們把業務規則,劃分為更細的維度時,我們可以輕松上在業務層組裝不同的功能,講述不同的故事。
##2.16.5 越痛苦的事情,越早做
在有一次敏捷開發分享會上,有位前輩說:要對小問題不斷進行優化迭代,而不要等到大問題來了再作變革。
也就是我們常聽說的黃金法則: **演變優于劇變** 。
同樣,在持續集成中,也提倡著類似的理念,即:越痛苦的事情,越早做。
有時,對自己狠一點,是會有所收獲的。
我們都贊揚美好的事物,但我們很少也會那樣去做,因為我們知道美好需要付出更多的努力,意味著有所犧牲。
如很多女生,都很喜歡苗條的身材,卻總忍不住零食的誘惑,也很難堅持鍛煉。
我們很多人都喜歡優雅的代碼,自已卻也會寫下一些臨時性的代碼,而沒多及時清理代碼的異味,也沒有嘗試去重構,更沒有堅持單元測試。
從另外一個角度說,如果一個項目的問題,我們在前期及時溝通并解決的話,根本不值得過多去關注。但若我們因為團隊關系或者心煩意亂有意識去抵觸多變的需求時,一個很小的問題,到了上線后,就可能會演變成一個災難。
到了那時,即使只是一行代碼的改變,也會涉及到開發、測試、產品、運維、用戶、商務、老板等等一系列的干涉人。為了修復上線,我們還要走一系列的發布流程,事后還需要為這樣的故障買單。
既然如此,明明知道當初一個不確定的需求時,為什么沒去及時處理呢?
我知道,我相信我們大家也知道,程序員總會有一些很煩很想抵觸的時候,這時我們會拒絕改變,拒絕去做一些正確的事情。
但越痛苦的事情,越早做,不僅僅需要我們增強對自身的情商控制,更讓我們能很好地應對高價值的系統。
試想,誰會把一個影響到千千萬萬用戶、涉及到動則百萬金額的系統交給一個動不動就發脾氣的人呢?
所以,對自己“狠”一點吧,明天的你,將會感謝今天努力的你。
## 2.16.6 收篇
領域驅動設計所提供的思想、概念和設計非常廣泛,這里不能一一說明。本章僅僅是摘取其中的部分內容進行再傳播,以喚醒入門同學的注意,培養一種約定編程的意識。
更為重要的是,要懂得去學習,學習后應用到自己當前的工作或項目中,慢慢地你將體會到開發編程的樂趣。
- 歡迎使用PhalApi!
- 接口,從簡單開始!
- [1.1]-下載與安裝
- [1.2]-創建一個自己的項目
- [1.3]-在線體驗
- [1.4]-文檔、幫助和官網
- [1.10]-對PhalApi框架的抉擇
- [1.11]-快速入門(backup)
- [1.12]-參數規則:接口參數規則配置
- [1.13]-統一的接口請求方式:_sevice=XXX.XXX
- [1.14]-統一的返回格式和結構:ret-data-msg
- [1.15]-數據庫操作:基于NotORM的使用及優化
- [1.16]-配置讀取:內外網環境配置的完美切換
- [1.17]-日記紀錄:簡化版的日記接口
- [1.18]-快速函數:人性化的關懷
- [1.19]-DI服務速查:各資源服務一覽表
- [1.20]-DB操作:數據庫基本操作速查
- [1.21]-類的自動加載:遵循PEAR包的命名規范
- [1.22]-簽名驗證:自定義簽名規則
- [1.23]-請求和響應:GET和POST兩者皆可得及超越JSON格式返回
- [1.24]-緩存策略:更靈活地可配置化的多級緩存
- [1.25]-國際化翻譯:為走向國際化提前做好翻譯準備
- [1.26]-數據安全:數據對稱加密方案
- [1.27]-精益開發:更富表現力的Model層和重量級數據獲取的應對方案
- [1.28]-COOKIE:對COOKIE原生態的支持及記憶加密升級版
- [1.29]-開放與封閉:多入口和統一初始化
- [1.30]-保持的力量:接口開發最佳實踐
- [1.31]-新型計劃任務:以接口形式實現的計劃任務
- [2.11]-核心思想:DI依賴注入-讓資源更可控
- [2.12]-海量數據:可配置的分庫分表
- [2.13]-接口調試:在線SQL語句查看與性能優化
- [2.14]-測試驅動開發:意圖導向編程下的接口開發
- [2.15]-演進:新型計劃任務續篇
- [2.16]-領域驅動設計:應對復雜領域業務的Domain層
- [2.17]-微服務:Api接口服務層
- [2.18]-定制化:資源服務的再實現
- [2.19]-擴展庫:可重用的擴展類庫
- [2.20]-約定編程:架構明顯的編程風格
- [2.21]-服務器統一部署方案簡明版:CentOs---Nginx---php-fpm---MySql-[--Memcached]
- [2.22]-更多工具:精益項目和團隊建設
- [3.1]-擴展類庫:微信開發
- [3.2]-擴展類庫:代理模式下phprpc協議的輕松支持
- [3.3]-擴展類庫:基于PHPMailer的郵件發送
- [3.4]-擴展類庫:優酷開放平臺接口調用
- [3.5]-擴展類庫:七牛云存儲接口調用
- [3.6]-擴展類庫:新型計劃任務
- [3.8]-擴展類庫:用戶、會話和第三方登錄集成
- [3.9]-擴展類庫:swoole支持下的長鏈接和異步任務實現
- [3.11]-擴展類庫:基于FastRoute的快速路由
- [4.2]-開發實戰2:模擬優酷開放平臺接口項目開發
- [4.3]-開發實戰3:一個簡單的小型項目開發(奔跑吧兄弟投票活動)
- [5.1]-架構與思想:PhalApi核心設計和思想解讀
- [5.2]-雜談:扯一些PhalApi的前世和今生
- [5.3]-框架總結:術語表和PHP開發建議
- [5.4]-許可
- [5.5]-聯系和加入我們
- [5.6]-更新日記
- [5.8]-致框架貢獻者:加入PhalApi開源指南
- [6.1]-基于接口查詢語言的SDK包
- [6.2]-SDK包(JAVA版)
- [6.3]-SDK包(PHP版)
- [6.4]-SDK包(Objective-C版)
- [6.5]-SDK包(javascript版)
- [6.6]-SDK包(Ruby版)
- [8.1]-PhalApi視頻教程
- 附錄1:接口文檔參考模板