溫馨提示:此篇章需要比較長的時間才能最終定稿,因為我還要尋找最合適的方式和語言來表述。
很多框架關(guān)心性能,而不關(guān)心人文;很多項目關(guān)心技術(shù),而不關(guān)注業(yè)務。
就這造成了復雜的領(lǐng)域業(yè)務在項目中得不到很好地體現(xiàn)和描述,也沒有統(tǒng)一的規(guī)則,更沒有釋意的接口。最終導致了在“純面向?qū)ο蟆笨蚣芾锩媪鑱y的代碼編寫,為后期的維護擴展、升級優(yōu)化帶來很大的阻礙。這就變成了,框架只關(guān)注性能,項目只關(guān)心技術(shù),而項目卻可憐地失去了演進的權(quán)利,慢慢地步履維艱,最終牽一發(fā)而動全身。
很多人都不知道該如何真正應對和處理領(lǐng)域的業(yè)務 ,盡管領(lǐng)域業(yè)務和單元測試都是如此重要并被廣泛推崇。正如同表面上我們都知道單元測試卻沒有具體真實地接觸過,并且一旦到真正需要編寫一行單元測試的代碼時就傻眼了。
這里不是發(fā)明一些新技術(shù),也不是提供一些新的模式,而是繼續(xù)將前人、大神和頂級大師關(guān)于領(lǐng)域驅(qū)動設(shè)計這方面的思想結(jié)合真實后臺接口開發(fā)進行分享,進而推廣之。
很多人,都喜歡聽故事。像我以前中學的時候,就很喜歡看《故事會》。
如果,我們能讓代碼也像小說一樣,在講述某個故事時,將會更加吸引“讀者”(也就是其他開發(fā)同學),從而易于理解和維護。
最近,我在做一個項目時,再一次發(fā)現(xiàn)了這種講述故事的威力。
我們先以F項目來命名這個項目,在F項目中,我們跟其他App一樣,需要接入第三方登錄,其中包括:微信登錄、微博登錄和QQ登錄、郵箱登錄等。
以下,則是我根據(jù) 講述故事 的方式,為微信登錄編寫的代碼:
<?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;
}
}
溫馨提示:
以下代碼為我正在參與開發(fā)的一個項目的源代碼,已征得項目負責人同意。同時出于對項目的尊重,已省去部分代碼。
細細品讀上面的代碼,其實就是在描述登錄場景的故事:
當用戶進行微信登錄時,先查看用戶是否首次登錄;如果是,則為用戶自動生成了一個帳號并綁定,如果不是,則獲取已綁定的用戶ID;最后,生成一個登錄態(tài)的token。
當然,這里為了突出故事的主線,已去除了很多異常情況的處理。
更為有趣的是,此次參與F項目開發(fā)的還有另外一位同學。
這位同學擁有多年資深的iOS開發(fā)經(jīng)驗,但對PHP開發(fā)還是首次接觸,但他在參考微信登錄的寫法后,很快就交付了微博和QQ登錄這兩個接口服務。
但令我為之驚訝和興奮的不是他的速度,而是他所編寫的代碼是如此的優(yōu)雅美麗,猶如出自資深PHP開發(fā)人員之手。
這讓我再一次相信,使用 講述故事 的方式來開發(fā)接口,不僅能讓代碼更易于傳送業(yè)務邏輯,也能為更多的同學乃至新手接受并快速上手。
講述故事 有一個很明顯的特點就是,全部的操作都是處于同一抽象級別的,即都是釋意接口下的領(lǐng)域業(yè)務規(guī)則和操作。
但對于如何引出這個業(yè)務場景,很多人用傳統(tǒng)的方式都是寫一個接口,然后在瀏覽器調(diào)試。
其實,這并不是最好的開發(fā)體驗。因為,使用這種傳統(tǒng)的開發(fā)方式,你難免會落入技術(shù)纏繞的糾結(jié)中,比如在想使用哪些類型的數(shù)據(jù)庫表字段。也就是說,你在丟失關(guān)注點。
而通過測試驅(qū)動,則會先引導你做正確的事,再將你的關(guān)注引導到領(lǐng)域業(yè)務上,最后將自然而然地就知道應用使用什么技術(shù)了。
講故事,是針Domain領(lǐng)域?qū)油獠渴褂玫恼f明。下面,我們將走進Domain層內(nèi)部,闡明我們應該如何為講故事做好準備。
釋意接口的作用是很大的,這可以使得后來的同學在看待一個接口時,無須深入內(nèi)部實現(xiàn)即可明白它的用意和產(chǎn)生的影響。
如一個get系列的操作,我們可以推斷出它是無副作用的。但如果當時的開發(fā)者不遵守約定,在里面作了一些“手腳”,則會破壞我們這些“望文生義”的推斷。
在我曾經(jīng)就職的一個游戲公司里面,我常根據(jù)接口的命名來推斷它的作用,但往往會倍受傷害。因為以前的開發(fā)人員沒有遵守這些約定,當時的team leader還責怪我不能太相信這些接口的命名。然而我想,如果我們都不能相信我們團隊其他人員的接口,我們又能相信誰呢?我們是否應該反思,是否應該遵守約定編程所帶來的好處?
任何一個問題,都不是個人的問題,而是一個團隊的問題。如果我們經(jīng)常不斷地發(fā)生一生項目的問題而要去指責某個人時,我們又為何不從一開始就遵守約定而去避免呢?
簡單來說,釋意接口會將“命令-查詢”分離、會將多個操作分解成更小粒度的操作而保持同一層面的處理。根據(jù)《領(lǐng)域驅(qū)動設(shè)計》一書的說法:
類型名、方法名和參數(shù)名一起構(gòu)成了一個釋意接口(Intention-Revealing Interface),以解釋設(shè)計意圖,避免開發(fā)人員需要考慮內(nèi)部如何實現(xiàn),或者猜測。
如下面的家庭組成員領(lǐng)域業(yè)務類:
<?php
class Domain_Group_Member {
public function joinGroup($userId, $groupId) {
//TODO
}
public function hasJoined($userId, $groupId) {
//TODO
}
}
我們可以知道,Domain_Group_Member::joinGroup()用于加入家庭組,會產(chǎn)生副作用,是一個命令操作;Domain_Group_Member::hasJoined()用于檢測用戶是否已加入家庭組,無副作用,則是一個查詢操作。
規(guī)則出現(xiàn)且僅出現(xiàn)一次。
當代碼出現(xiàn)重復時,我們都知道會面臨維護的高成本。而當規(guī)則多次出現(xiàn)時,我們更知道當規(guī)則發(fā)生變化時所帶來的各種嚴重的問題,這也正是為什么總有一些這樣那樣的BUG的原因。
系統(tǒng)出現(xiàn)問題,大多數(shù)上都是業(yè)務的問題。而業(yè)務的問題在于我們不能把規(guī)則收斂起來,匯集于一處。
在以往的開發(fā)中,我都很注意對這些規(guī)則統(tǒng)一的重構(gòu)工作。這使得我可以非常相信我所提供業(yè)務的穩(wěn)定性,以及在給別人解講時的信心。
如有一次,我們有一個大型的系統(tǒng)中的一個頁面跳轉(zhuǎn)鏈接的生成規(guī)則,后來系統(tǒng)進行了調(diào)整,需要對URL生成規(guī)則作出調(diào)整。我跟另一位新來的同事說只需改一處時,他仍然很驚訝地問我怎么可能?!因為他看到是這么多場景,如此多的頁面,怕會有所遺漏。然而,事實證明,我們確實只需要改動一處就可以了。
類似這樣的URL拼接規(guī)則,我們可以這樣表示:
<?php
class Domain_Page_Helper {
public static function createUrl($userId) {
return DI()->config->get('app.web.host') . '/u/' . $userId;
}
}
正如你看到了,我們使用了static靜態(tài)方法,是因為這個規(guī)則生成可以當作一個工具方法來使用。我們不反對使用static方法,但推薦只在合適的時候使用。
規(guī)則出現(xiàn)且僅出現(xiàn)一次,可以說是一個知易行難的做法,因為我們總會有不經(jīng)意間重復實現(xiàn)規(guī)則。有時我們會忽略已有的規(guī)則,有時我們會出于當前緊張開發(fā)進度的考慮,有時我們可能懶得去統(tǒng)一。
但把規(guī)則的實現(xiàn)統(tǒng)一起來,再重復調(diào)用,會讓你在今后的項目開發(fā)中,長期收益。沒錯,真的會長期收益。
首先,讓我們簡單來了解一下PHP語言的運行機制。
PHP是一個運行于服務端的腳本解析語言,每一個HTTP請求都會觸發(fā)一個php-fpm進程來響應,所以不同于其他長時間運行的語言或者系統(tǒng),不用過多地考慮內(nèi)存的回收或者對實體的管理和共享。
這樣是有明顯的好處,作為PHP開發(fā)人員,由于每一次請求所消耗的內(nèi)存都會在本次釋放,即使運行錯誤也不會影響其他的調(diào)用。從而,我們可以放心快速地開發(fā)。
但我們也應該看到這樣的便利給很多開發(fā)同學所帶來的誤導。正因為不用再擔心一些傳統(tǒng)的問題(如內(nèi)存管理),他們變得更無限制。當這種無限制日積月累而引發(fā)諸多項目的問題時,他們會開始責怪PHP這門語言。
其實,語言本身沒有對錯,關(guān)鍵在于我們怎么使用。
先前,在開源中國進行翻譯時,我從翻譯的文章中收獲了兩點。
這里,我將嘗試說明如何在PhalApi現(xiàn)有的分層機制基礎(chǔ)上,結(jié)合不可變值和無狀態(tài),應對復雜的領(lǐng)域業(yè)務開發(fā)。
通常,我們在程序中處理的變量可以分為:值和實體。簡單來說,值是一些基本的類型,如整數(shù)、布爾值、字符串;實體則是類對象,有自己內(nèi)部的狀態(tài)。當一個實體表示一個值的概念時(如坐標、金額、日期等),我們可以稱之為值對象。
明顯地,系統(tǒng)的復雜性不在于對值的處理,而在于對一系列實體以及與其關(guān)聯(lián)的另一系列實體間的處理。
如同其他語言一樣,如果我們也在PHP遵循 不可變值 與 無狀態(tài) 這兩個用法,我們的系統(tǒng)乃至業(yè)務都可以從中獲益。
不可變值 是指一個實體在創(chuàng)建后,其內(nèi)部的狀態(tài)是不可變更的,這樣就能在系統(tǒng)內(nèi)放心地流通使用,而無須擔心有副作用。
舉個簡單的例子,在我們國際交易系統(tǒng)中有一個金額為100RMB的對象,表示用戶此次轉(zhuǎn)賬的金額。如果此對象是不可變值,那么我們在系統(tǒng)內(nèi),無論是計算手費、日記紀錄,還是轉(zhuǎn)賬事務或其他,我們都能信任此對象放心使用,不用擔心哪里作了篡改而導致一個隱藏的致使BUG。
也就說,當你需要修改此類對象時,你需要復制一個再改之。有人會擔心new所帶來的內(nèi)存消耗,但實際上,new一個只有一些屬性的對象消耗很少很少。
要明白為什么在修改前需要再創(chuàng)建新的對象,也是很容易理解的。首先,我們保持了和基本類型一致的處理方式;其次,我們保持了概念的一致性,如坐標A(1,2)和坐標B(1,3)是兩個不同的坐標。
當坐標A發(fā)生改變,坐標A就不再是原來的坐標A,而是一個新的坐標。從哲學角度上看,這是兩個不同的概念。
在PhalApi中,我們可以看到不可變值在Query對象中的應用:
$query1 = new PhalApi_ModelQuery();
$query1->id = 1;
$query2 = new PhalApi_ModelQuery($query1->toArray());
$query2->id = 2;
這樣以后,我們就不再需要小心翼翼維護“漂洋過?!钡闹祵ο罅?,而是可以輕松地逐層傳遞,這有點像網(wǎng)絡協(xié)議的逐層組裝。
這又讓我想起了《領(lǐng)域驅(qū)動設(shè)計》一書中較為中肯的說法:
把值對象看成是不可變的。不要給它任何標識,這樣可以避免實體的維護工作,降低設(shè)計的復雜性。
前面提到了PHP的運行機制,不同于長時間運行的語言或系統(tǒng),PHP很少會在不同的php-fpm進程中共享實體,最多也只是在同一次請求中共享。
這樣,當我們在一次請求中需要處理兩個或兩個以上的用戶實體時,可以怎么應對呢?
關(guān)于對實體的追蹤和識別,可以使用ORM進行實體與關(guān)系數(shù)據(jù)庫映射,但PhalApi弱化了這種映射,取而代之的是更明朗的處理方式,即: 無狀態(tài)操作 。
因為PhalApi都是通過“空洞”的實體來獲得數(shù)據(jù),即實體無內(nèi)部屬性,對數(shù)據(jù)庫的處理采用了 表數(shù)據(jù)入口模式 。
當我們需要獲取兩個用戶的信息時,可以這樣:
$model = new Model_User();
$user1 = $model->get(1); //$user1是一個數(shù)組
$user2 = $model->get(2);
//而不是
$user1 = new Model_User(1); //$user1是一個對象
$user2 = new Model_User(2);
//或者可以這樣批量獲取
$users = $model->multiGet(array(1, 2)); //$users是一個二維數(shù)組,下標是用戶的ID
這樣做,沒有絕對的對錯,可以根據(jù)你的項目應用場景作出調(diào)整。但我覺得無狀態(tài)在PhalApi應用,可以更簡單便捷地處理各種數(shù)據(jù)以及規(guī)則的統(tǒng)一,以實現(xiàn)操作的無狀態(tài)。因為:
Domain層作為ADM(Api-Domain-Model)分層中的橋梁,主要負責處理業(yè)務規(guī)則。
將值對象與無狀態(tài)操作引申到Domain層,同樣有處于簡化我們對數(shù)據(jù)和業(yè)務規(guī)則的處理。
我們可以根據(jù)上述的家庭組成員領(lǐng)域類來完成類似下面功能場景的業(yè)務需求:
$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也以此類推。
當我們把業(yè)務規(guī)則,劃分為更細的維度時,我們可以輕松上在業(yè)務層組裝不同的功能,講述不同的故事。
在有一次敏捷開發(fā)分享會上,有位前輩說:要對小問題不斷進行優(yōu)化迭代,而不要等到大問題來了再作變革。
同樣,在持續(xù)集成中,也提倡著類似的理念,即:越痛苦的事情,越早做。
有時,對自己狠一點,是會有所收獲的。
我們都贊揚美好的事物,但我們很少也會那樣去做,因為我們知道美好需要付出更多的努力,意味著者犧牲。
如很多女生,都很喜歡苗條的身材,卻總?cè)滩蛔×闶车恼T惑,也很難堅持鍛煉。
我們很多人都喜歡優(yōu)雅的代碼,自動卻也總會寫下一些臨時性的代碼,而沒多及時清理代碼的異味,也沒有嘗試去重構(gòu),更沒有堅持單元測試。
從另外一個角度說,如果一個項目的問題,我們在前期及時溝通并解決的話,根本不值得過多去關(guān)注。但若我們因為團隊關(guān)系或者心煩意亂有意識去抵觸多變的需求時,一個很小的問題,到了上線后,就可能會演變成一個災難。
到了那時,即使只是一行代碼的改變,也會涉及到開發(fā)、測試、產(chǎn)品、運維、用戶、商務、老板等等一系列的干涉人。為了修復上線,我們還要走一系列的發(fā)布流程,事后還需要為這樣的故障買單。
既然如此,明明知道當初一個不確定的需求時,為什么沒去及時處理呢?
我知道,我相信我們大家也知道,程序員總會有一些很煩很想抵觸的時候,這時我們會拒絕改變,拒絕去做一些正確的事情。
但越痛苦的事情,越早做,不僅僅需要我們增強對自身的情商控制,更讓我們能很好地應對高價值的系統(tǒng)。
試想,誰會把一個影響到千千萬萬用戶、涉及到動則百萬金額的系統(tǒng)交給一個動不動就發(fā)脾氣的人呢?
所以,對自己“狠”一點吧,明天的你,將會感謝今天努力的你。
領(lǐng)域驅(qū)動設(shè)計所提供的思想、概念和設(shè)計非常廣泛,這里不能一一說明。本章僅僅是摘取其中的部分內(nèi)容進行再傳播,以喚醒入門同學的注意,培養(yǎng)一種約定編程的意識。
更為重要的是,要懂得去學習,學習后應用到自己當前的工作或項目中,慢慢地你將會體會到開發(fā)的樂趣。
更多建議: