Builder Pattern là gì và cách sử dụng trong PHP?

Builder Pattern là gì

Trong bài viết trước, chúng ta đã cùng nhau tìm hiểu về Abstract Factory Pattern. Trong bài viết này, chúng ta sẽ tiếp tục tìm hiểu một design pattern khác cũng thuộc nhóm khởi tạo (creational patterns) mang tên “Builder Pattern”.

Builder Pattern là gì thế?

Builder Pattern là một design pattern thuộc nhóm khởi tạo (creational patterns). Nó được được xây dựng nhằm khắc phục một số nhược điểm còn tồn đọng của Factory PatternAbstract Factory Pattern.

Khi đối tượng có nhiều thuộc tính, sẽ có một số vấn đề chính với Factory Pattern và Abstract Factory Pattern:

  • quá nhiều tham số phải truyền vào từ phía client tới Factory class.
  • Một số tham số có thể là tùy chọn nhưng trong Factory Pattern, chúng ta phải gửi tất cả tham số, với tham số tùy chọn nếu ko nhập gì thì sẽ truyền là null.

Chúng ta có thể giải quyết các vấn đề nêu trên bằng cách cung cấp một hàm khởi tạo với những tham số bắt buộc và các phương thức getter/ setter để cài đặt các tham số tùy chọn. Tuy nhiên, hướng tiếp cận này sẽ khiến cho trạng thái của đối tượng trở nên không nhất quán cho tới khi tất cả các thuộc tính của nó được cài đặt một cách rõ ràng.

Xuất phát từ lý do đó, builder pattern đã được ra đời. Nó giải quyết vấn đề trên này bằng việc cung cấp một cách xây dựng đối tượng từng bước một và cung cấp một phương thức để trả về đối tượng cuối cùng.

Cấu trúc

Builder Pattern bao gồm năm thành phần cơ bản là: Builder, Concrete Builder, Product, DirectorClient.

Cấu trúc của Builder Pattern

Trong đó:

  • Builder: Là thành phần định nghĩa một lớp trừu tượng (abstract class) để tạo ra một hoặc nhiều phần của đối tượng Product.
  • Concrete Builder: Là thành phần triển khai, cụ thể hóa các lớp trừu tượng cho để tạo ra các thành phần và tập hợp các thành phần đó với nhau. thành phần này sẽ xác định và nắm giữ các thể hiện mà nó tạo ra. Đồng thời nó cũng cung cấp phương thức để trả các các thể hiện mà nó đã tạo ra trước đó.
  • Product: Là thành phần sẽ đại diện cho các đối tượng phức tạp phải tạo ra.
  • Director: Là thành phần sẽ khởi tạo đối tượng Builder (Có thể có hoặc không).
  • Client: Là thành phần sẽ sử dụng các ConcreteBuilder và Director.

Nên sử dụng Builder Pattern khi nào?

Dưới đây là một số trường hợp bạn nên cân nhắc sử dụng builder pattern cho code của mình:

  • Các class của bạn chứa quá nhiều hàm khởi tạo hoặc những hàm khởi tạo quá cồng kềnh và phức tạp.
  • Bạn không muốn việc gán giá trị cho các tham số của hàm khởi tạo phải tuân theo một trật tự cố định nào đó, ví dụ: Thay vì phải gán giá trị tuần tự từ tham số A rồi mới đến tham số B và tham số C, bạn có thể gán giá trị cho tham số B trước rồi mới đến A và C.

Vấn đề đặt ra

Giả sử bạn là thành viên trong đội ngũ kỹ sư phát triển phần mềm cho ngân hàng XYZ. Vì là hệ thống của ngân hàng nên tài khoản ngân hàng (bank account) là một khía cạnh mà bạn không thể không thao tác đến. Dưới đây là cách bạn xây dựng class BankAccount cho hệ thống phần mềm của ngân hàng này vào những ngày đầu:

public class BankAccount 
{
   private $accountNumber;
   private $owner;
   private $balance;

   public function __construct($accountNumber, $owner, $balance) 
  {
       $this->accountNumber = $accountNumber;
       $this->owner = $owner;
       $this->balance = $balance;
   }
}

Không quá khó để hiểu được đoạn code trên đúng không nào? Dựa vào đoạn code trên, để tạo ra một tài khoản mới, tất cả những gì bạn cần làm chỉ đơn giản là:

$account = new BankAccount(123, "Bart", 100);

