Repository Design Pattern và ứng dụng của nó trong Laravel

Có khá nhiều bạn đã yêu cầu mình một bài viết về Repository Design Pattern. Vậy mục đích của nó là gì? Nó có thực sự cần thiết cho ứng dụng của bạn hay không? Những điểm mạnh, điểm yếu của nó là gì? Chúng ta cùng đi sâu tìm hiểu qua bài viết này nhé.
Repository Design Pattern và ứng dụng của nó trong Laravel
Repository Design Pattern và ứng dụng của nó trong Laravel

Repository Design Pattern là gì?

Đây là một mẫu thiết kế nâng cao mà các bạn mới tiếp xúc lập trình có lẽ cũng không để ý về nó lắm. Đối với các bạn đã có kinh nghiệm thực tập hay làm việc ở các công ti - chắc hẳn cũng đã được nghe các mentor của mình nói về nó. 

Repository Design Pattern (mình sẽ tạm viết tắt nó thành RD) là một trong những mẫu thiết kế được sử dụng nhiều nhất trong hầu hết các ngôn ngữ lập trình, các framework... như .NET, Java, PHP..., trải dài từ websites, services, applications,... hay kể cả mobile apps. 

Repository Design Pattern

RD là một lớp trung gian giữa Business Logic (BL) và Data Source (DB), các đối tượng trong lớp trung gian này được gọi là Repository. Giao tiếp giữa BL và DB sẽ được thực hiện thông qua các Interface.

Chúng đem lại sự chuẩn hóa (standardized) cho output và tách biệt hoàn toàn việc xử lí business logic và data access logic, giúp cho BL hoàn toàn không cần quan tâm tới công việc của DB (và ngược lại). Việc chia để trị này hướng tới mục tiêu: ai làm việc nấy, điều đó cũng khiến code của bạn sáng sủa hơn, rõ ràng hơn, và dễ maintenance hơn.

Nói một ví dụ thực tế - trong một nhà máy may mặc, mỗi công nhân đa số đều được chia theo nhóm, và mỗi nhóm chỉ làm một phần nhỏ trong khâu sản xuất. Có nhóm thì chịu trách nhiệm may cổ áo, nhóm may tay áo, nhóm may thân áo, nhóm ráp các bộ phận này lại với nhau, nhóm chịu trách nhiệm hấp ủi... Một nhóm chỉ tập trung vào một công việc cụ thể chắc chắn sẽ nhanh và ít tạo ra sản phẩm lỗi hơn so với một người làm từ đầu đến cuối đúng không nào? ^^

 

Lợi ích của Repository Design Pattern

  • Code dễ phát triển và maintenance khi làm việc theo nhóm.
  • Giảm thiểu thay đổi code khi có thay đổi về cấu trúc dữ liệu, DB hoặc BL.
  • BL và DB có thể test độc lập
  • Chuẩn hóa đầu ra dữ liệu
  • Giảm thiểu trùng lặp code (DRY - Don't Repeat Yourself)

 

Cũng có lợi bất cập hại

  • Viết nhiều, viết mệt, cái gì cũng phải nghĩ đến tách rời và đem xuống Repository và tái sử dụng =))
  • Dự án nhỏ, mì ăn liền thì không cần xài cũng được
  • Với việc thế giới đang chuyển dần sang microservice thì việc áp dụng RD cho mỗi mắt nhỏ trong microservice khá là dư thừa và tốn nhiều chi phí phát triển

 

Repository Design Pattern và Laravel

Nãy giờ nói lan man quá, toàn là kiến thức khô khan =)), giờ mình xin được phép tiếp tục phần chính của bài viết.

Trong Laravel, Repository là "cây cầu dừa" nối giữa Model và Controller, đây cũng là nơi tập trung xử lí các logic truy vấn dữ liệu.

