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 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.
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 ^^