Repository Pattern and Service Layer

PHP:7.2

Laravel:5.7

在大型的專案之中,如果程式的耦合度太高(比如商業邏輯都塞在 Controller,或者把 Model 當 Library 用),很容易遇到兩個問題:

  • 彈性極低,難以維護
  • 不易寫單元測試

光這兩點所衍伸出來的苦頭,就夠所有人吃的了。因此,我們需要透過一些架構將這些商業邏輯一步一步拆分出來,讓整個專案好測試、好調整。

LaravelService Container 提供了 DI(Dependency injection)功能,方便我們在各個元件之間自動注入相關的 Class

Repository Pattern

Notice: Model 原則上只留 relation,盡量不要有取資料的邏輯。

Repository Pattern 做法很簡單,概念上就是將 Model 取資料的邏輯拆到 Repository 中,讓 Model 變成真正的 Pure-Model,而實作上搭配 DI(Dependency injection)就輕鬆許多。

Model 範例:app/Models/Album.php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Album extends Model
{
    protected $primaryKey = 'album_id';
    protected $table = 'album';

    public function songs()
    {
        return $this->hasMany('App\Models\Song', 'album_id');
    }
}

Repository 範例:app/Repositories/AlbumRepository.php

namespace App\Repositories;

use App\Models\Album;

class AlbumRepository
{
    protected $album;

    // 透過 DI 注入 Model
    public function __construct(Album $album)
    {
        $this->album = $album;
    }

    // 取資料邏輯:取得公司為 Sony 的專輯
    public function getSonyAlbum($limit = 1)
    {
        return $this->album
            ->where('album_company', 'Sony')
            ->limit($limit)
            ->get();
    }
}

這樣一來,Controller 就不會直接跟 Model 依賴了。
Controller 範例

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Repositories\AlbumRepository;

class TestController extends Controller
{
    protected $albumRepo;

    透過 DI 注入 Repository
    public function __construct(AlbumRepository $albumRepo)
    {
        $this->albumRepo = $albumRepo;
    }

    public function test(Request $request)
    {
        // 直接呼叫 Repository 包裝好的 method
        return $this->albumRepo->getSonyAlbum(10);
    }
}

Service Layer

雖然已經將 Model 取資料的邏輯拆出來了,可是在真實的應用上,一個 Controller 通常都會一次操作好幾張表,雖然 Model 乾淨了, Controller 卻還是塞滿了 Repository。因此,我們必須再將真正的商業邏輯封裝成 ServiceController 使用。

實作也是透過 DI(Dependency injection)的方式進行。

Service 範例:app/Services/RecommendService.php

namespace App\Services;

use App\Repositories\AlbumRepository;
use App\Repositories\SongRepository;

class RecommendService
{
    protected $albumRepo;
    protected $songRepo;

    // 透過 DI 注入 Repository
    public function __construct(AlbumRepository $albumRepo, SongRepository $songRepo)
    {
        $this->albumRepo = $albumRepo;
        $this->songRepo = $songRepo;
    }

    // 推薦專輯及歌曲
    public function get()
    {
        return [
            'album' => $this->albumRepo->getSonyAlbum(),
            'song' => $this->songRepo->getValidSong()
        ];
    }
}

Controller 範例

namespace App\Http\Controllers;

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

class TestController extends Controller
{
    protected $recommendService;

    透過 DI 注入 Service
    public function __construct(RecommendService $recommendService)
    {
        $this->recommendService = $recommendService;
    }

    public function test(Request $request)
    {
        // 直接呼叫 Service 包裝好的 method
        return $this->recommendService->get();
    }
}

這樣一來,就可以解決不易維護及測試的問題啦。

Categories: Laravel