Tuy nhiên, không lâu sau đó bạn nhận được một yêu cầu mới từ sếp đề nghị bổ sung thêm mức lãi suất hàng tháng cho từng tài khoản, ngày mở tài khoản và chi nhánh mở tài khoản (tùy chọn). Sau khi đọc xong yêu cầu mới này của sếp, bạn bĩu môi và thầm nghĩ trong đầu “Tưởng chuyện gì to tát, dăm ba cái vặt vãnh này sửa tí là xong á mà”. Nói rồi bạn bắt tay ngay vào việc nâng cấp class BankAccount trước đó. Dưới đây là class BankAccount sau khi được nâng cấp lần 2:

public class BankAccount 
{
    private $accountNumber;
    private $owner;
    private $branch;
    private $balance;
    private $interestRate;

    public function __construct($accountNumber, $owner, $branch, $balance, $interestRate) 
    {
        $this->accountNumber = $accountNumber;
        $this->owner = $owner;

        $this->branch = $branch;
        $this->balance = $balance;
        $this->interestRate = $interestRate;
    }
}

Sau đợt nâng cấp lần 2, để tạo ra một tài khoản mới cũng không có gì là quá khó khăn.

BankAccount account = new BankAccount(456, "Marge", "Springfield", 100, 2.5);
BankAccount anotherAccount = new BankAccount(789, "Homer", null, 2.5, 100);

Trình biên dịch vẫn tiến hành biên dịch và thực thi đoạn code trên một cách bình thường. Tuy nhiên bạn có nhận ra điểm bất thường nào trong đoạn code ở trên không? Nếu để ý kỹ một chút bạn sẽ dễ dàng nhận thấy rằng giá trị balance và interestRate của tài khoản của khách hàng có tên là Homer đã bị gán nhầm giá trị cho nhau. Nhìn vào đoạn code trên, chúng ta dễ dàng nhận ra rằng số tiền của Homer sẽ tăng gấp đôi sau mỗi tháng với lãi suất 100% (balance = 2.5; interestRate = 100). Nếu bạn có biết ngân hàng nào áp dụng mức lãi suất 100% thì nhớ chỉ mình với nhé.

Vấn đề nêu trên cho thấy rằng nếu hàm khởi tạo có nhiều tham số liên tiếp cùng kiểu dữ liệu, việc gán nhầm giá trị của tham số này thành giá trị của tham số khác là điều khó có thể tránh khỏi. Vì trình biên dịch không hề nhận ra nó là một lỗi, nên đối với các chương trình phức tạp việc phát hiện và sửa lỗi là khá khó khăn.

Ngoài ra, việc nhồi nhét quá nhiều tham số cho hàm khởi tạo sẽ khiến cho code của chúng ta trở nên khó đọc và khó theo dõi hơn. Với số lượng là 10, 20 hay 30 tham số, sẽ là rất khó để bạn có thể nắm bắt được hết tất cả các tham số của hàm khởi tạo trong nháy mắt.

Tệ hơn nữa, một số tham số có thể là tùy chọn, điều đó đồng nghĩa với việc chúng ta sẽ phải tạo ra một loạt các hàm khởi tạo hoặc sẽ phải gán giá trị null cho các tham số không cần thiết.

Nhiều bạn đề xuất rằng chúng ta có thể giải quyết vấn đề trên bằng cách gọi một hàm khởi tạo không có bất kì một tham số nào và sau đó thiết lập giá trị của các thuộc tính thông qua các phương thức setter thay thế. Tuy nhiên, chính điều này lại mang đến cho chúng ta một vấn đề khác – điều gì sẽ xảy ra nếu một lập trình viên nào đó quên gọi phương thức setter của một thuộc tính cụ thể nào đó và thuộc tính này lại là một thuộc tính bắt buộc của đối tượng? Kết quả là chúng ta có thể tạo ra một đối tượng mới chỉ được khởi tạo một phần và tiếp tục một lần nữa, trình biên dịch sẽ không phát hiện ra bất kỳ vấn đề nào với nó.

Đó cũng chính là lý do mà Builder Pattern đã ra đời.

Giải pháp

Builder pattern là mẫu thiết kế thuộc nhóm khởi tạo (Creational patterns) được tạo ra nhằm giải quyết vấn đề khởi tạo các đối tượng phức tạp bằng việc cung cấp một cách xây dựng đối tượng theo từng bước một và cung cấp một phương thức để trả về đối tượng cuối cùng.

Với vấn đề đã đặt ra, chúng ta sẽ tiến hành xây dựng class Builder chứa tất cả các thuộc tính của class BankAccount và chúng ta sẽ chỉ sử dụng class Builder này để khởi tạo một đối tượng mới thay vì sử dụng hàm khởi tạo của class BankAccount như trước.

Áp dụng Builder Pattern để giải quyết vấn đề đã đặt ra, dưới đây là tất cả những gì mà chúng ta sẽ thu được:

