若将商业逻辑都写在 controller,会造成 controller 肥大而难以维护,基于SOLID原则,我们应该使用 Service 模式辅助 controller,将相关的商业逻辑封装在不同的 service,方便中大型项目的维护。
商业逻辑
商业逻辑中,常见的如 :
牵涉到外部行为 : 如发送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'));
});
}
Mail::queue()只有一行可能无感,但很多外部服务需要一连串 API,甚至还要有 try/catch 处理。
在中大型项目,会有几个问题 :
将牵涉到外部行为的商业逻辑写在 controller,造成 controller 的肥大难以维护。
违反 SOLID 的单一职责原则 : 外部行为不应该写在 controller。
controller 直接相依于外部行为,使得我们无法对 controller 做单元测试。
比较好的方式是使用 service :
将 service 注入到 controller。
EmailService.php
namespace App\Services;
use Illuminate\Mail\Mailer;
use Illuminate\Mail\Message;
class EmailService
{
/** @var Mailer */
private $mail;
/**
* EmailService constructor.
* 将依赖的 Mailer 注入到 EmailService。
* @param Mailer $mail
*/
public function __construct(Mailer $mail)
{
$this->mail = $mail;
}
/**
* 发送Email
* 将发送 Email 的商业逻辑写在 send()。
* 不是使用 Mail facade,而是使用注入的 $this->mail。
* @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'));
});
}
}
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)
{
//将依赖的 EmailService 注入到 UserController。
$this->emailService = $emailService;
}
/**
* Store a newly created resource in storage.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function store(Request $request)
{
//从原本直接依赖于 Mail facade,改成相依于注入的 EmailService。
$this->emailService->send($request->all());
}
}
将外部行为写在 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 注入到 controller。
OrderService.php
namespace App\Services;
class OrderService
{
/**
* 計算折扣
* @param int $qty
* @return float
*/
//为了符合 SOLID 的单一职责原则,将计算折扣独立成 getDiscount(),将PHP写的判断逻辑写在里面。
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
*/
//为了符合 SOLID 的单一职责原则,将计算加总独立成 getTotal(),将PHP写的计算逻辑写在里面。
public function getTotal($qty, $discount)
{
return 500 * $qty * $discount;
}
}
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)
{
//将依赖的 OrderService 注入到 UserController。
$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');
//将原本的 if...else 逻辑改成调用 OrderService,
//controller 变得非常干净,也达到了controller 接收 HTTP request,调用其他 class 的责任。
$discount = $this->orderService->getDiscount($qty);
$total = $this->orderService->getTotal($qty, $discount);
echo($total);
}
}
将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 中解放,不仅更容易维护、更容易扩展、更容易重复使用,且更容易测试。