Các truy vấn này trước đây được thực hiện trực tiếp ở Controller bây giờ sẽ được đưa vào Repository, lúc này Controller sẽ tương tác với DB thông qua Repository thay vì gọi trực tiếp Model. Việc thực hiện truy vấn như thế nào sẽ được Repository giấu kín bên trong (và Controller bản thân nó cũng chẳng cần quan tâm, cứ trả đúng - đủ dữ liệu về cho nó là được rồi).

Điều này cũng giống như bạn ra ngân hàng rút tiền vậy. Bạn chỉ có thể gởi yêu cầu tới nhân viên ngân hàng, sau đó nhân viên ngân hàng kiểm tra và lấy tiền đưa cho bạn. Bạn thử tự xông vào lấy tiền xem sao, vô tù bóc lịch là có nha =))

Ủa rồi phần xử lí BL đâu rồi?

Không phải mình code cùi nên bỏ trực tiếp phần xử lí BL vào trong Controller như vậy đâu nha các bạn =)) 

Trên thực tế, một số thao tác get dữ liệu đơn giản sẽ được gọi trực tiếp ở Controller thông qua Repository.

Đối với các business phức tạp sẽ có thêm một tầng Service ở giữa nữa. Có nghĩa là lúc này, Controller chỉ có trách nhiệm điều hướng xử lí logic xuống Service, và Service mới là nơi thực hiện các BL và cập nhật xuống DB

Phần Service này mình sẽ nói rõ thêm với các bạn ở một bài viết khác, dù sao bài viết này cũng chỉ nói về RD thôi mà đúng không ^^

 

Triển khai Repository Design Pattern đơn giản cho Laravel

Khách hàng của chúng ta cần xây dựng một mạng xã hội cho phép các publishers chia sẻ các albums ảnh và kiếm tiền donate cũng như sự nổi tiếng.

Trước tiên chúng ta sẽ xây dựng một Model.

// app/Album.php namespace App;

use Illuminate\Database\Eloquent\Model;

class Album extends Model
{
    protected $guarded = [
        'id',
        'created_at',
        'updated_at',
    ];
}

Kế tiếp là Controller

// app/Http/Controllers/AlbumController.php

namespace App\Http\Controllers;

use App\Album;

class AlbumController extends Controller
{
    /**
     * Nội dung trang Albums List
     */
    public function index()
    {
        $albums = Album::all();

        return $albums;
    }

    /**
     * Nội dung trang Albums Details
     */
    public function show($id)
    {
        $album = Album::findOrFail($id);

        return $album;
    }
}

Trong Controller, Album được gọi trực tiếp để truy vấn dữ liệu. Mọi chuyện đều êm đẹp cho tới khi khách hàng muốn thay đổi cách truy vấn dữ liệu: các Album sẽ được sắp xếp theo độ tương tác, số lượng views, hoặc trang Album Details được truy vấn bằng hash_id thay vì id... Chắc chắn chúng ta sẽ cần phải cập nhật lại Controller để truy vấn dữ liệu cho phù hợp với requirements của khách hàng.

Điều này hết sức nguy hiểm và củ chuối. Bạn thử tưởng tượng không chỉ có mỗi AlbumController thực hiện các thao tác như thế này, mà rất nhiều Controller khác cũng thực hiện điều tương tự. Việc update code nhiều chỗ như vậy sẽ làm tăng khả năng bỏ sót hoặc thao tác sai lầm.

Và đây là lúc Repository lên sàn =))

Chúng ta sẽ tạo một Repository như sau

// app/Repositories/Eloquent/AlbumRepository.php

namespace App\Repositories\Eloquent;

use App\Album;

class AlbumRepository
{
    public function all()
    {
        return Album::orderBy('views_count', 'desc')->all();
    }

    public function find($id)
    {
        return Album::firstOrFail(['hash_id' => $id]);
    }
}

Cập nhật lại nội dung Controller

// app/Http/AlbumController.php

namespace App\Http\Controllers;

use App\Album;
use App\Repositories\Eloquent\AlbumRepository;

class AlbumController extends Controller
{
    protected $albumRepository;

    public function __construct(AlbumRepository $albumRepository)
    {
        $this->albumRepository = $albumRepository;
    }

