Laravel Design Patterns Series: Repository Pattern - Part 3

Respository Pattern

  1. Builder (Manager) Pattern - Part 1
  2. Factory Pattern - Part 2
  3. Repository Pattern - Part 3 (current)
  4. Strategy Pattern - Part 4
  5. Provider Pattern - Part 5
  6. Facade Pattern - Part 6

Mở đầu

Ngày nay có rất nhiều Design Pattern được giới thiệu, một trong số những câu hỏi chúng ta thường gặp đó là: "Làm thế nào để tôi có thể sử dụng một Pattern với một vài kỹ thuật nào đó?". Với Laravel hoặc một số framework khác, khi chúng ta nhận được một yêu cầu tìm hiểu về Repository Pattern chẳng hạn, câu hỏi thường đặt ra hoặc từ khóa chúng ta thường dùng để tìm kiếm đó là: "How i can use repository pattern in Laravel 4 or 5". Tuy nhiên một vấn đề quan trọng mỗi chúng ta cần lưu ý đó là Design Pattern không hề phụ thuộc vào bất kỳ một ngôn ngữ nào, framework nào hay kỹ thuật nào cả (yaoming).

Hôm nay tôi xin giới thiệu với các bạn về Repository Pattern, đồng thời xây dựng một demo nho nhỏ về việc tích hợp Repository Pattern trong Laravel 5.2. Trước hết, tôi muốn giới thiệu sơ qua về một số khái niệm mà ta cần làm quen trước khi triển khai.

Giới thiệu

Repository Pattern được giới thiệu lần đầu tiên bởi Eric Evans trong quyển Domain-Driven Design Book. Có thể hiểu đơn giản repository là nơi mà ứng dụng của chúng ta truy cập vào Domain Layer.

Định nghĩa Repository Pattern:

A Repository mediates between the domain and data mapping layers, acting like an in-memory domain object collection. Client objects construct query specifications declaratively and submit them to Repository for satisfaction. Objects can be added to and removed from the Repository, as they can from a simple collection of objects, and the mapping code encapsulated by the Repository will carry out the appropriate operations behind the scenes.

(Nguồn: Repository)

Repository Pattern làm nhiệm vụ chia nhỏ logic truy cập dữ liệu (data access logic) và nối chúng với các thực thể business trong khối business logic. Giao tiếp giữa data access logicbusiness logic sẽ được thực hiện thông qua các interface.

repository_pattern.png

Repository Pattern có thể hiểu là nơi chứa các logic truy cập dữ liệu. Nó cho phép chúng ta giấu kín việc truy cập dữ liệu như thế nào. Nói một cách khác, business logic có thể lấy dữ liệu cần thiết mà không cần biết dữ liệu được thiết kế ra sao hoặc tương lai cấu trúc dữ liệu thay đổi như thế nào.

Một số ưu điểm của việc thực hiện này có thể chỉ ra như sau:

  • Code dễ dàng maintain.
  • Business logic và Data access logic có thể test độc lập.
  • Tránh việc duplicate code.
  • Lỗi ít hơn.

Repository trong Laravel

Nói chung thì lý thuyết vẫn là lý thuyết, vẫn hàn lâm và có gì đó khó hiểu. Các cụ có câu: "Trăm nghe không bằng một thấy". Vì vậy phần còn lại của bài viết này tôi xin được đi sâu vào việc triển khai Repository Pattern để chúng ta có thể thấy được rõ hơn các ưu điểm mà nó mang lại.

Bạn có thể quyết định dùng Repository hoặc không. Trong các ứng dụng nhỏ cấu trúc dữ liệu không thay đổi nhiều chúng ta cũng không nhất thiết phải sử dụng Repository.

Bài toán ví dụ mà tôi muốn đưa ra trong bài viết này đó là một chức năng nhỏ về quản lý sản phẩm (Product). Trong đó tôi đi sâu vào 2 chức năng chính đó là danh sách các sản phẩm và thông tin chi tiết một sản phẩm nào đó. Tôi sẽ sử dụng Laravel phiên bản 5.2 và giả định rằng bạn đã có thể cài đặt mới một bản Laravel 5.2 trên local với cấu trúc thư mục sẵn có.

Chúng ta sẽ có bảng products chứa thông tin: id, name, description của sản phẩm.

