Q3: IoC Container內的$aliases跟$bindings分別是什麼?兩者如何互動?
要解決這個問題,必須去讀container最核心的function make部份程式碼。
/**
* Resolve the given type from the container.
*
* @param string $abstract
* @param array $parameters
* @return mixed
*/
// 首先注意到的是,之前我們稱呼’app’、’auth’這些字串為 IoC Container 內的 「key」
// 其實正確名稱是「abstract」
public function make($abstract, array $parameters = [])
{
// 這個getAlias的功能如下
// >>> $app->getAlias('app');
// => "app"
// >>> $app->getAlias('Illuminate\Foundation\Application');
// => "app"
$abstract = $this->getAlias($abstract);
// If an instance of the type is currently being managed as a singleton we'll
// just return an existing instance instead of instantiating new instances
// so the developer can keep using the same objects instance every time.
// 如果是用singleton或是instance方法,應該是會把物件存在instances陣列
//這地方會檢查abstract是否真的跟物件綁定,是的話就回傳,收工
if (isset($this->instances[$abstract])) {
return $this->instances[$abstract];
}
// getConcrete內會去查 $this->bindings[$abstract]['concrete']
// 尋找這abstract對應到的是哪個類別、或是Closure
$concrete = $this->getConcrete($abstract);
// 後面一大段省略
}
// Order.php
class Order extends Eloquent
{
public function addProdut($product)
{
$this->subtotal += $product->price
if ($this->subtotal > 1000) {
$this->shipping_fee = 0;
} else {
$this->shipping_fee = 100;
}
}
}
然後可以這樣寫測試:
//OrderTest.php
class OrderTest extends TestCase
{
public function test_addProdut_with_shipping_fee()
{
$product = new Product();
$product->price = 800;
$order = new Order();
$order->addProduct($product);
$this->assertEquals(100, $order->shipping_fee);
}
public function test_addProdut_without_shipping_fee()
{
$product = new Product();
$product->price = 1500;
$order = new Order();
$order->addProduct($product);
$this->assertEquals(0, $order->shipping_fee);
}
}
class AddProductToOrder
{
public function handle($product, $order)
{
//blah
}
}
測試的時候,改寫成這樣即可:
class AddProductToOrderTest extends TestCase
{
public function test_handle_with_shipping_fee()
{
$product = new Product();
$product->price = 800;
$order = new Order();
$service = new AddProductToOrder();
$service->handle($product, $order);
$this->assertEquals(100, $order->shipping_fee);
}
public function test_handle_product_amount()
{
// blah
}
public function test_handle_product_trending_score()
{
// blah
}
}
跟第一招一樣,可以在程式變大之後、有必要的時候才獨立成service類別並補寫測試。
轉移的成本是很低的,幾乎只是把code從這邊剪下貼上到那邊而已。
第三招、轉移到專門的POPO
POPO(Plain Old PHP Object)是指單純的全手寫類別,不去繼承Eloquent之類的華麗類別。
雖然是最基本的OOP用法,卻常常被人們所忽略。
現在假設訂單進一步變複雜,運費的邏輯會根據消費者的所在地區而有不同:
class AddProductToOrder
{
public function handle($product, $order, $country)
{
// blah
if ($country == 'taiwan') {
if ($order->subtotal > 1000) {
// set shipping fee
} else {
// set shipping fee
}
} else if ($country == 'korea') {
// set shipping fee
}
//blah blah
}
}
再加上金額的變化,光是運費本身的計算就很複雜、需要寫很多測試才安心。
與其在entity或service內計算,不如弄一台運費計算機出來,會更分工明確:
class ShippingFeeCalculator
{
public function calculate($productPrice, $customerCountry)
{
// blah
}
}
接著就能替計算機寫許多測試,測到安心為止:
class ShippingFeeCalculatorTest extends TestCase
{
public function test_calculate_taiwan_100_dollar_order()
{
$calculator = new ShippingFeeCalculator();
$result = $calculator->calculate(100, 'taiwan');
$this->assertEquals($someNumber, $result);
}
public function test_calculate_taiwan_600_dollar_order()
{
// blah
}
public function test_calculate_korea_100_dollar_order()
{
// blah
}
}
本來的service就可以改寫成,吃計算機當作參數:
class AddProductToOrder
{
public function handle($product, $order, $country, $calculator)
{
$shippingFee = $calculator->calculate($product->price, $country);
// blah
}
}
// The entity now can't do anything.
// So anemic, so poor.
/*
$order->iCanDoThis();
$order->iCanDoThat();
$order->iCanDoEverything();
*/
// They are all in the services instead.
$service = new DoThisService();
$service->handle($order);