    public function index()
    {
        $albums = $this->albumRepository->all();

        return $albums;
    }

    public function show($id)
    {
        $album = $this->albumRepository->find($id);

        return $album;
    }
}

Vậy là từ giờ trở đi, bạn cần thêm logic gì cứ chui vào Repository mà sửa, rõ ràng - sạch sẽ - khô thoáng - dễ hiểu phải không nào ^^

 

Câu chuyện vẫn chưa tới hồi kết

Vào một ngày nọ, khách hàng của chúng ta nghe phong phanh đâu đó bảo rằng dữ liệu của website mình hầu như người ta chỉ có xem là chính, không cần cập nhật gì nhiều cả. Kết thúc chương trình, ông khách hàng yêu cầu chúng ta đọc dữ liệu lên từ cache thay vì truy cập DB như hiện tại. 

Giờ chúng ta phải làm sao? Sửa lại các hàm trong AlbumRepository chăng?

Sai. Chúng ta sẽ tạo ra một repository khác chịu trách nhiệm xử lí caching cho AlbumRepository.

Ở đây mình sẽ áp dụng một mẫu thiết kế khác, đó chính là Decorator Pattern. Mẫu thiết kế này giúp chúng ta thêm các tính năng mới mà không cần phải cập nhật lại các lớp hiện tại (lớp ở đây chính là AlbumRepository).

// app/Repositories/Cache/AlbumRepositoryCacheDecorator.php

namespace App\Repositories\Cache;

use App\Repositories\Eloquent\AlbumRepository;;

class AlbumRepositoryCacheDecorator
{
    protected $repository;

    public function __construct()
    {
        $this->repository = new AlbumRepository();
    }

    public function all()
    {
        /*If cache exists, get data from cache*/
        if ('has-cache') {
            return 'data-from-cache';
        }

        $albums = $this->repository->all();

        /*Logic to store cache*/

        return $albums;
    }

    public function find($id)
    {
        /*If cache exists, get data from cache*/
        if ('has-cache') {
            return 'data-from-cache';
        }

        $album = $this->repository->find($id);

        /*Logic to store cache*/

        return $album;
    }

    public function update($id, array $data)
    {
        $this->repository->update($id, $data);

        /*Logic to clear cache*/
    }
}

Sau đó chúng ta cần import AlbumRepositoryCacheDecorator thay vì AlbumRepository

// app/Http/AlbumController.php

namespace App\Http\Controllers;

use App\Repositories\Cache\AlbumRepositoryCacheDecorator;

class AlbumController extends Controller
{
    protected $albumRepository;

    public function __construct(AlbumRepositoryCacheDecorator $albumRepository)
    {
        $this->albumRepository = $albumRepository;
    }

    public function index()
    {
        $albums = $this->albumRepository->all();

        return $albums;
    }

    public function show($id)
    {
        $album = $this->albumRepository->find($id);

        return $album;
    }
}

Các bạn cần chú ý sự thay đổi ở đây: chúng ta đã thay đổi thứ được inject vào __construct

Củ chuối lắm các bạn à. Bởi AlbumRepository không chỉ được sử dụng ở AlbumController như ví dụ trên, nó còn có thể được sử dụng ở hàng tá chỗ khác, nếu như chúng ta cập nhật một cách thủ công như vậy sẽ có thể dẫn đến nhiều lỗi không muốn, và khiến code của chúng ta lặp đi lặp lại nhiều lần.

Với sự trợ giúp của Laravel Service Container, chúng ta có thể bind một interface tới một class nhất định. 

Đầu tiên chúng ta sẽ tạo ra một interface như sau

// app/Repositories/Contracts/AlbumRepositoryContract.php

namespace App\Repositories\Contracts;

interface AlbumRepositoryContract
{
    public function all();

    public function find($id);
}

Sau đó chúng ta cần chỉnh sửa nội dung cho hai lớp AlbumRepository và AlbumRepositoryCacheDecorator sao cho chúng implements AlbumRepositoryContract trên.