Source code trong bài viết này vui lòng tham khảo thêm tại đây: https://github.com/nguyenthanhtung88/repository-design-pattern

Để phục vụ cho việc test output, chúng ta hãy cùng tạo một route resource cho products:

<?php
// app/Http/routes.php

Route::resource('products', 'ProductsController');

Tạo Model phục vụ cho việc tương tác với bảng products:

<?php
// app/Models/Product.php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Product extends Model
{
    protected $fillable = [
        'name', 'description'
    ];
}

Việc đơn giản cần làm tiếp theo đó là tạo ProductsController:

<?php
// app/Http/Controllers/ProductsController.php

namespace App\Http\Controllers;

use App\Models\Product;

class ProductsController extends Controller
{
    public function index()
    {
        $products = Product::all();

        return $products;
    }

    public function show($id)
    {
        $product = Product::find($id);

        return $product;
    }
}

Tôi không xây dựng view ở đây vì mục đích chính chỉ là tập trung vào việc xử lý logic lấy dữ liệu. Đến đây tạm thời chúng ta đã có một bức tranh chức năng nho nhỏ:

  • Truy cập /products để có list các sản phẩm.
  • Truy cập /products/xxx để lấy chi tiết sản phẩm xxx.

Okie. Hãy cùng tập trung chính vào ProductsController và phân tích. Trong Controller, Product model được dùng trực tiếp để lấy thông tin từ trong Database. Mọi chuyện vẫn diễn ra hết sức êm đẹp cho đến một ngày đẹp trời, khách hàng thay đổi yêu cầu cho việc hiển thị danh sách các sản phẩm cần sắp xếp mặc định theo ngày tạo (created_at) giảm dần. Một ý tưởng nảy ra ngay trong đầu bạn hết sức đơn giản: chỉ cần thay Product::all() thành Product::orderBy('created_at', 'desc')->get() là mọi việc được giải quyết.

Và rồi một ngày đẹp trời khác lại đến, khách hàng không muốn id của sản phẩm xuất hiện trên URL vì dễ lộ thông tin, họ yêu cầu bạn đưa name của sản phẩm thay vì dùng id. Giải pháp cũng sẽ tương tự như trên, chỉ đơn giản thay việc dùng Product::find($id) thành Product::where('name', $name)->first().

Nhưng mọi chuyện không hề đơn giản như vậy, với trường hợp ví dụ của tôi mọi chuyện đều được giải quyết rất nhanh chóng. Tuy nhiên nếu ứng dụng của bạn khá lớn, bạn đã sử dụng việc tìm kiếm một sản phẩm nào đó theo id (không phải name) không phải chỉ ở các action Controller này mà ở một số Controller khác nữa. Việc bạn phải làm là gì? Tìm kiếm trong cả Project xem những chỗ nào có sử dụng tìm kiếm sản phẩm theo id thay thế bằng tìm kiếm theo name? Việc copy/paste sẽ làm code của bạn bị lặp, đồng thời việc unit test lại sẽ gặp không ít khó khăn phải không?

Đây chính là lúc bạn cần đến Repository. Ý tưởng cơ bản thì Repository là cầu nối trung gian giữa Models và Controllers, đồng thời tách sự phụ thuộc của Controller vào Model.

Triển khai Repository cơ bản

Để giải quyết vấn đề nêu trên, chúng ta cần tạo thêm một thư mục Repositories trong thư mục app (Sở dĩ phải tạo thêm vì Laravel mặc định không có thư mục này), đồng thời triển khai ProductRepository như sau:

<?php
// app/Repositories/Eloquents/ProductRepository.php

namespace App\Repositories\Eloquents;

use App\Models\Product;

class ProductRepository
{
    public function all()
    {
        return Product::all();
    }

    public function find($id)
    {
        return Product::find($id);
    }
}

ProductRepository sẽ đảm nhận vai trò chính tương tác với Model, có thể các bạn sẽ thắc mắc tại sao lại phải đặt trong thư mục Eloquents, tôi xin phép được dành phần trả lời ở phần sau, trước mắt thì cứ tạm hiểu là tôi có thêm thư mục Eloquents trong thư mục Repositories.

Tiếp theo ta sẽ thay thế phần code trong ProductsController:

<?php
// app/Http/ProductsController.php

namespace App\Http\Controllers;

use App\Models\Product;
use App\Repositories\Eloquents\ProductRepository;

