使用 Service 模式来封装业务代码

老牛浏览 340评论 0发表于

1. 说明

如果将商业逻辑都写在 Controller 中,随着业务复杂度的提高,会造成 Controller 臃肿而难以维护的困境。基于 SOLID 原则,我们应该使用 Service 模式,将相关的商业逻辑封装在不同的 Service 中,以方便中大型项目的维护。

2. 商业逻辑

商业逻辑中,常见的有:

  1. 涉及外部行为。如:发送邮件,调用第三方 API 等。

  2. 使用 PHP 写的逻辑。如:根据购买的件数,获得不同的折扣。

2.1 涉及外部行为

如发送邮件,初学者常会在 Controller 中直接调用 Mail::queue()

php
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'));
    });
}

这个,在中大型项目中,会出现一些问题:

  1. Controller 臃肿而难以维护。

  2. 违反 SOLID 单一职责原则:外部行为不应该写在 Controller。

  3. Controller 直接依赖于外部行为,无法对其做单元测试。

比较好的方式是使用 Service:

  1. 将外部行为注入到 Service。

  2. 在 Service 中使用外部行为。

  3. 将 Service 注入到 Controller。

代码修改后如下:

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;
    }

    /**
     * 发送邮件
     * @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'));
        });
    }
}

将依赖的 Mailer 注入到 EmailService。将发送邮件的商业逻辑写在 send() 中。没有使用 Mail 的 Facade,而是使用注入的 $this->mail

然后进行调用:

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());
    }
}

将依赖的 EmailService 注入到 UserController。原本依赖 Mail 的 Facade,现在改为注入 EmailService。

改用这种写法,有如下优点:

  1. 将外部行为写在 Service,解决了 Controller 臃肿的问题。

  2. 符合 SOLID 的单一职责原则。

  3. 符合 SOLID 的依赖反转原则。Controller 并非直接依赖于 Service,而是将 Service 注入到 Controller。

2.2 使用 PHP 写的逻辑

如根据购买的件数,获得不同的折扣,初学者常常会在 Controller 中直接写条件判断逻辑。

php
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);
}

同样,比较好的方式是使用 Service 模式将商业逻辑抽取出来:

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;
    }
}

为了符合 SOLID 的单一职责原则,我们分别使用 getDiscount() 来获取折扣,使用 getTotal() 来获取总价格。

最后在 Controller 中调用:

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);
    }
}

将 OrderService 注入到 UserController。

3. 总结

  • 业务会包含很多 Service,需要自己根据 SOLID 原则去判断是否应该封装为单独的 Service。

  • Service 使得商业逻辑从 Controller 中分离出来,不仅更易维护、更易扩展、更易重复使用,而且也方便测试。

原文地址

点赞
收藏
暂无评论,快来发表评论吧~
私信
老牛@ilaoniu
老牛,俗称哞哞。单纯的九零后理工小青年。喜欢折腾,爱玩,爱音乐,爱游戏,爱电影,爱旅游...
最后活跃于