use App\Repositories\Contracts\AlbumRepositoryContract;

class AlbumRepository implements AlbumRepositoryContract {}

class AlbumRepositoryCacheDecorator implements AlbumRepositoryContract {}

Bước kế tiếp quan trọng nhất: chúng ta cần khai báo cho Laravel biết cách xử lí khi chúng ta gọi interface binding. Chúng ta sẽ cập nhật nội dung phương thức register bên trong tập tin app/Providers/AppServiceProvider.php.

namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use App\Repositories\Contracts\AlbumRepositoryContract;
use App\Repositories\Cache\AlbumRepositoryCacheDecorator;

class AppServiceProvider extends ServiceProvider
{
    /**
     * Register any application services.
     *
     * @return void
     */
    public function register()
    {
        $this->app->bind(AlbumRepositoryContract::class, AlbumRepositoryCacheDecorator::class);
    }
}

Việc cuối cùng chúng ta cần làm là cập nhật lại file AlbumController một chút

namespace App\Http\Controllers;

use App\Repositories\Contracts\AlbumRepositoryContract;

class AlbumController extends Controller
{
    protected $albumRepository;

    public function __construct(AlbumRepositoryContract $albumRepository)
    {
        $this->albumRepository = $albumRepository;
    }

    ...
}

Boom! Mọi thứ tới đây đã khá ổn rồi. Nếu như khách hàng có đổi ý, không muốn sử dụng cache nữa, chúng ta chỉ việc cập nhật lại AppServiceProvider. Khá là đơn giản đúng không nào ^^

namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use App\Repositories\Contracts\AlbumRepositoryContract;
use App\Repositories\Eloquent\AlbumRepository;

class AppServiceProvider extends ServiceProvider
{
    /**
     * Register any application services.
     *
     * @return void
     */
    public function register()
    {
        $this->app->bind(AlbumRepositoryContract::class, AlbumRepository::class);
    }
}

Thậm chí sau này, nếu như khách hàng muốn lưu các albums ở một database khác, chúng ta cũng sẽ không cần cập nhật lại các logic nghiệp vụ ở AlbumController nữa, mà sẽ tạo ra các Repository liên quan và bind chúng lại ở AppServiceProvider.

 

Bài viết đến đây là kết thúc rồi. Mình hi vọng bài viết này sẽ giúp các bạn hiểu rõ hơn về RD cũng như ứng dụng nó vào Laravel.

Nếu bài viết có nội dung nào đó không chính xác, hoặc các bạn có câu hỏi muốn hỏi mình, hãy để lại comments bên dưới hoặc liên lạc với mình nhé.

Cám ơn các bạn đã theo dõi. Hẹn gặp lại các bạn trong các bài viết vọc vạch Laravel kế tiếp ^^

 

Comments

Bài viết nổi bật

PHP là ngôn ngữ được sử dụng rộng rãi nhất trên thế giới trong lập trình web. Nó cũng bị ghét nhất. Nhưng tại sao nhiều developer lại ghét nó đến vậy? Hôm nay chúng ta hãy cùng tìm hiểu lý do xem chúng có thuyết phục không nhé ^_^
Dạo gần đây đi đâu cũng nghe nói về microservices, người người nhà nhà rục rịch chuyển dịch hệ thống sang microservices. Trước khi đưa ra sự so sánh, mình sẽ khái quát một chút về Monolith Application và MicroServices một chút cho các bạn chưa biết nắm rõ hơn nhé.
Lúc trước mình hay sử dụng cách này trên laptop phụ của mình, giờ mua license luôn rồi. Hôm nay mình xin chia sẻ cho bạn nào cần nhé.
JWT Tokens là một cách thức lưu trữ thông tin xác thực hiệu quả, nhưng làm cách nào để chúng ta có thể giúp chúng an toàn hơn? Có 2 cách thường dùng để lưu trữ JWT Tokens là LocalStorage và Cookies. Bây giờ chúng ta sẽ bắt đầu "mổ xẻ" các ưu - nhược điểm của mỗi loại nhé.
Có khá nhiều bạn đã yêu cầu mình một bài viết về Repository Design Pattern. Vậy mục đích của nó là gì? Nó có thực sự cần thiết cho ứng dụng của bạn hay không? Những điểm mạnh, điểm yếu của nó là gì? Chúng ta cùng đi sâu tìm hiểu qua bài viết này nhé.

