Repository design pattern là 1 mô hình code rất phổ biến trong các ngôn ngữ hiện đại. Nó không chỉ biểu đạt 1 cách rất xuất sắc kĩ thuật Dependence Injection mà còn giúp cho dự án tách biệt được giữa việc xử lý data access logic và business logic. Phong cách code này không phụ thuộc vào ngôn ngữ nào hết, dù bạn đang code Php, C#, Java… hoặc dù bạn đang code bằng framewok Laravel, CakePHP, Zend… thì cũng áp dụng được Repository design pattern. Trong phạm vi bài này mình sẽ sử dụng code laravel để mô tả ví dụ cho dễ hiểu nhé.
Bài viết này nằm trong seri laravel core:
- Những khái niệm quan trọng cần nắm vững – Recipes & Best Practices.
- N+1 trong laravel – Nguyên nhân và cách khắc phục.
- Phân biệt Method và Properties trong laravel, 4 trường hợp không nên sử dụng Properties.
- Phân biệt Eloquent và Query Builder, Nên sử dụng cái nào?
- Dependency Injection là gì? 3 phương pháp DI thường gặp
- Repository Design Pattern là gì? 3 Bước tối ưu code trong pattern này
- Service Container – 4 Phương pháp Binding thường dùng?
- Service Provider – Dùng như nào cho chuẩn.
- Facade là cái quần què gì – Cơ chế hoạt động của Facade – Thứ mà không 1 IDE nào hiểu được.
- Contract là gì? – Tưởng không quen mà quen không tưởng.
Repository Design Pattern
Repository Design Pattern là 1 mô hình làm việc với laravel project rất phổ biến. Hiểu 1 cách đơn giản thì chúng ta sẽ tạo ra thêm 1 tầng Repository ở giữa controller và Model. Tầng repository này sẽ có nhiệm vụ là thực hiện logic xử lý database. Từ đó sẽ tránh được việc viết logic xử lý data ở cả controller và model. Ngoài ra có thể tăng tính tái sử dụng cho dự án của mình.
Với Repository Design Pattern thì controller không làm việc trực tiếp với model nữa mà khi cần xử lý các tác vụ liên quan đến database thì controller sẽ làm việc thông qua 1 tầng gọi là repository. Mô hình này phổ biến trong laravel tới mức là có khá nhiều package được viết ra với chức năng support cho mô hình này ví dụ như: Laravel 5 Repository, Laravel Repository…
Ở 1 số dự án mình còn thấy có thêm 1 tầng service nữa, khi này service sẽ gọi tới repository, repository sẽ gọi tới model (Như ảnh minh họa bên trên). Họ sẽ tách biệt rõ ràng là service sẽ xử lý logic nghiệp vụ hệ thống, còn repository sẽ xử lý các logic liên quan tới data như format data, gom nhóm data…
Ví dụ liên quan đến Repository Design pattern
Để hiểu rõ hơn về repository design pattern ta cùng đi tìm hiểu về ví dụ của bài viết trước: 1 Author có thể đăng bài viết, có thể comment bài viết…
Cấu trúc thư mục như sau:
Trước tiên ta có thể hình dung là tất cả model trong hệ thống đều có vài method giống nhau như lấy tất cả bài viết, tìm kiếm bài viết theo ID… Ngoài ra trong bài viết về Dependence Injection chúng ta đã biết rằng để dễ maintain thì nên truyền Interface vào Constructor. Do đó chúng ta sẽ viết ra 1 cái interface để các Repository implement chúng:
interface RepositoryInterface { public function all($columns = array('*')); public function paginate($limit = null, $columns = array('*')); public function find($id, $columns = array('*')); public function findByField($field, $value, $columns = array('*')); public function findWhere( array $where , $columns = array('*')); public function findWhereIn( $field, array $values, $columns = array('*')); public function findWhereNotIn( $field, array $values, $columns = array('*')); public function create(array $attributes); public function update(array $attributes, $id); public function delete($id); public function with($relations); public function hidden(array $fields); public function visible(array $fields); public function scopeQuery(\Closure $scope); public function getFieldsSearchable(); public function setPresenter($presenter); public function skipPresenter($status = true); }
Tiếp theo là ứng với mỗi 1 model ta sẽ tạo 1 repository tương ứng với chúng và emplement RepositoryInterface này:
class AuthorRepository implements RepositoryInterface { public function all($columns = array('*')) { return Author::all($columns); } // các hàm khác sẽ được biết bên dưới }
Đại loại là như thế, tới đây chắc có anh em sẽ nghĩ ủa thế model nào cũng phải viết lại 1 đống method như thế ư???
Dễ thấy là dù lấy all của Author hay là all của Post thì logic xử lý chỉ khác nhau mỗi tên model thôi. Tới đây ta lại nhớ tới DI, nếu ta tiêm Tên model vào chả phải xong rồi ư??
Để giải quyết được vấn đề này thì ta tạo thêm 1 abtract class chứa các method giống nhau giữa các repository, đồng thời phải đảm bảo rằng các class extend abtract class này phải truyền model thích hợp vào cho nó.
abstract class BaseRepository implements RepositoryInterface { protected $app; protected $model; protected $fieldSearchable = array(); protected $presenter; protected $validator; protected $rules = null; protected $criteria; protected $skipCriteria = false; protected $skipPresenter = false; protected $scopeQuery = null; public function __construct(Application $app) { $this->app = $app; $this->criteria = new Collection(); $this->makeModel(); $this->makePresenter(); $this->makeValidator(); $this->boot(); } public function boot() { } public function resetModel() { $this->makeModel(); } abstract public function model(); public function presenter() { return null; } public function validator() { if ( isset($this->rules) && ! is_null($this->rules) && is_array($this->rules) && !empty($this->rules) ) { if ( class_exists('Prettus\Validator\LaravelValidator') ) { $validator = app('Prettus\Validator\LaravelValidator'); if ($validator instanceof ValidatorInterface) { $validator->setRules($this->rules); return $validator; } } else { throw new Exception( trans('repository::packages.prettus_laravel_validation_required') ); } } return null; } public function setPresenter($presenter) { $this->makePresenter($presenter); return $this; } public function makeModel() { $model = $this->app->make($this->model()); if (!$model instanceof Model) { throw new RepositoryException("Class {$this->model()} must be an instance of Illuminate\\Database\\Eloquent\\Model"); } return $this->model = $model; } public function makePresenter($presenter = null) { $presenter = !is_null($presenter) ? $presenter : $this->presenter(); if ( !is_null($presenter) ) { $this->presenter = is_string($presenter) ? $this->app->make($presenter) : $presenter; if (!$this->presenter instanceof PresenterInterface ) { throw new RepositoryException("Class {$presenter} must be an instance of Prettus\\Repository\\Contracts\\PresenterInterface"); } return $this->presenter; } return null; } public function makeValidator($validator = null) { $validator = !is_null($validator) ? $validator : $this->validator(); if ( !is_null($validator) ) { $this->validator = is_string($validator) ? $this->app->make($validator) : $validator; if (!$this->validator instanceof ValidatorInterface ) { throw new RepositoryException("Class {$validator} must be an instance of Prettus\\Validator\\Contracts\\ValidatorInterface"); } return $this->validator; } return null; } public function getFieldsSearchable() { return $this->fieldSearchable; } public function scopeQuery(\Closure $scope){ $this->scopeQuery = $scope; return $this; } public function all($columns = array('*')) { $this->applyCriteria(); $this->applyScope(); if ( $this->model instanceof \Illuminate\Database\Eloquent\Builder ){ $results = $this->model->get($columns); } else { $results = $this->model->all($columns); } $this->resetModel(); return $this->parserResult($results); } public function paginate($limit = null, $columns = array('*')) { $this->applyCriteria(); $this->applyScope(); $limit = is_null($limit) ? config('repository.pagination.limit', 15) : $limit; $results = $this->model->paginate($limit, $columns); $this->resetModel(); return $this->parserResult($results); } public function find($id, $columns = array('*')) { $this->applyCriteria(); $this->applyScope(); $model = $this->model->findOrFail($id, $columns); $this->resetModel(); return $this->parserResult($model); } public function findByField($field, $value = null, $columns = array('*')) { $this->applyCriteria(); $this->applyScope(); $model = $this->model->where($field,'=',$value)->get($columns); $this->resetModel(); return $this->parserResult($model); } public function findWhere( array $where , $columns = array('*')) { $this->applyCriteria(); $this->applyScope(); foreach ($where as $field => $value) { if ( is_array($value) ) { list($field, $condition, $val) = $value; $this->model = $this->model->where($field,$condition,$val); } else { $this->model = $this->model->where($field,'=',$value); } } $model = $this->model->get($columns); $this->resetModel(); return $this->parserResult($model); } public function findWhereIn( $field, array $values, $columns = array('*')) { $this->applyCriteria(); $model = $this->model->whereIn($field, $values)->get($columns); $this->resetModel(); return $this->parserResult($model); } public function findWhereNotIn( $field, array $values, $columns = array('*')) { $this->applyCriteria(); $model = $this->model->whereNotIn($field, $values)->get($columns); $this->resetModel(); return $this->parserResult($model); } public function create(array $attributes) { if ( !is_null($this->validator) ) { $this->validator->with($attributes) ->passesOrFail( ValidatorInterface::RULE_CREATE ); } $model = $this->model->newInstance($attributes); $model->save(); $this->resetModel(); event(new RepositoryEntityCreated($this, $model)); return $this->parserResult($model); } public function update(array $attributes, $id) { $this->applyScope(); if ( !is_null($this->validator) ) { $this->validator->with($attributes) ->setId($id) ->passesOrFail( ValidatorInterface::RULE_UPDATE ); } $_skipPresenter = $this->skipPresenter; $this->skipPresenter(true); $model = $this->model->findOrFail($id); $model->fill($attributes); $model->save(); $this->skipPresenter($_skipPresenter); $this->resetModel(); event(new RepositoryEntityUpdated($this, $model)); return $this->parserResult($model); } public function delete($id) { $this->applyScope(); $_skipPresenter = $this->skipPresenter; $this->skipPresenter(true); $model = $this->find($id); $originalModel = clone $model; $this->skipPresenter($_skipPresenter); $this->resetModel(); $deleted = $model->delete(); event(new RepositoryEntityDeleted($this, $originalModel)); return $deleted; } public function with($relations) { $this->model = $this->model->with($relations); return $this; } public function hidden(array $fields) { $this->model->setHidden($fields); return $this; } public function visible(array $fields) { $this->model->setVisible($fields); return $this; } }
Tới đây ta đã nhận ra cách code như vậy có các ưu điểm như sau:
- Giảm thiểu được code trùng lặp: Dù có tạo thêm bao nhiêu model đi chăng nữa thì cũng thao tác cơ bản như CRUD hay phân trang ta chỉ cần viết đúng 1 lần duy nhất mà thôi.
- Nhanh: Dù phần base sẽ hơi mất công 1 chút nhưng sau đó mọi thứ đều trở lên rất nhanh chóng.
- Giảm thiểu sai sót: Do code base đã được dựng trước, các code sau dường như phải thêm rất ít nên giảm thiểu được sự sai sót khi thêm model mới.
- Tiết kiệm thời gian khi maintain: sau này có sửa cũng chỉ cần sửa 1 chỗ base là đã áp dụng cho toàn bộ model rồi.
Lời kết
Tới đây mình chỉ giới thiệu về mô hình code repository design pattern thôi, còn xung quanh vấn đề này cũng không có gì để viết cả =)).
Bài viết sau mình sẽ viết về chủ đề Service Container – Tuyệt chiêu khiến 1 thằng ranh con không sợ bọn từ lớp 1 đến lớp 5 nếu anh em quan tâm có thể xem qua nhé
Chúc anh em có 1 ngày mới vui vẻ, mạnh khỏe bên gia đình và những người yêu thương. Nếu thấy bài viết này có ích thì ae hãy chia sẻ cho bạn bè và đồng nghiệp nhé…!