如何使用Service模式

没人能嘲笑你的梦想,他们都是在嘲笑你的实力
若将商业逻辑都写在 controller,会造成 controller 肥大而难以维护,基于SOLID原则,我们应该使用 Service 模式辅助 controller,将相关的商业逻辑封装在不同的 service,方便中大型程序的维护。

问题的提出

商业逻辑中,常见的如 :

  • 牵涉到外部行为 : 如发送Email,使用外部API。
  • 使用PHP写的逻辑 : 如根据购买的件数,有不同的折扣。

若将商业逻辑写在 controller,会造成 controller 肥大,日后难以维护。

如发送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 的肥大难以维护。
  • 违反 SOLID 的单一职责原则 : 外部行为不应该写在 controller。
  • controller 直接相依于外部行为,使得我们无法对 controller 做单元测试。

比较好的方式是使用 service :

  • 将外部行为注入到 service。
  • 在 service 使用外部行为。
  • 将 service 注入到 controller。

问题的解决

EmailService.php

app/Services/EmailService.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
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'));
});
}
}

将相依的Mailer注入到EmailService。

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

将发送 Emai的商业逻辑写在send()。不是使用Mail facade,而是使用注入的$this->mail。

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

UserController.php

app/Http/Controllers/UserController.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
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

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

从原本直接相依于 Mail facade,改成相依于注入的 EmailService

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

实际的问题

问题提出

如根据购买的件数,有不同的折扣,初学者常会在 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

app/Services/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;
}
}

OrderController.php

app/Http/Controllers/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);
}
}

详细链接

  1. 如何使用Repository模式
  2. 如何使用Presenter模式
  3. 如何使用Service模式
  4. laravel的中大型架构

http://oomusou.io/laravel/laravel-service/
http://blog.iwanli.me/article/B8ApLAab.html