Mục lục

Related posts

Triển khai Saga Pattern trong microservices với NodeJS và Choreography-Based Saga
Mô hình Saga đưa ra một giải pháp có cấu trúc để giải quyết thách thức này. Nó cung cấp một phương pháp có hệ thống để quản lý transaction qua nhiều microservices. Điều này giải quyết những phức tạp của các transaction phân tán và hoàn toàn tương thích với các nguyên tắc của kiến trúc microservices, được đặc trưng bởi sự kết nối lỏng lẻo và khả năng triển khai độc lập của các service.
Một API cho phép giao tiếp hai chiều giữa các ứng dụng phần mềm thông qua các requests. Một Webhook là một API nhẹ, hỗ trợ chia sẻ dữ liệu một chiều được kích hoạt bởi các events.
Một trong những câu hỏi được đặt thường xuyên nhất về TypeScript là liệu chúng ta nên sử dụng interface hay type. Câu trả lời cho câu hỏi này, giống như nhiều câu hỏi lập trình khác, là nó phụ thuộc vào tình hình cụ thể. Trong một số trường hợp, một cái có lợi thế rõ rệt hơn cái kia, nhưng trong nhiều trường hợp, chúng có thể thay thế cho nhau.
Đây là các types cơ bản nhưng cũng phổ biến nhất trong Typescript. Một số types khác phức tạp hơn cũng được xây dựng dựa trên những types cơ bản này.
Trong thế giới lập trình, trách nhiệm lớn nhất của chúng ta không phải chỉ làm cho code chạy được, mà còn phải đảm bảo rằng các đoạn code mà chúng ta viết có thể dễ dàng kiểm tra và bảo trì trong một khoảng thời gian dài.
Phân trang - một thành phần không thể thiếu trong các ứng dụng có lượng dữ liệu lớn. Tuy nhiên, bạn hiểu được bao nhiêu về nó?
Javascript là một thành phần không thể thiếu đối với frontend developers. Tuy nhiên, ngay từ lúc ra đời, nó đã tồn tại khá nhiều vấn đề cần khắc phục. Đó là lý do tại sao từ 2015 (ES6) tới 2021 (ES12) ra đời nhằm giúp Javascript trở nên tốt hơn.
Dạo này mình làm việc với mấy bạn trên github, thấy hay xài mấy từ viết tắt mà mình không hiểu lắm. Thôi thì tổng hợp lại một list các từ viết tắt hay dùng trong github luôn cho ai cần :D
Dạo gần đây đi đâu cũng nghe nói về microservices, người người nhà nhà rục rịch chuyển dịch hệ thống sang microservices. Trước khi đưa ra sự so sánh, mình sẽ khái quát một chút về Monolith Application và MicroServices một chút cho các bạn chưa biết nắm rõ hơn nhé.
Cách bỏ qua câu lệnh --set-upstream quen thuộc cho các con lười
Mình sẽ giới thiệu 2 cách để xóa một property trong Javascript Object. Một cách sử dụng mutable - toán tử delete, một cách còn lại là immutable - tính năng Object Restructuring.

Tin mới nhất

