欢迎光临
我们一直在努力

如何使用 Service 模式?

若将商业逻辑都写在 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

赞(1)
版权归原作者所有,如有侵权请告知。达维营-前端网 » 如何使用 Service 模式?

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址