Laravel框架中如何使用Service模式
源:http://www.sangeng.org/blog/index/detail/id/519.html
若將商業邏輯都寫在controller,會造成controller肥大而難以維護,基於SOLID原則,我們應該使用Service模式輔助controller,將相關的商業邏輯封裝在不同的service,方便中大型專案的維護。
Version
Laravel 5.1.22
商業邏輯
商業邏輯中,常見的如:
- 牽涉到外部行為:如傳送Email,使用外部API…。
- 使用PHP寫的邏輯:如根據購買的件數,有不同的折扣。
若將商業邏輯寫在controller,會造成controller肥大,日後難以維護。
Service
牽涉到外部行為
如傳送Email,初學者常會在controller直接呼叫Mail::queue():
public function store(Request $request) { Mail::queue('email.index', $request->all(), function (Message $message) { $message->sender(env('MAIL_USERNAME')); $message->subject(env('MAIL_SUBJECT')); $message->to(env('MAIL_TO_ADDR')); }); }
在中大型專案,會有幾個問題:
- 將牽涉到外部行為的商業邏輯寫在controller,造成controller的肥大難以維護。
- 違反SOLID的單一職責原則:外部行為不應該寫在controller。
- controller直接相依於外部行為,使得我們無法對controller做單元測試。
比較好的方式是使用service:
- 將外部行為注入到service。
- 在service使用外部行為。
- 將service注入到controller。
EmailService.php
app/Services/EmailService.php namespace App\Services; use Illuminate\Mail\Mailer; use Illuminate\Mail\Message; class EmailService { /** @var Mailer */ private $mail; /** * EmailService constructor. * @param Mailer $mail */ public function __construct(Mailer $mail) { $this->mail = $mail; } /** * 傳送Email * @param array $request */ public function send(array $request) { $this->mail->queue('email.index', $request, function (Message $message) { $message->sender(env('MAIL_USERNAME')); $message->subject(env('MAIL_SUBJECT')); $message->to(env('MAIL_TO_ADDR')); }); } }
第 8 行
/** @var Mailer */
private $mail;
/**
* EmailService constructor.
* @param Mailer $mail
*/
public function __construct(Mailer $mail)
{
$this->mail = $mail;
}
將相依的Mailer注入到EmailService。
20 行
/**
* 傳送Email
* @param array $request
*/
public function send(array $request)
{
$this->mail->queue('email.index', $request, function (Message $message) {
$message->sender(env('MAIL_USERNAME'));
$message->subject(env('MAIL_SUBJECT'));
$message->to(env('MAIL_TO_ADDR'));
});
}
將傳送Emai的商業邏輯寫在send()。
不是使用Mail facade,而是使用注入的$this->mail
UserController.php
app/Http/Controllers/UserController.php
namespace App\Http\Controllers;
use App\Http\Requests;
use Illuminate\Http\Request;
use MyBlog\Services\EmailService;
class UserController extends Controller
{
/** @var EmailService */
protected $emailService;
/**
* UserController constructor.
* @param EmailService $emailService
*/
public function __construct(EmailService $emailService)
{
$this->emailService = $emailService;
}
/**
* Store a newly created resource in storage.
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function store(Request $request)
{
$this->emailService->send($request->all());
}
}
第9行
/** @var EmailService */
protected $emailService;
/**
* UserController constructor.
* @param EmailService $emailService
*/
public function __construct(EmailService $emailService)
{
$this->emailService = $emailService;
}
將相依的EmailService注入到UserController。
22行
/**
* Store a newly created resource in storage.
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function store(Request $request)
{
$this->emailService->send($request->all());
}
從原本直接相依於Mail facade,改成相依於注入的EmailService
改用這種寫法,有幾個優點
- 將外部行為寫在service,解決controller肥大問題。
- 符合SOLID的單一職責原則:外部行為寫在service,沒寫在controller。
- 符合SOLID的依賴反轉原則:controller並非直接相依於service,而是將service依賴注入進controller。
使用PHP寫的邏輯
如根據購買的件數,有不同的折扣,初學者常會在controller直接寫if…else邏輯。public function store(Request $request) { $qty = $request->input('qty'); $price = 500; if ($qty == 1) { $discount = 1.0; } elseif ($qty == 2) { $discount = 0.9; } elseif ($qty == 3) { $discount = 0.8; } else { $discount = 0.7; } $total = $price * $qty * $discount; echo($total); }
在中大型專案,會有幾個問題:
- 將PHP寫的商業邏輯直接寫在controller,造成controller的肥大難以維護。
- 違反SOLID的單一職責原則:商業邏輯不應該寫在controller。
- 違反SOLID的單一職責原則:若未來想要改變折扣與加總的演算法,都需要改到此method,也就是說,此method同時包含了計算折扣與計算加總的職責,因此違反SOLID的單一職責原則。
- 直接寫在controller的邏輯無法被其他controller使用。
比較好的方式是使用service。
- 將相依物件注入到service。
- 在service寫PHP邏輯使用相依物件。
- 將service注入到controller。
OrderService.php
app/Services/OrderService.php
namespace App\Services;
class OrderService
{
/**
* 計算折扣
* @param int $qty
* @return float
*/
public function getDiscount($qty)
{
if ($qty == 1) {
return 1.0;
} elseif ($qty == 2) {
return 0.9;
} elseif ($qty == 3) {
return 0.8;
} else {
return 0.7;
}
}
/**
* 計算最後價錢
* @param integer $qty
* @param float $discount
* @return float
*/
public function getTotal($qty, $discount)
{
return 500 * $qty * $discount;
}
}
第 5 行
/**
* 計算折扣
* @param int $qty
* @return float
*/
public function getDiscount($qty)
{
if ($qty == 1) {
return 1.0;
} elseif ($qty == 2) {
return 0.9;
} elseif ($qty == 3) {
return 0.8;
} else {
return 0.7;
}
}
為了符合SOLID的單一職責原則,將計算折扣獨立成getDiscount(),將PHP寫的判斷邏輯寫在裡面。
23行
/**
* 計算最後價錢
* @param int $qty
* @param float $discount
* @return float
*/
public function getTotal($qty, $discount)
{
return 500 * $qty * $discount;
}
為了符合SOLID的單一職責原則,將計算加總獨立成getTotal(),將PHP寫的計算邏輯寫在裡面。
OrderController.php
app/Http/Controllers/OrderController.php
namespace App\Http\Controllers;
use App\Http\Requests;
use App\MyBlog\Services\OrderService;
use Illuminate\Http\Request;
class OrderController extends Controller
{
/** @var OrderService */
protected $orderService;
/**
* OrderController constructor.
* @param OrderService $orderService
*/
public function __construct(OrderService $orderService)
{
$this->orderService = $orderService;
}
/**
* Store a newly created resource in storage.
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function store(Request $request)
{
$qty = $request->input('qty');
$discount = $this->orderService->getDiscount($qty);
$total = $this->orderService->getTotal($qty, $discount);
echo($total);
}
}
第 9 行
/** @var OrderService */
protected $orderService;
/**
* OrderController constructor.
* @param OrderService $orderService
*/
public function __construct(OrderService $orderService)
{
$this->orderService = $orderService;
}
將相依的OrderService注入到UserController。
21行
/**
* Store a newly created resource in storage.
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function store(Request $request)
{
$qty = $request->input('qty');
$discount = $this->orderService->getDiscount($qty);
$total = $this->orderService->getTotal($qty, $discount);
echo($total);
}
將原本的if…else邏輯改成呼叫OrderService,controller變得非常乾淨,也達成原本controller接收HTTP request,呼叫其他class的責任。
改用這種寫法,有幾個優點:
- 將PHP寫的商業邏輯寫在service,解決controller肥大問題。
- 符合SOLID的單一職責原則:商業邏輯寫在service,沒寫在controller。
- 符合SOLID的單一職責原則:計算折扣與計算加總分開在不同method,且歸屬於OrderService,而非OrderController。
- 符合SOLID的依賴反轉原則:controller並非直接相依於service,而是將service依賴注入進controller。
- 其他controller也可以重複使用此段商業邏輯。
Controller
牽涉到外部行為
public function store(Request $request)
{
$this->emailService->send($request->all());
}
使用PHP寫的邏輯
public function store(Request $request)
{
$qty = $request->input('qty');
$discount = $this->orderService->getDiscount($qty);
$total = $this->orderService->getTotal($qty, $discount);
echo($total);
}
若使用了service輔助controller,再搭配依賴注入與service container,則controller就非常乾淨,能專心處理接收HTTP request,呼叫其他class的職責了。
Conclusion
實務上會有很多service,須自行依照SOLID原則去判斷是否該建立service。
Service使得商業邏輯從controller中解放,不僅更容易維護、更容易擴充套件、更容易重複使用,且更容易測試。
Sample Code
完整的示例可以在我的GitHub上找到
External API
If…else