public interface Builder
{
     public function setAccountNumber($accountNumber);
     public function setOwner($owner);
     public function setBranch($branch);
     public function setBalance($balance);
     public function setInterestRate($interestRate);
     public function build();
}

public class BankAccountBuilder implements Builder
{
     private $accountNumber;
     private $owner;
     private $branch;
     private $balance;
     private $interestRate;

     public function setAccountNumber($accountNumber)
     {
          $this->accountNumber = $accountNumber;
          return $this;
     }

     public function setOwner($owner)
     {
          $this->owner = $owner;
          return $this;
     } 

     public function setBranch($branch)
     {
          $this->branch = $branch;
          return $this;
     }

     public function setBalance($balance)
     {
          $this->balance = $balance;
          return $this;
     }

     public function setInterestRate($interestRate)
     {
          $this->interestRate = $interestRate;
          return $this;
     }

     public function build()
     {
          return new BankAccount($this->accountNumber, $this->owner, $this->branch, $this->balance, $this->interestRate);
     }
}

public class BankAccount
{
     private $accountNumber;
     private $owner;
     private $branch;
     private $balance;
     private $interestRate;

     public function __construct($accountNumber, $owner, $branch, $balance, $interestRate)
     {
          $this->accountNumber = $accountNumber;
          $this->owner = $owner;
          $this->branch = $branch;
          $this->balance = $balance;
          $this->interestRate = $interestRate;
     }

     public function getAccountNumber()
     {
          return $this->accountNumber;
     }

     public function getOwner()
     {
          return $this->owner;
     }

     public function getBranch()
     {    
          return $this->branch;
     }

     public function getBalance()
     {
          return $this->balance;
     }

     public function getInterestRate()
     {
          return $this->interestRate;
     }

     public function showInfo()
     {
          echo "XYZ Bank – Account Information";
          echo "\n";
          if (!empty( (string) $this->getAccountNumber() ))
               echo "* Number: " . $this->getAccountNumber();
          if (!empty( (string) $this->getOwner() ))
               echo "* Owner: " . $this->getOwner();
          if (!empty( (string) $this->getBranch() ))
               echo "* Branch: " . $this->getBranch();
          if (!empty( (string) $this->getBalance() ))
               echo "* Balance: " . $this->getBalance();
          if (!empty( (string) $this->getInterestRate() ))
               echo "* Interest Rate: " . 
$this->getInterestRate();
     }
}

$bankAccount = new BankAccountBuilder().setOwner("Homer").setBalance(100.00).setInterestRate(2.5).build();
$bankAccount.showInfo();

Sau khi thực thi đoạn code trên, dưới đây là kết quả mà chúng ta thu được:

XYZ Bank – Account Information; 
* Number: 789 
* Owner: Homer 
* Balance: 100.00 
* Interest Rate: 2.5

Bằng việc sử dụng Builder Pattern, chúng ta thấy rằng việc khởi tạo một đối tượng mới đã trở nên dễ dàng và trực quan hơn rất nhiều. Giờ đây thay vì phải vật lộn với một hàm khởi tạo cồng kềnh với hàng tá các tham số, chúng ta chỉ cần truyền giá trị cho các setter với tên gọi cụ thể và cần thiết mà thôi.

Ưu và nhược điểm của Builder Pattern

Ưu điểm

  • Builder pattern giúp người dùng tránh được việc phải viết nhiều hàm khởi tạo cho class.
  • Với builder pattern, giờ đây người dùng không cần phải truyền giá trị null cho các tham số mà đối tượng của họ không cần sử dụng tới.
  • Builder pattern giúp người dùng biết được chính xác giá trị mà họ đang gán là gì khi gọi tới phương thức setter tương ứng. Chính điều này giúp hạn chế các lỗi phát sinh do việc gán sai hoặc gán nhầm tham số như trước đây.
  • Builder pattern giúp kiểm soát tốt hơn quá trình xây dựng của đối tượng: chúng ta có thể thêm các đoạn code xử lý kiểm tra ràng buộc trước khi đối tượng được trả về cho phía người dùng.
  • Builder pattern giúp code của chúng ta trở nên dễ đọc và dễ bảo trì hơn trong tương lai.

Nhược điểm

  • Code có thể trở nên nhiều hơn và phức tạp hơn do đòi hỏi phải sử dụng nhiều class mới có thể cài đặt được pattern này.

Kết luận

Trong bài viết này, chúng ta đã cùng nhau tìm hiểu về Builder Pattern, hy vọng pattern này sẽ giúp ích cho các bạn trong tương lai. Trong bài viết tiếp theo của series, mình sẽ tiếp tục giới thiệu cho các bạn một design pattern mới thuộc nhóm khởi tạo mang tên “Prototype”. Các bạn nhớ đón xem nhé!

Gửi phản hồi