我發(fā)現(xiàn),我越是努力,就越發(fā)幸運(yùn)。 -- Thomas Jefferson
Martin Fowler(我喜歡和敬仰的大師)曾發(fā)表了上面這一段話。這段話也出現(xiàn)在了2015年QCon分享會(huì)上,并加了一張PPT“什么是微服務(wù)”加以說明。
里面提到了 微服務(wù) 這個(gè)概念,在PhalApi框架中即對(duì)應(yīng)我們的Api接口服務(wù)層,只是我們不是稱之為微服務(wù),而是接口服務(wù)。
不管何種說法,我們都應(yīng)該關(guān)注里面提及到的這幾點(diǎn)重要特質(zhì):
這里不過多地討論微服務(wù)相關(guān)的分享,而是重溫接口服務(wù)層Api與領(lǐng)域驅(qū)動(dòng)和單元測(cè)試之間的關(guān)系,以及如何開發(fā)一個(gè)優(yōu)雅、穩(wěn)定又簡(jiǎn)單的接口。
整體上講根據(jù)從Api接口層、Domain領(lǐng)域?qū)釉俚組odel數(shù)據(jù)源層的順序進(jìn)行開發(fā)。
在開發(fā)過程中,需要注意不能 越層調(diào)用 也不能 逆向調(diào)用 ,即不能Api調(diào)用Model。而應(yīng)該是 上層調(diào)用下層,或者同層級(jí)調(diào)用 ,也就是說,我們應(yīng)該:
如果用一張圖來表示,則是:
為了更明確調(diào)用的關(guān)系,以下調(diào)用是 錯(cuò)誤 的:
這樣的約定,便于我們形成統(tǒng)一的開發(fā)規(guī)范,降低學(xué)習(xí)維護(hù)成本。
比如需要添加緩存,我們知道應(yīng)該定位到Model層數(shù)據(jù)源進(jìn)行擴(kuò)展;若發(fā)現(xiàn)業(yè)務(wù)規(guī)則處理不當(dāng),則應(yīng)該進(jìn)入Domain層探其究竟;如果需要對(duì)接口的參數(shù)進(jìn)行調(diào)整,即使是新手也知道應(yīng)該找到對(duì)應(yīng)的Api文件進(jìn)行改動(dòng)。
現(xiàn)實(shí)項(xiàng)目開發(fā)過程中,絕大部分我們編寫的接口是給別人使用的,或許給Android客戶端同學(xué)使用,或者給iOS客戶端同學(xué)使用,抑或提供給其他后臺(tái)系統(tǒng)的同學(xué)使用。
為了提高并行開發(fā)的速度,我們不能等待接口完全開發(fā)完成后才提供接口文檔,而且他們也不能忍受這么漫長(zhǎng)的等待。
所以,客戶端同學(xué)時(shí)常會(huì)問我們:什么時(shí)候可以提供接口文檔?
我們提倡“接口先行”,如果有時(shí)不能很好地做到這一點(diǎn)(畢竟多變的需求促發(fā)多變的情境),我們可以快速提供接口的定義。
這有點(diǎn)像規(guī)約層對(duì)接口的定義一樣,在PhalApi中定義一個(gè)接口,再具體一點(diǎn)即:
簡(jiǎn)單來說,就是創(chuàng)建一個(gè)類,寫個(gè)函數(shù),定義參數(shù)和返回結(jié)果。
下面以 開發(fā)實(shí)戰(zhàn)3:一個(gè)簡(jiǎn)單的小型項(xiàng)目開發(fā)(奔跑吧兄弟投票活動(dòng)) 中的團(tuán)隊(duì)參賽接口為例,說明這三步操作的過程。
//$ vim ./Vote/Api/Act.php
<?php
class Api_Act extends PhalApi_Api {
public function joinIn() {
}
}
<?php
class Api_Act extends PhalApi_Api {
public function getRules() {
return array(
'joinIn' => array(
'teamName' => array('name' => 'team_name', 'require' => true, 'min' => 1, 'max' => 100),
),
);
}
public function joinIn() {
}
}
<?php
class Api_Act extends PhalApi_Api {
public function getRules() {
return array(
'joinIn' => array(
'teamName' => array('name' => 'team_name', 'require' => true, 'min' => 1, 'max' => 100),
),
);
}
/**
* 團(tuán)隊(duì)參賽接口
*
* @return int code 0,參賽成功;1,隊(duì)名已存在
* @return int team_id 新建的團(tuán)隊(duì)ID
*/
public function joinIn() {
}
}
在完成上面的動(dòng)作后,我們可以通過在線工具來看下實(shí)時(shí)的效果,在瀏覽打開后訪問:
http://api.vote.phalapi.com/vote/checkApiParams.php?service=Act.JoinIn
可以看到:
到了這里,即使我們未完成接口的開發(fā),也未提供更完善的接口文檔,但接口客戶端同學(xué)也可以根據(jù)這個(gè)在線的接口參數(shù)進(jìn)行開發(fā)了。
我們一直推崇測(cè)試驅(qū)動(dòng)開發(fā),但在對(duì)于Api接口開發(fā),更準(zhǔn)確來說是ATDD,即:驗(yàn)收測(cè)試驅(qū)動(dòng)開發(fā)(Acceptance Test Driven Development)。
在前面Domain層文檔中,我們提到了Api層是講述故事的場(chǎng)景。因此,為了驗(yàn)證我們的業(yè)務(wù)場(chǎng)景是否正確,我們應(yīng)該事先編寫好單元測(cè)試,以不斷引導(dǎo)我們前往正確的目的地。
我們可以使用腳本來快速生成測(cè)試骨架:
$ pwd
$ /path/to/api.vote.phalapi.com/Vote/Tests/Api
$ phalapi-buildtest ../../Api/Act.php Api_Act ../test_env.php > Api_Act_Test.php
然后,稍微修改完善測(cè)試場(chǎng)景:
/**
* @group testJoinIn
*/
public function testJoinIn()
{
//Step 1. 構(gòu)建請(qǐng)求URL
$url = 'service=Act.JoinIn';
$params = array(
'sign' => 'phalapi',
'team_name' => 'test team name',
'user_id' => '1',
'token' => '193CE82D1F4588A9A168BDE6E6B83868B1464F523D16C05206F308E51EB91731',
);
DI()->notorm->team->where('team_name', $params['team_name'])->delete();
//Step 2. 執(zhí)行請(qǐng)求
$rs = PhalApiTestRunner::go($url, $params);
//var_dump($rs);
//Step 3. 驗(yàn)證
$this->assertNotEmpty($rs);
$this->assertArrayHasKey('code', $rs);
$this->assertArrayHasKey('team_id', $rs);
$this->assertEquals(0, $rs['code']);
$this->assertGreaterThan(0, $rs['team_id']);
//create again
$rs = PhalApiTestRunner::go($url, $params);
$this->assertEquals(1, $rs['code']);
}
從上面測(cè)試的代碼可以看出,我們先后進(jìn)行了兩次報(bào)名。明顯地,第一次報(bào)名應(yīng)該是成功的,第二次則應(yīng)該提示不能重復(fù)報(bào)名。
單元測(cè)試的好處,不但在于可以引導(dǎo)我們做正確的事情,還可以提高我們的關(guān)注點(diǎn),不致于在開發(fā)過程中被各種事務(wù)(如臨時(shí)性的會(huì)議)打斷后回來卻不知?jiǎng)偛砰_發(fā)到哪了。
然而,更多的是為后期的維護(hù)、擴(kuò)展提供可驗(yàn)證的業(yè)務(wù)場(chǎng)景。這點(diǎn)是很重要的。因?yàn)槊恳粋€(gè)測(cè)試場(chǎng)景,都保存了對(duì)應(yīng)場(chǎng)景的模擬信息,這樣不僅僅在后面的擴(kuò)展,還是突如其來的BUGFIXED,我們都可以快速證明我們的修改是正確的,至少不會(huì)影響到原來的業(yè)務(wù)流程。
試想一下,如果原來可以下單支付的接口,突然被影響到而導(dǎo)致支付不成功,這是何等的損失!
我現(xiàn)在慢慢地,每當(dāng)需要修改別人的代碼時(shí),我都會(huì)看下有沒有對(duì)應(yīng)的單元測(cè)試。如果沒有,我會(huì)先補(bǔ)回,這樣能增強(qiáng)我修改別人代碼的信心。
傳統(tǒng)的接口開發(fā),由于沒有很好的分層結(jié)構(gòu),而且熱衷于在一個(gè)文件里面完成絕大部分事情,最終導(dǎo)致了臃腫漫長(zhǎng)的代碼,也就是通常所說的意大利面條式的代碼。
在PhalApi中,我們針對(duì)接口領(lǐng)域開發(fā),提供了新的分層思想:ADM(Api - Domain - Model)。
即便這樣,如果項(xiàng)目在實(shí)際開發(fā)中,仍然使用原來的做法,縱使使用再好的接口開發(fā)框架,也還是會(huì)退化到原來的局面。
為了能讓大家更為明確Api接口層的職責(zé)所在,我們建議:
在明確了上面應(yīng)該做的和不應(yīng)該做的,并且也完成了接口的定義,還有驗(yàn)收測(cè)序驅(qū)動(dòng)開發(fā)的場(chǎng)景準(zhǔn)備后,相信這時(shí),即使是新手也可以編寫出高質(zhì)量的接口代碼。
因?yàn)樗麜?huì)受到約束,他知道他需要做什么,主要他按照限定的開發(fā)流程和約定稍加努力即可。
如果真的這樣,相信我們也就慢慢能體會(huì)到 精益開發(fā) 的樂趣。
最后,讓我們一起來看下上述團(tuán)隊(duì)參賽接口開發(fā)的代碼實(shí)現(xiàn):
/**
* 團(tuán)隊(duì)參賽接口
*
* @return int code 0,參賽成功;1,隊(duì)名已存在
* @return int team_id 新建的團(tuán)隊(duì)ID
*/
public function joinIn() {
$rs = array('code' => 0, 'team_id' => 0);
DI()->userLite->check(true);
$domain = new Domain_Team();
if ($domain->isExists($this->teamName)) {
$rs['code'] = 1;
return $rs;
}
$teamId = $domain->joinIn($this->teamName);
$rs['team_id'] = $teamId;
return $rs;
}
可以看出,上面的代碼短小達(dá)意,簡(jiǎn)單清晰。
更多建議: