Giới thiệu cơ bản về Repository Pattern

Giới thiệu cơ bản về  Repository Pattern

Ngày nay có rất nhiều lời bàn tán về các mẫu design patterns trong phát triển các phần mền, và một trong những số câu hỏi thường gặp nhất đó là " Làm thế nào để triển khai và sử dụng được các công nghệ đó trong phầm mền. Theo câu hỏi này các bạn phải nhớ đó là các design patterns không phụ thuộc vào technology, framework hoặc programing language nào cả. Trong kỹ thuật phần mền một design pattern là một giải pháp có thể tái sử dụng cho một vấn đề chung xảy ra trong một bối cảnh nhất định trong khi thiết kế phần mền và một design pattern không phải là một design được chuyển đổi trực tiếp vào mã code của chúng ta.

Nếu các bạn thực sự hiểu về Repository Pattern thì không quan trọng các bạn đang sử dụng Framework hay programming languge nào. Điều quan trọng là các bạn hiểu đằng sau Repository Pattern là gì và áp dụng nó. Sau đó thì các bạn có thể áp dụng nó ở bất kỳ loại công nghệ (technology) nào mà các bạn muốn. Với cách giải thích của mình ở trên thì để biết thêm chi tiết thì chúng ta hãy bắt đầu với định nghĩa về Repository pattern.

1. Khái niệm về Repository pattern là gì.
Đầu tiên để hiểu về khái niệm chúng ta sẽ cùng xem ảnh dưới đây.

repository

Nhìn vào ảnh này các ban có thể hình dung qua qua nó rồi chứ, Repository Pattern là lớp trung gian giữa tầng Data AccessBusiness Logic, hiểu môm na thì nó là lớp trung gian giữa việc truy cập dữ liệu và xử lý logic giúp cho việc truy cập dữ liệu chặt chẽ và bảo mật hơn.

Bình thường với các bạn mới bắt đầu code hoặc mới tiếp cần đến lập trình php hay bất kỳ một loại ngôn ngữ nào đó thì để lấy dữ liệu gì đó hiển thị ra view thì chúng ta đơn giản viết một Controller query đến Database để lấy dữ liệu đó ra phải không nào. Nhưng với Repository pattern như trên hình mình vừa thêm thì chúng ta thấy Repository nó nằm giữa là lớp trung gian giữa ControllerModel. Để hiểu đơn giản là như này khi có request gọi tới Controller và tiếp đến thì Controller gọi tới Repository rồi nó mới gọi đến Model để lấy data và xử lý logic. Khi nào Controller muốn lấy dự liệu thì cứ gọi đến thằng này là lấy được data. Trên đây là lý thuyết còn để áp dụng vào dự án thì các bạn xem ví dụ dưới đây. Mình làm ví dụ về php laravel các bạn nhé.

2. Sử dụng Repository pattern trong laravel.
Mình đặt ra một bài toán như sau: Giả sử mình có một lớp Product và các bạn cần và muốn lấy ra danh sách các sản phẩm và sắp xếp theo ID giảm dần?
Đề bài khá là đơn giản đúng không nào. Theo như cách các bạn làm thông thường thì các bạn sẽ tạo ra ProductController và viết một hàm như dưới đây.

public function getProduct()
{
    $products = Product::orderBy('id', 'desc')->get();

    return view('product.index', compact('products'));
}

Vậy là xong easy phải không nào. Còn nếu viết theo Repository pattern thì chúng ta sẽ phải tạo thêm một lớp là ProductRepository trong một thư mục tên là Repositories, thư mục này trong app/

namespace App\Repositories;
use App\Models\Product;

class ProductRepository
{
    public function getProductOrderById()
    {
        return Product::orderBy('id', 'desc')->get();
    }
}

Và trong ProductController lúc này chúng ta sẽ viết như sau:

namespace App\Http\Controllers;

class ProductController extends Controller
{
    protected $productRepository;

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

        return view('product.index', compact('products'));
    }
}

Đến đây chắc các bạn sẽ tự hỏi là tại sao lại phải mất công làm như thế có đúng không nào, đang từ một lớp lấy dữ liệu cũng OK tại sao lại phải viết thêm một lớp nữa làm gì cho mệt. Dữ liệu lấy ra cũng như vậy chả khác gì mà ban đầu chỉ mất vài dòng code => là lấy được, giờ tốn thêm time và thêm vài chục dòng code nữa, tại sao lại phải như vậy ?.

Mới đầu mình tìm hiểu về Repository cũng nẩy ra câu hỏi này trong đầu và khi mình viết lên bài này thì mĩnh nghĩ là một số bạn cũng có cùng suy nghỉ này như mình khi mới đầu tìm hiểu về repository.

Các bạn thấy đấy như từ đầu mình có nói theo các viết không dùng repository thì Controller sẽ gắn chặt và làm việc trực tiếp với model. Nếu khi mà Model có sự thay đổi hay là refactor code bảng này bảng kia mình ví dụ như Name của bảng Products chúng ta cần thay đổi lại là Title thì chúng ta sẽ gặp rất nhiều rác rối khi phải đi tìm code trong Controller xem chỗ nào dùng đến cái tên đó để sửa. Hoặc là vào một ngày đẹp trời khách hàng của các bạn muốn thay đổi không lấy theo ID giảm dần nữa mà lại lấy theo số sản phẩm bán chạy nhất giảm dần thế thì biết làm sao được các bạn bắt buộc phải làm theo ý khách thôi có đúng không nào.

Các bạn lại nghĩ qúa Easy đúng không là vào Controller tìm đến chỗ nào có OrderbyId desc rồi sửa thành OrderBy số lượng sản phẩm bán nhiều desc là xong. Nhưng vấn đề ở đây mà mình muốn nói tới đó là một dự án các bạn sẽ dùng lại function đó rất nhiều lần và có thể là trong nhiều Class khác nhau. Khi đó chúng ra lại phải đi mò tìm xem các Class Controller nào có đoạn code đó để sửa nó theo yêu cầu của khách hàng thì qúa tội và qúa mất nhiều thời gian, chưa kể trong khi sửa lúc tỉnh lúc mơ không may các bạn lại xoá nhầm hay thêm bớt gì đó trong code ở một vài chỗ thì toang, rồi lại ngồi mò bug thì rất là mệt => Lúc này chình Repository phát huy được tác dụng của nó mạng lại cho các bạn.

Khi mà các bạn viết trong reposiroy thì đơn giản các bận chỉ cần vào repositories rồi tìm chỗ cần sửa, chỉ cần sửa 1 chỗ thì tất cả các controller mà gọi và sử dụng repository này cũng sẽ thay đổi như mình mong muốn => các bạn đến đây đã thấy công dụng mà nó mạng lại chưa ạ.

Chưa hết đâu các bạn nhé. Tiếp đến trời hôm nay không đẹp nữa mà chuyển sang âm u và mưa nhiều. Ông khách hàng của các bạn lại đưa ra một yêu cầu là không dùng MySql Database nữa mà dùng MogoDB để lưu chữ dữ liệu mà mình khuyển nhủ mãi mà ông ấy không có nghe nếu mình mà không làm thì khách hàng đánh gía các bạn và công ty các bạn ra sao, thôi thì đành chiều theo ý ông ấy vậy phải không nào. Các bạn sẽ phải tìm đến ProductRepository vừa mới tạo hôm qua rồi đổi lại thành ProductRepositoryMongo ... đúng không qúa Easy, sửa xong rồi một vài ngày sau ông ấy lại không thấy oke cho lắm và tốn chi phí hơn chẳng hạn mình ví dụ vậy, ông ấy lại muốn qua trở về như cũ => đến đây là có vấn đề rồi. Vậy giải pháp của các bạn ở đây là gì?

Để  giải quyết vấn đề trên thì mình chỉ cho các bạn đó là chúng ta sẽ tạo ra một Interface chung cho các loại repositories. Để làm được điều này chúng ra sẽ tạo thêm một thư mục là Contracts và bên trong tạo thêm một thư mục tên là Repositories để viết Interface chung như đã nói trên vào đó, sau đó tạo một interface tên là ProductRepositoryInterface ở trong đó. Tên mình đặt kia là không bắt buộc các bạn có thể đặt tên khác tùy ý các bạn đặt sao cho dễ hiểu tránh nhầm tưởng sang trức năng khác. Hoặc viết thư mục Contracts bên trong thư mục Repositories mà các bạn tạo ban đầu cũng đươc. Tất nhiên là với một dự án thì chúng ta phải xây dựng nhiều Interface ví dụ này mình xây dựng cho Product. Có thểm Interface khác thì các bạn điều đặt hết trong App/Contracts/Repositories

namespace App\Contracts\Repositories;

interface ProductRepositoryInterface
{
    public function getProductOrderById();
    public function getProductById(int $productId);
    public function deleteProduct(int $productId);
    public function createProduct(array $productDetails);
    public function updateProduct(int $productId, array $newDetails);
    ...
}

Vậy là chúng ta đã có 1 interface như một khuôn mẫu để chúng ta cho các Repositories inplement. Nếu trong project của các bạn có không chỉ là ProductRepositoryInterface mà còn nhiều Interface khác như là UserRepositoryInterface, NewRepositoryInterface,  và các bạn nhận ra là trong các Interface này điều có các function tương tự nhau getAll(), update(), create(), delete().... Các function mà Interface nào mà các bạn cũng có thì các bạn nên xây dựng một Interface chung để khai báo các hàm chung trong đó, và lúc này ProductRepositoryInterface, UserRepositoryInterface, NewRepositoryInterface sẽ extend từ cái Interface chung vừa nói trên đặt nó là BaseRepositoryInterface.

namespace App\Contracts\Repositories;

interface BaseRepositoryInterface
{
    public function model();
    public function getAll();
    public function create(array $data);
    public function show(int $id);
    public function edit(int $id);
    public function delete(int $id);
    ....
}

Và nếu cần một function riêng nào đó không có trong BaseRepositoryInterface thì chúng ta chỉ cần khai báo thêm trong các Interface extend

namespace App\Contracts\Repositories;

interface ProductRepositoryInterface extends BaseRepositoryInterface
{
    public function getFulfilledProduct(int $productId);
    public function getProductTags(int $productId);
    .....
}

Lúc này thì ProductRepository chúng ta vừa viết lúc nãy sẽ implements từ ProductRepositoryInterface sẽ phải có chút thay đổi như sau:

namespace App\Repositories;
​
use App\Models\Product;
​
class ProductRepository implements ProductRepositoryInterface
{
{
    //override
    public function getProductOrderById()
    {
        return Product::orderBy('id', 'desc')->get();
    }
}

Với  Mongo db thì tương tự cũng sẽ implements từ ProductRepositoryInterface. Và bây giờ trong ProductController chúng ta sẽ thay đổi.

namespace App\Http\Controllers;
use ProductRepositoryInterface;

class ProductController extends Controller
{    
     protected $productRepository;

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

    public function getProduct()
    {
        $products = $this->productRepository->getProductOrderById();
        return view('product.index', compact('products'));
    }
}

Các bạn có nhận ra là, khi chúng ta thay đổi ProductController thế kia thì khi chạy chắc chắc sẽ bị báo lỗi vì ProductRepositoryInterface là một Interface và tất nhiên Interface thì không thể tạo instance được, chúng ta không thể inject Interface vào Controller. Nhưng với Laravel thì được đấy, với Service container nó có thể giúp chúng ta bind một interface vào một implement của nó. Các bạn có thể tìm hiểu Service container và Dependency Injection để hiểu rõ hơn cách làm việc của nó nhé.

Tiếp theo các bạn phải vào thư mục Providers và tìm đến AppServiceProvider để đăng kí. Trong phương thức register() các bạn sẽ thêm như sau.

use App\Contracts\Repositories\PropductRepositoryInterface;
use App\Repositories\ProductRepository;

public function register()
{
   $this->app->bind(ProductRepositoryInterface::class, ProductRepository::class);
}

Như vậy là chúng ta đã register xong và có thể inject ProductRepositoryInterface trong ProductController. Nếu project các cần bind nhiều Interface thì tốt nhất là nên tạo ra một file riêng trong app/Providers chứ không nhất thiết phải dùng AppServiceProvider. Nếu dung cách tạo ra file mới, thì phải khai báo file đó config/app.php và thêm vào providers.

'providers' => [
...
    App\Providers\RepositoryServiceProvider::class,
],

Tiếp ProductController của các bạn sẽ chỉ làm việc với ProductRepositoryInterface, các bạn đã thấy lợi ích của Repository Pattern rồi chứ. Trong ProductRepositoryInterface chúng ta sẽ xây dựng những phương thức chung cho các Repositories implements để thực hiện chúng, trong Controller thì chúng ta sẽ tiến hành gọi như trên để lấy dữ liệu.

Vậy là qua bài này mình đã giới thiệu cơ bản xong cho các bạn về Repository Pattern trong Laravel. Hy vọng có thể giúp các bạn đang muốn tìm hiểu và muốn clean code có thể phần nào hiểu được và áp dụng.

Link tham khảo:

https://viblo.asia/p/laravel-design-patterns-series-repository-pattern-part-3-ogBG2l1ZRxnL