若将商业逻辑都写在 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 的肥大难以维护。1
1Mail::queue()只有一行可能无感,但很多外部服务需要一连串 API,甚至还要有 try/catch 处理。
违反 SOLID 的单一职责原则 : 外部行为不应该写在 controller。
controller 直接相依于外部行为,使得我们无法对 controller 做单元测试。
比较好的方式是使用 service :
将外部行为注入到 service。
在 service 使用外部行为。
将 service 注入到 controller。
EmailService.php2
2GitHub Commit : 新增 EmailService
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')); }); } } /** @var Mailer */ private $mail; /** * EmailService constructor. * @param Mailer $mail */ public function __construct(Mailer $mail) { $this->mail = $mail; }
将相依的Mailer注入到EmailService。
/** * 发送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.php3
3GitHub Commit : 新增 UserController
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()); } } /** @var EmailService */ protected $emailService; /** * UserController constructor. * @param EmailService $emailService */ public function __construct(EmailService $emailService) { $this->emailService = $emailService; }
将相依的 EmailService 注入到 UserController。
/** * 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; } } /** * 计算折扣 * @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写的判断逻辑写在里面。
/** * 计算最后价钱 * @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); } }
/** @var OrderService */
protected $orderService;
/**
* OrderController constructor.
* @param OrderService $orderService
*/
public function __construct(OrderService $orderService)
{
$this->orderService = $orderService;
}
将相依的 OrderService 注入到 UserController。
/** * 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