一個需求
做的某項目有一個“轉(zhuǎn)賬”的功能谈截,但是轉(zhuǎn)賬的類型有很多種,對應(yīng)每種轉(zhuǎn)賬需要的參數(shù)也不同涧偷,舉個例子一種轉(zhuǎn)賬是由系統(tǒng)轉(zhuǎn)賬給用戶簸喂,那么就只有接收方和金額兩個參數(shù),另一種轉(zhuǎn)賬是用戶之間的轉(zhuǎn)賬且支持留言燎潮,那么就有發(fā)送方接收方金額和留言四個參數(shù)娘赴。當(dāng)然最簡單的思路就是采用四個參數(shù),對于第一種轉(zhuǎn)賬將不用的兩個參數(shù)留空跟啤,這種方法的問題在于诽表,考慮到未來可能增加的新的轉(zhuǎn)賬類型唉锌,可能會引入新的參數(shù),那么代碼很可能需要推倒重來竿奏,有沒有更優(yōu)雅的解決方式呢袄简?
一個例子
其實Laravel里就有實現(xiàn)類似需求的例子,那就是查詢構(gòu)造器(Query Builder)泛啸,它的一個使用的例子如下:
$users = DB::table('users')
->select(DB::raw('count(*) as user_count, status'))
->where('status', '<>', 1)
->groupBy('status')
->get();
這個方法和我們的需求就很像了绿语,對于查詢這一功能,傳入哪些參數(shù)是未知的候址,例如某次具體的查詢吕粹,可能需要調(diào)用groupBy也可能調(diào)用orderBy,也可能兩者需要同時調(diào)用或者都不調(diào)用岗仑。一個思路就是針對每一個參數(shù)都寫一個方法匹耕,需要時則調(diào)用,不需要時則不調(diào)用荠雕。
解決方案
整體的解決思路是寫兩個類稳其,一個叫Transfer,一個叫Builder炸卑,每個參數(shù)對應(yīng)的方法寫在Builder里既鞠,由Transfer去調(diào)用Builder構(gòu)造我們需要的轉(zhuǎn)賬類型,完成相關(guān)操作盖文。這樣當(dāng)需求更新時(要增加新的參數(shù)時)嘱蛋,只要在Builder里添加相應(yīng)的方法即可,而不用改動現(xiàn)有代碼五续。下面先貼一下對應(yīng)的代碼再做詳細(xì)解釋浑槽。
Transfer類代碼:
class Transfer
{
public function __call($method, $parameters)
{
$builder = new Builder();
return call_user_func_array([$builder, $method], $parameters);
}
public static function __callStatic($method, $parameters)
{
$instance = new static;
return call_user_func_array([$instance, $method], $parameters);
}
}
Builder類實際上只涉及到具體的功能實現(xiàn),就貼部分代碼意思意思看看就行:
class Builder
{
protected $from = 0; // 0 represents system
protected $to = 0;
protected $amount = 0;
protected $comments = '';
protected $related = [];
public function from($user)
{
if ($user instanceof User) {
$this->from = $user->getAuthIdentifier();
} elseif (is_int($user)) {
$this->from = $user;
} else {
throw new InvalidArgumentException(sprintf('%s excepts $user parameter to be \App\User or integer, %s given.', __METHOD__, gettype($user)));
}
return $this;
}
public function to($user){...}
public function amount(int $amount){...}
public function comments($comments){...}
public function related(int $type, int $id, $extra = null){...}
public function transfer(){...}
}
具體調(diào)用Transfer功能的代碼:
Transfer::from($sender)->to($receiver)->amount($amount)->comments($comments)->related($related_type, $related_id, $related_extra)->transfer();
需要的儲備知識:
- PHP魔術(shù)方法__call()和__callStatic()
- PHP面向?qū)ο蠡A(chǔ)知識
下面我們來走一遍調(diào)用Transfer的流程來看看返帕。首先調(diào)用了Transfer
類中的靜態(tài)方法from
桐玻,然而Transfer
中并不存在這個靜態(tài)方法,則會自動調(diào)用__callStatic()
這個魔術(shù)方法荆萤。這個方法首先實例化了一個static
對象镊靴。注意這里的static是一個類名,new出來的$instance
是屬于static
這個類的一個實例化對象链韭,有點拗口(:з」∠)然后返回時調(diào)用了call_user_func_array
這個方法偏竟,這個方法具體可以參考php的手冊,實際上它完成了類似$instance->method($parameters)
這樣的操作敞峭,放到我們當(dāng)前的情境下實際執(zhí)行了$transfer_instance->from($user)
這樣的操作踊谋。
然后發(fā)覺Transfer
中并不存在這個動態(tài)方法,于是又會自動調(diào)用__call()
這個魔術(shù)方法旋讹。這個方法首先創(chuàng)建了一個Builder
類的實例殖蚕,之后調(diào)用call_user_func_array
這個方法轿衔,實際上相當(dāng)于執(zhí)行了$builder->from($user)
方法,然后終于得Builder
類里找到了這個from()
方法睦疫,注意它的返回值是$this
害驹。(補充閱讀:self vs. this in PHP)
然后當(dāng)前這個Builder
這個對象繼續(xù)調(diào)用to
方法,發(fā)覺又不存在又去調(diào)用__call()
這個魔術(shù)方法蛤育,之后的過程同上宛官,反復(fù)調(diào)用Builder
中的方法把所有需要的參數(shù)都處理過以后最后調(diào)用了transfer()
方法最終完成轉(zhuǎn)賬操作。