若將商業邏輯都寫在 controller,會造成 controller 肥大而難以維護,基於SOLID原則,我們應該使用 Service 模式輔助 controller,將相關的商業邏輯封裝在不同的 service,方便中大型專案的維護。
Version
Laravel 5.1.22
商業邏輯
商業邏輯中,常見的如 :
- 牽涉到外部行為 : 如
發送Email
,使用外部API
…。 - 使用PHP寫的邏輯 : 如
根據購買的件數,有不同的折扣
。
若將商業邏輯寫在 controller,會造成 controller 肥大,日後難以維護。
Service
牽涉到外部行為
如
發送Email
,初學者常會在 controller 直接調用 Mail::queue()
:1 2 3 4 5 6 7 8 | 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 的肥大難以維護。11
Mail::queue()
只有一行可能無感,但很多外部服務需要一連串 API,甚至還要有try/catch
處理。 - 違反 SOLID 的單一職責原則 : 外部行為不應該寫在 controller。
- controller 直接相依於外部行為,使得我們無法對 controller 做單元測試。
比較好的方式是使用 service :
- 將外部行為注入到 service。
- 在 service 使用外部行為。
- 將 service 注入到 controller。
EmailService.php2 2GitHub Commit : 新增 EmailService
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | 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 行
1 2 3 4 5 6 7 8 9 10 11 | /** @var Mailer */ private $mail; /** * EmailService constructor. * @param Mailer $mail */ public function __construct(Mailer $mail) { $this->mail = $mail; } |
將相依的
Mailer
注入到EmailService
。
20 行
1 2 3 4 5 6 7 8 9 10 11 12 13 | /** * 發送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.php33GitHub Commit : 新增 UserController
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | 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行
1 2 3 4 5 6 7 8 9 10 11 | /** @var EmailService */ protected $emailService; /** * UserController constructor. * @param EmailService $emailService */ public function __construct(EmailService $emailService) { $this->emailService = $emailService; } |
將相依的
EmailService
注入到 UserController
。
22行
1 2 3 4 5 6 7 8 9 10 | /** * 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
邏輯。1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | 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 行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | /** * 計算折扣 * @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 行
1 2 3 4 5 6 7 8 9 10 | /** * 計算最後價錢 * @param int $qty * @param float $discount * @return float */ public function getTotal($qty, $discount) { return 500 * $qty * $discount; } |
為了符合 SOLID 的單一職責原則,將計算加總獨立成
getTotal()
,將PHP寫的計算邏輯寫在裡面。
OrderController.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | 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 行
1 2 3 4 5 6 7 8 9 10 11 | /** @var OrderService */ protected $orderService; /** * OrderController constructor. * @param OrderService $orderService */ public function __construct(OrderService $orderService) { $this->orderService = $orderService; } |
將相依的
OrderService
注入到 UserController
。
21 行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | /** * 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
牽涉到外部行為
1 2 3 4 | public function store(Request $request) { $this->emailService->send($request->all()); } |
使用 PHP 寫的邏輯
1 2 3 4 5 6 7 8 9 | 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上找到。
reference : http://oomusou.io/laravel/laravel-service/
沒有留言:
張貼留言