class ProductsController extends Controller
{
    protected $productRepository;

    public function __construct(ProductRepository $productRepository)
    {
        $this->productRepository = $productRepository;
    }

    public function index()
    {
        $products = $this->productRepository->all();

        return $products;
    }

    public function show($id)
    {
        $product = $this->productRepository->find($id);

        return $product;
    }

}

Các phần thay đổi dễ dàng nhận ra đó là:

  • Thêm khai báo sử dụng ProductRepository.
  • Thêm hàm constructor, dùng kỹ thuật Dependecy Injection (nếu bạn quan tâm có thể tìm hiểu thông qua bài viết của @thangtd90 về Service Container) để inject ProductRepository và khởi tạo giá trị cho thuộc tính protected productRepository.
  • Ở các phương thức index()show(), ta nhận được dữ liệu thông qua việc sử dụng productRepository.

Như vậy ta đã tập trung được việc lấy danh sách và chi tiết sản phẩm trong Repository, nếu logic có thay đổi chung (như tôi đề cập ở phần trên), ta cũng chỉ việc thay đổi trong Repository là tất cả các nơi sử dụng đều được áp dụng mà không cần phải lo nghĩ nữa. Đồng thời cũng dễ dàng hơn cho chúng ta trong việc test, ta chỉ việc viết unit test riêng cho Repository.

Triển khai Repository với Interface

Mọi chuyện lại tiếp tục êm xuôi và tưởng chừng đến đây code đã ngon nghẻ rồi. Một ngày đẹp trời khác lại đến, khách hàng của chúng ta đi uống cafe với một ông chuyên gia kỹ thuật nào đó. Kết thúc buổi cafe vui vẻ, chúng ta nhận một yêu cầu là nên lưu các sản phẩm trong Redis để lấy thông tin ra nhanh hơn (vì một lý do nào đó kiểu như sản phẩm ít khi thêm mới).

Chúng ta sẽ chọn giải pháp sửa hàm trong ProductRepository?

Câu trả lời là: KHÔNG. Và đây chính là lúc tôi sẽ trả lời cho các bạn tại sao trong thư mục Repositories lại có thư mục con là Eloquents. Bởi vì chúng ta cần một thư mục con khác có tên là Redis chẳng hạn, đồng thời ta sẽ xây dựng thêm một Repository mới có tên là RedisProductRepository:

<?php
// app/Repositories/Redis/RedisProductRepository.php

namespace App\Repositories\Redis;

class RedisProductRepository
{
    public function all()
    {
        return 'Get all product from Redis';
    }

    public function find($id)
    {
        return 'Get single product by id: ' . $id;
    }
}

Và ta sẽ thay đổi ProductsController thành:

<?php
// app/Http/Controllers/ProductsController.php

namespace App\Http\Controllers;

use App\Repositories\Redis\RedisProductRepository;

class ProductsController extends Controller
{
    protected $productRepository;

    public function __construct(RedisProductRepository $productRepository)
    {
        $this->productRepository = $productRepository;
    }

    public function index()
    {
        $products = $this->productRepository->all();

        return $products;
    }

    public function show($id)
    {
        $product = $this->productRepository->find($id);

        return $product;
    }

}

Ta chỉ có 2 thay đổi nhỏ:

  • Sử dụng App\Repositories\Redis\RedisProductRepository.
  • Thay đổi phần inject vào constructor.

Có vẻ có gì đó bất ổn ở đây, giả sử chúng ta đã sử dụng ProductRepository không chỉ ở Controller này mà còn một số Controller khác nữa. Thật thảm họa khi phải tìm hết các Controller và thay đổi lại thành RedisProductRepository. Rồi một ngày đẹp trời khác lại đến và chúng ta lại phải quay trở lại sử dụng ProductRepository thì sao? Công việc find and replace lại lặp lại. Như vậy rõ ràng cách làm này không ổn cho lắm.

Giải pháp đưa ra đó là sử dụng một Interface chung cho ProductRepositoryRedisProductRepository. Chúng ta sẽ thiết kế một ProductRepositoryInterface trong thư mục con của Repositories đặt tên là Contracts (bạn có thể đặt tên khác nếu muốn):

<?php
// app/Repositories/Contracts/ProductRepositoryInterface.php

namespace App\Repositories\Contracts;

interface ProductRepositoryInterface
{
    public function all();
    public function find($id);
}

