2016年9月2日 星期五

如何使用 Service 模式?

若將商業邏輯都寫在 controller,會造成 controller 肥大而難以維護,基於SOLID原則,我們應該使用 Service 模式輔助 controller,將相關的商業邏輯封裝在不同的 service,方便中大型專案的維護。

Version


Laravel 5.1.22

商業邏輯


商業邏輯中,常見的如 : 
  1. 牽涉到外部行為 : 如發送Email使用外部API…。
  2. 使用PHP寫的邏輯 : 如根據購買的件數,有不同的折扣
若將商業邏輯寫在 controller,會造成 controller 肥大,日後難以維護。

Service


牽涉到外部行為

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

在中大型專案,會有幾個問題 : 
  1. 將牽涉到外部行為的商業邏輯寫在 controller,造成 controller 的肥大難以維護。11Mail::queue()只有一行可能無感,但很多外部服務需要一連串 API,甚至還要有 try/catch 處理。
  2. 違反 SOLID 的單一職責原則 : 外部行為不應該寫在 controller。
  3. controller 直接相依於外部行為,使得我們無法對 controller 做單元測試。
比較好的方式是使用 service : 
  1. 將外部行為注入到 service。
  2. 在 service 使用外部行為。
  3. 將 service 注入到 controller。
EmailService.php2 2GitHub Commit : 新增 EmailService

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

第 8 行

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

將相依的Mailer注入到EmailService
20 行

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

將發送 Emai的商業邏輯寫在send()
不是使用Mail facade,而是使用注入的$this->mail
UserController.php33GitHub Commit : 新增 UserController

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

第9行

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

將相依的 EmailService 注入到 UserController
22行

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

從原本直接相依於 Mail facade,改成相依於注入的 EmailService
 改用這種寫法,有幾個優點 :
  1. 將外部行為寫在 service,解決 controller 肥大問題。
  2. 符合 SOLID 的單一職責原則 : 外部行為寫在 service,沒寫在 controller。
  3. 符合 SOLID 的依賴反轉原則 : controller 並非直接相依於 service,而是將 service 依賴注入進 controller。

使用 PHP 寫的邏輯

根據購買的件數,有不同的折扣,初學者常會在 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);
}

在中大型專案,會有幾個問題 : 
  1. 將 PHP 寫的商業邏輯直接寫在 controller,造成 controller 的肥大難以維護。
  2. 違反 SOLID 單一職責原則 : 商業邏輯不應該寫在 controller。
  3. 違反 SOLID 單一職責原則 : 若未來想要改變折扣與加總的算法,都需要改到此 method,也就是說,此 method 同時包含了計算折扣與計算加總的職責,因此違反 SOLID 的單一職責原則。
  4. 直接寫在 controller 的邏輯無法被其他 controller 使用。
比較好的方式是使用 service。
  1. 將相依物件注入到 service。
  2. 在 service 寫 PHP邏輯使用相依物件。
  3. 將 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;
    }
}

第 5 行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
 * 計算折扣
 * @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寫的判斷邏輯寫在裡面。
23 行

1
2
3
4
5
6
7
8
9
10
/**
 * 計算最後價錢
 * @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
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);
    }
}

第 9 行

1
2
3
4
5
6
7
8
9
10
11
 /** @var OrderService */
protected $orderService;

/**
 * OrderController constructor.
 * @param OrderService $orderService
 */
public function __construct(OrderService $orderService)
{
    $this->orderService = $orderService;
}

將相依的 OrderService 注入到 UserController
21 行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
 * 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 的責任。
 改用這種寫法,有幾個優點 :
  1. 將PHP寫的商業邏輯寫在 service,解決 controller 肥大問題。
  2. 符合 SOLID 的單一職責原則 : 商業邏輯寫在 service,沒寫在 controller。
  3. 符合 SOLID 的單一職責原則 : 計算折扣與計算加總分開在不同 method,且歸屬於 OrderService,而非 OrderController
  4. 符合 SOLID 的依賴反轉原則 : controller 並非直接相依於 service,而是將 service依賴注入進 controller。
  5. 其他 controller 也可以重複使用此段商業邏輯。

Controller


牽涉到外部行為

1
2
3
4
public function store(Request $request)
{
    $this->emailService->send($request->all());
}

使用 PHP 寫的邏輯

1
2
3
4
5
6
7
8
9
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上找到。

  1. External API
  2. If…else

reference : http://oomusou.io/laravel/laravel-service/

沒有留言:

wibiya widget