Laravel Design Patterns Series: Builder (Manager) Pattern - Part 1
Trong series bài viết này tôi muốn giới thiệu với các bạn về các Design Pattern sử dụng trong Laravel Framework. Với từng Pattern chúng ta sẽ cùng tìm hiểu cơ bản về định nghĩa, vấn đề hay bài toán đặt ra, giải pháp, cách áp dụng trong PHP nói chung và cụ thể trong Laravel Framework nói riêng. ...
Trong series bài viết này tôi muốn giới thiệu với các bạn về các Design Pattern sử dụng trong Laravel Framework. Với từng Pattern chúng ta sẽ cùng tìm hiểu cơ bản về định nghĩa, vấn đề hay bài toán đặt ra, giải pháp, cách áp dụng trong PHP nói chung và cụ thể trong Laravel Framework nói riêng. Danh sách các Pattern mà tôi muốn giới thiệu:
- Builder (Manager) Pattern - Part 1 (current)
- Factory Pattern - Part 2
- Repository Pattern - Part 3
- Strategy Pattern - Part 4
- Provider Pattern - Part 5
- Facade Pattern - Part 6
Bài viết đầu tiên tôi xin giới thiệu về Builder Pattern.
Bài toán đặt ra
Khi khai báo một lớp (Class) nào đó chúng ta biết đến khái niệm constructor. Một thực thể (Instance) của lớp được tạo ra bao giờ cũng được gọi hàm constructor để khởi tạo các thành phần (hay có thể hiểu như các thuộc tính) ban đầu. Như vậy các đối tượng được sinh ra từ một lớp nào đó với cùng một constructor sẽ có các thể hiện giống nhau.
Vấn đề đặt ra khi ta phải làm việc với các đối tượng phức tạp:
- Được tạo ra từ nhiều thành phần nhỏ lắp ghép lại.
- Trong các thành phần nhỏ tạo nên đối tượng có những thành phần bắt buộc và có những thành phần không bắt buộc.
Để dễ tưởng tượng hơn chúng ta hãy cùng đến với một ví dụ cụ thể. Trong ứng dụng của tôi có quản lý đối tượng người dùng (user), tôi có một khai báo cho lớp User như sau:
class User { private $username; // required private $email; // required private $birthday; // optional private $description; // optional function __construct($usernameParam, $emailParam, $birthdayParam = "", $descriptionParam = "") { $this->username = $usernameParam; $this->email = $emailParam; $this->birthday = $birthdayParam; $this->description = $descriptionParam; } }
Chúng ta có lớp User với các thuộc tính bắt buộc là username và email, đồng thời các thuộc tính không bắt buộc là birthday và description. Hàm constructor gồm bốn đầu vào và các giá trị optional sẽ có giá trị mặc định (nếu không được truyền giá trị).
Ta có thể nhận ra một số nhược điểm của cách làm này:
- Nếu có thêm nhiều thuộc tính thì constructor sẽ phải khai báo dài.
- Khi khởi tạo đối tượng từ lớp User chúng ta luôn phải để ý xem thứ tự khai báo các biến thế nào, giá trị nào bắt buộc phải truyền vào, giá trị nào không cần phải truyền và nếu không truyền thì giá trị mặc định là bao nhiêu.
- Khi thêm thuộc tính hoặc thứ tự thuộc tính trong hàm khởi tạo thay đổi sẽ gây không ít phiền hà.
Giải pháp
Đến đây ta có thể đưa ra giải pháp cho những nhược điểm vừa nêu ở trên như sau:
- Chỉ truyền những thuộc tính bắt buộc vào hàm khởi tạo.
- Các thuộc tính không bắt buộc cho phép khởi tạo thông qua setter.
Cụ thể cách triển khai như sau:
class User { private $username; // required private $email; // required private $birthday; // optional private $description; // optional function __construct($usernameParam, $emailParam) { $this->username = $usernameParam; $this->email = $emailParam; } public function setBirthday($birthdayParam) { $this->birthday = $birthdayParam; } public function setDescription($descriptionParam) { $this->description = $descriptionParam; } }
Tuy nhiên cách làm này vẫn tồn tại những nhược điểm:
- Khi số lượng thuộc tính không bắt buộc tăng lên ta sẽ phải triển khai nhiều setter.
- Việc sử dụng setter sẽ khiến cho trạng thái của đối tượng sau khi tạo ra biến đổi, khó kiểm soát (vì chúng ta có thể chủ động thay đổi giá trị).
Ý tưởng cải tiến
Ý tưởng đặt ra để khắc phục những nhược điểm nêu trên:
- Tách các xử lý phức tạo ra ngoài Constructor.
- Bàn giao công việc khởi tạo cho một đối tượng khác, chia việc khởi tạo các thuộc tính ra riêng rẽ sau đó sẽ lắp ghép lại để xây dựng nên đối tượng.
Đây chính là ý tưởng cơ bản của Builder Pattern.
Builder Pattern là gì?
Separate the construction of a complex object from its representation so that the same construction process can create different representations
- Là Pattern phục vụ cho việc khởi tạo các đối tượng (thuộc nhóm Creational)
- Tách rời quá trình tạo object với nội dung và cấu trúc bên trong của nó, nhờ vậy tương ứng với một quá trình tạo object có thể có nhiều cách tạo nhiều thể hiện khác nhau.
- Sử dụng khi quá trình khởi tạo object phức tạp.
- Sử dụng một đối tượng đóng vai trò là Builder để phục vụ cho việc khởi tạo đối tượng khác.
Áp dụng ý tưởng của Builder Pattern với ví dụ ban đầu, chúng ta sẽ triển khai khởi tạo một class UserBuilder làm nhiệm vụ khởi tạo đối tượng cho class User.
Mô hình Builder Pattern
Trước khi đi vào phân tích mô hình Builder Pattern, tôi xin đưa ra một bài toán để các bạn tiện hình dung. Chúng ta cần tạo ra một đối tượng cụ thể ví dụ là chiếc xe máy. Thành phần bao gồm: khung xe, lốp xe, động cơ, xích, phanh, ... việc tạo ra các thành phần không nhất thiết phải thực hiện đồng thời, cũng không nhất thiết phải có trình tự trước sau thế nào mà có thể được tạo ra độc lập bởi các cá nhân/tổ chức khác nhau.
Tuy nhiên để có một chiếc xe hoàn chỉnh chúng ta cần tập hợp lại các thành phần từ các nơi sản xuất riêng lẻ và lắp ráp lại thành một chiếc xe máy hoàn chỉnh. Việc làm này được thực hiện bởi một nhà máy sản xuất (VD: Honda Vietnam, Honda Laos, ...) và nhà máy sản xuất tôi đang nhắc đến đóng vai trò Builder.
Hãy cùng xem mô hình Builder Pattern dưới đây:
- Builder: định nghĩa một interface hoặc abstract class cho việc tạo ra các thành phần của đối tượng Product.
- ConcreteBuilder: lớp cài đặt chi tiết các thành phần được định nghĩa bởi Builder
- Xây dựng và lắp ráp các thành phần của Product bằng cách thực thi Builder.
- Định nghĩa và giữ liên kết các thành phần tạo ra.
- Đưa ra interface để lấy Product.
- Product: đại diện cho đối tượng đang được xây dựng.
- Director: xây dựng đối tượng Product sử dụng Builder interface.
Builder (Manager) Pattern trong Laravel
Trong Laravel, Builder Pattern cũng được hiểu như là Manager Pattern. Ví dụ class AuthManager cần tạo ra một số thành phần cần được bảo mật để tái sử dụng với những thành phần lưu trữ như cookie, session hay custom (gọi là driver). Để giải quyết vấn đề này, AuthManager class sử dụng các hàm để lưu trữ như callCustomCreator() và getDrivers() từ class Manager.
Chúng ta hãy cùng xem Builder (Manager) Pattern thể hiện như thế nào thông qua file vendor/Illuminate/Support/Manager.php:
public function driver($driver = null) { ... } protected function createDriver($driver) { $method = 'create'.ucfirst($driver).'Driver'; ... } protected function callCustomCreator($driver) { return $this->customCreators[$driver]($this->app); } public function extend($driver, Closure $callback) { $this->customCreators[$driver] = $callback; return $this; } public function getDrivers() { return $this->drivers; } public function __call($method, $parameters) { return call_user_func_array(array($this->driver(), $method), $parameters); }
và vendor/Illuminate/Auth/AuthManager.php:
protected function createDriver($driver) { .... } protected function callCustomCreator($driver) { ... } public function createDatabaseDriver() { ... } protected function createDatabaseProvider() { .... } public function createEloquentDriver() { ... } protected function createEloquentProvider() { ... } public function getDefaultDriver() { ... } public function setDefaultDriver($name) { ... }
Nhìn vào 2 file tóm tắt ở trên ta có thể thấy class AuthManager kế thừa từ class Manager. Như chúng ta đã biết Laravel cung cấp cơ chế basic auth, chúng ta lưu trữ thông tin xác thực trong Database. Đầu tiên, class AuthManager kiểm tra cấu hình lưu trữ thông tin xác thực có phải mặc định là database không thông qua phương thức getDefaultDriver(). Hàm này thực chất là sử dụng class Manager phục vụ cho các thao tác với Eloquent. Tất cả các tùy chọn cho Database và xác thực (auth) đều lấy được từ các file config (được đặt trong thư mục config).
Để hiểu hơn về Manager Pattern ta có thể xem qua sơ đồ ví dụ sau:
Client muốn đặt pizza: Asian pizza và/hoặc Chinese pizza. Chiếc pizza được yêu cầu từ class Waiter. class PizzaBuilder (trong trường hợp này có thể hiểu như class Manager) sẽ làm chiếc pizza theo đúng yêu cầu (trường hợp này là thông qua yêu cầu từ class AuthManager), sau đó sẽ chuyển chiếc pizza tới đúng nơi request thông qua Waiter.
Ngoài ra thì các bạn có thể đọc thêm code ở vendor/Illuminate/Session/SessionManager.php để tìm hiểu thêm về cách sử dụng Manager Pattern trong Laravel.