Các bạn có thể mường tượng ra rằng ProductRepositoryRedisProductRepository của chúng ta sẽ phải implement từ ProductRepositoryInterface này. Hãy cùng triển khai chúng:

ProductRepository

<?php
// app/Repositories/Eloquents/ProductRepository.php

namespace App\Repositories\Eloquents;

use App\Repositories\Contracts\ProductRepositoryInterface;
use App\Models\Product;

class ProductRepository implements ProductRepositoryInterface
{
    public function all()
    {
        return Product::all();
    }

    public function find($id)
    {
        return Product::find($id);
    }
}

RedisProductRepository

<?php
// app/Repositories/Redis/RedisProductRepository.php

namespace App\Repositories\Redis;

use App\Repositories\Contracts\ProductRepositoryInterface;

class RedisProductRepository implements ProductRepositoryInterface
{
    public function all()
    {
        return 'Get all product from Redis';
    }

    public function find($id)
    {
        return 'Get single product by id: ' . $id;
    }
}

Tới đây các bạn có thể sẽ thắc mắc là xây dựng Interface để làm gì? Làm sao có thể inject Interface vào Controller được? Interface đâu có tạo ra instance được?

Đây chính là lúc ta cần sự trợ giúp của Laravel Service Container. Sự trợ giúp ở đây là gì? Nhìn link bạn cũng có thể đoán được đó là chúng ta có thể binding một interface vào một đối tượng implement nó. Chúng ta sẽ khai báo việc này trong hàm register của app/Providers/AppServiceProvider.php:

<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    /**
     * Bootstrap any application services.
     *
     * @return void
     */
    public function boot()
    {
        //
    }

    /**
     * Register any application services.
     *
     * @return void
     */
    public function register()
    {
        $this->app->bind(
            'App\Repositories\Contracts\ProductRepositoryInterface',
            'App\Repositories\Eloquents\ProductRepository'
        );
    }
}

(Thực ra thì có một cách hay hơn đó là chúng ta chủ động viết riêng một RepositoryServiceProvider và đăng ký với app tuy nhiên tôi xin phép không đề cập trong bài viết này mà tận dụng luôn AppServiceProvider)

Và thay đổi nhỏ trong ProductsController:

<?php
// app/Http/Controllers/ProductsController.php

namespace App\Http\Controllers;

use App\Repositories\Contracts\ProductRepositoryInterface;

class ProductsController extends Controller
{
    protected $productRepository;

    public function __construct(ProductRepositoryInterface $productRepository)
    {
        $this->productRepository = $productRepository;
    }

    public function index()
    {
        $products = $this->productRepository->all();

        return $products;
    }

    public function show($id)
    {
        $product = $this->productRepository->find($id);

        return $product;
    }

}

Ta có thể thấy Controller của chúng ta giờ chỉ phải làm việc với ProductRepositoryInterface, việc dữ liệu lấy ra từ Eloquent hay Redis Controller không cần quan tâm chỉ cần nhận được object đầu ra để xử lý tiếp. Mối quan tâm đó đã được đẩy sang cho AppServiceProvider giúp chúng ta. Sau này có thay đổi yêu cầu giữa EloquentRedis hay một loại nào khác (VD: Doctrine) ta chỉ cần sửa lại việc binding trong Service Provider (sửa một chỗ) mà Controller không cần phải quan tâm thay đổi gì. Đó chính là tính uyển chuyển cho ứng dụng của ta có được khi sử dụng Repository.

Kết luận

Trong bài viết này tôi đã giới thiệu tới các bạn:

  • Khái niệm cơ bản về Repository Pattern.
  • Các vấn đề thực tế sẽ gặp phải và cách giải quyết với Repository.
  • Một số ưu điểm dễ dàng nhận thấy được:
    • Giảm việc lặp code.
    • Quản lý riêng biệt, dễ dàng unit test cho phần business logic và phần logic lấy dữ liệu.
    • Code dễ dàng maintain.

Hy vọng bài viết giúp ích cho bạn trong quá trình tìm hiểu về Repository Design Pattern. Hẹn gặp lại các bạn ở các bài viết sau.

Tài liệu tham khảo

  1. Repository
  2. Don’t call Eloquent models directly, Use Repositories!
  3. Using Repository Pattern in Laravel 5
  4. REPOSITORY PATTERN ON LARAVEL