Triển khai Saga Pattern trong microservices với NodeJS và Choreography-Based Saga
Mô hình Saga đưa ra một giải pháp có cấu trúc để giải quyết thách thức này. Nó cung cấp một phương pháp có hệ thống để quản lý transaction qua nhiều microservices. Điều này giải quyết những phức tạp của các transaction phân tán và hoàn toàn tương thích với các nguyên tắc của kiến trúc microservices, được đặc trưng bởi sự kết nối lỏng lẻo và khả năng triển khai độc lập của các service.
Một API cho phép giao tiếp hai chiều giữa các ứng dụng phần mềm thông qua các requests. Một Webhook là một API nhẹ, hỗ trợ chia sẻ dữ liệu một chiều được kích hoạt bởi các events.
Một trong những câu hỏi được đặt thường xuyên nhất về TypeScript là liệu chúng ta nên sử dụng interface hay type. Câu trả lời cho câu hỏi này, giống như nhiều câu hỏi lập trình khác, là nó phụ thuộc vào tình hình cụ thể. Trong một số trường hợp, một cái có lợi thế rõ rệt hơn cái kia, nhưng trong nhiều trường hợp, chúng có thể thay thế cho nhau.
Trong phần này, chúng ta sẽ tìm hiểu một số khái niệm cơ bản nhất về AWS là gì và một số lợi ích khi sử dụng AWS.
Trở thành một software developer hiệu suất cao không phải là điều dễ dàng. Điều này đòi hỏi bạn phải có kỹ năng và kiến thức về lập trình, cũng như cách tiếp cận và giải quyết các vấn đề phức tạp. Tuy nhiên, nếu bạn có chút kiên nhẫn và sự nỗ lực, bạn hoàn toàn có thể trở thành một developer tài năng và thành công.
Đây là các types cơ bản nhưng cũng phổ biến nhất trong Typescript. Một số types khác phức tạp hơn cũng được xây dựng dựa trên những types cơ bản này.
Trong thế giới lập trình, trách nhiệm lớn nhất của chúng ta không phải chỉ làm cho code chạy được, mà còn phải đảm bảo rằng các đoạn code mà chúng ta viết có thể dễ dàng kiểm tra và bảo trì trong một khoảng thời gian dài.
Thông tin được định nghĩa dưới dạng dữ liệu, kiến thức về thông tin, và trí tuệ về tri thức.
Phân trang - một thành phần không thể thiếu trong các ứng dụng có lượng dữ liệu lớn. Tuy nhiên, bạn hiểu được bao nhiêu về nó?
Javascript là một thành phần không thể thiếu đối với frontend developers. Tuy nhiên, ngay từ lúc ra đời, nó đã tồn tại khá nhiều vấn đề cần khắc phục. Đó là lý do tại sao từ 2015 (ES6) tới 2021 (ES12) ra đời nhằm giúp Javascript trở nên tốt hơn.
Dạo này mình làm việc với mấy bạn trên github, thấy hay xài mấy từ viết tắt mà mình không hiểu lắm. Thôi thì tổng hợp lại một list các từ viết tắt hay dùng trong github luôn cho ai cần :D
Triển khai Saga Pattern trong microservices với NodeJS và Choreography-Based Saga
Mô hình Saga đưa ra một giải pháp có cấu trúc để giải quyết thách thức này. Nó cung cấp một phương pháp có hệ thống để quản lý transaction qua nhiều microservices. Điều này giải quyết những phức tạp của các transaction phân tán và hoàn toàn tương thích với các nguyên tắc của kiến trúc microservices, được đặc trưng bởi sự kết nối lỏng lẻo và khả năng triển khai độc lập của các service.
Một API cho phép giao tiếp hai chiều giữa các ứng dụng phần mềm thông qua các requests. Một Webhook là một API nhẹ, hỗ trợ chia sẻ dữ liệu một chiều được kích hoạt bởi các events.
Một trong những câu hỏi được đặt thường xuyên nhất về TypeScript là liệu chúng ta nên sử dụng interface hay type. Câu trả lời cho câu hỏi này, giống như nhiều câu hỏi lập trình khác, là nó phụ thuộc vào tình hình cụ thể. Trong một số trường hợp, một cái có lợi thế rõ rệt hơn cái kia, nhưng trong nhiều trường hợp, chúng có thể thay thế cho nhau.
Đây là các types cơ bản nhưng cũng phổ biến nhất trong Typescript. Một số types khác phức tạp hơn cũng được xây dựng dựa trên những types cơ bản này.