如何使用 Service 模式

houmuxu發表於2019-12-31

如何使用 Service 模式

若將資料庫邏輯都寫在 Controller 裡,會造成 Controller 程式碼的臃腫難以維護,基於 SOLID 原則,我們應該使用 Service 模式輔助 Controller,將相關的業務邏輯封裝在不同的 Service,方便專案的後期維護。

Laravel 框架版本

Laravel 5.4.17

業務邏輯

業務邏輯中,常見的如:

  • 牽涉到外部行為: 如 傳送 Email 郵件使用外部API ..

  • 使用 PHP 寫的邏輯: 如 根據購買的數量,給予不同的折扣

Service

牽涉到外部的行為

如 傳送Email,常常會在 Controller 中直接呼叫 Mail::queue()

 /**
 * @param \Illuminate\Http\Request $request
 */
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 注入到 Controlelr

app\Services\EmailService.php

<?php

namespace App\Services;
use Illuminate\Mail\Message;
use Mail;

/**
 * Class EmailService
 *
 * @package \App\Services
 */
/**
 * Class EmailService
 *
 * @package App\Services
 */
class EmailService
{
    /**
     * @var \Mail
     */
    protected $mailer;

    /**
     * 將相依的 Mailer 注入到 EmailService
     * EmailService constructor.
     *
     * @param $mailer
     */
    public function __construct(Mail $mailer)
    {
        $this->mailer = $mailer;
    }

    /**
     * 傳送 Email的邏輯寫在 send() 不是使用 Mail Facade,而是使用 $this->mailer
     * @param array $request
     */
    public function send(array $request)
    {
        $this->mailer->queue('email.index',$request,function(Message $message){
            $message->sender(env('MAIL_USERNAME'));
            $message->subject(env('MAIL_SUBJECT'));
            $message->to(env('MAIL_TO_ADDR'));
        });
    }
}

app\Controllers\UserController.php

<?php

namespace App\Http\Controllers;

use App\Services\EmailService;
use Illuminate\Http\Request;

/**
 * Class UserController
 *
 * @package App\Http\Controllers
 */
class UserController extends Controller
{
    /**
     * @var \App\Services\EmailService
     */
    protected $emailService;

    /**
     * @param \Illuminate\Http\Request $request
     */
    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 { ... } 邏輯。如下app\Controllers\UserController.php

public function index(Request $request)
{
    $number = $request->input('number');
    $price = 500;
    $discount = 1;
    if ($number == 1) {
        $discount = 1;
    } elseif ($number == 2) {
        $discount = 0.9;
    } elseif ($number == 3) {
        $discount = 0.8;
    } else {
        $discount = 0.7;
    }
    $total = $price * $number * $discount;

    return $total;
}

在中大型專案中,會有幾個問題:

  • 將 PHP 寫的業務邏輯直接寫在 Controller ,造成 Controller 的程式碼臃腫難以維護

  • 違反了 SOLID 的單一職責原則:業務邏輯不應該寫在 Controller

  • 違反了 SOLID 的單一職責原則:若未來想要改變折扣的寫演算法,都需要用到此 Method,也也就是說這個 Method 同時包含了計算折扣於計算加總的職責,因此違反了 SOLID 的單一職責原則

  • 直接寫在 Controller 的邏輯無法被其他 Controller 使用

app\Services\OrderService.php

<?php

namespace App\Services;

/**
 * Class OrderService
 *
 * @package App\Services
 */
/**
 * Class OrderService
 *
 * @package App\Services
 */
class OrderService
{
    /**
     * 計算折扣
     *
     * @param $number
     *
     * @return float
     */
    public function getDisCount($number)
    {
        switch ($number) {
            case 1:
                return 1.0;
                break;
            case 2:
                return 0.9;
                break;
            case 3:
                return 0.8;
                break;
            default:
                return 0.7;
        }
    }

    /**
     * 計算最後價格
     *
     * @param $number
     * @param $discount
     *
     * @return int
     */
    public function getTotal($number, $discount)
    {
        return 500 * $number * $discount;
    }
}

在 Controller 中呼叫程式碼,如下:

<?php

namespace App\Http\Controllers;

use App\Services\OrderService;
use Illuminate\Http\Request;

/**
 * Class UserController
 *
 * @package App\Http\Controllers
 */
class UserController extends Controller
{
    /**
     * @var \App\Services\EmailService
     */
    protected $orderService;

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

    /**
     * @param \Illuminate\Http\Request $request
     *
     * @return int
     */
    public function index(Request $request)
    {
        $number = $request->input('number');
        $discount = $this->orderService->getDisCount($number);
        return $this->orderService->getTotal($number, $discount);
    }
}

將原本的 if () { .. } else { .. } 邏輯改寫成使用 OrderService,Controller 變得非常感覺,也達成原來 Controller 接受 Http Request,呼叫其他 Class 的責任。

改用這種寫法的幾個優點:

  • 將 PHP 寫的業務邏輯寫在 Service ,解決了 Controller 程式碼臃腫的問題

  • 符合 SOLID 的單一職責原則: 業務邏輯寫在 Service,沒寫在 Controller

  • 符合 SOLID 的單一職責原則:計算折扣與計算加總分開在不同的 Method,且歸屬於 OrderService,而非 Controller

  • 符合 SOLID 的依賴反轉原則: Controller 並非直接相依於 Service,而是將 Service 依賴注入進 Controller

  • 其他 Controller 也可以重複使用這段業務邏輯

結束

  • 實際上會有很多 Service ,需要自行依照 SOLID 原則去判斷是否該建立 Service

  • Service 使得業務邏輯從 Controller 中解放,不僅更容易維護、更容易擴充、更容易重複使用且更容易測試

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章