Hế lô anh em ✌️✌️✌️
Tiếp tục seri Design Patterns hôm nay mình sẽ cùng anh em tìm hiểu về một Creational Design Pattern nữa cũng rất thú vị đó là Builder Pattern.
Ngoài ra nếu anh em nào quan tâm thì có thể tham khảo các bài viết khác cùng chủ đề Design Patterns được mình tổng hợp tại đây nhé.!
Ồ kê, bây giờ thì cùng bắt đầu bài viết hôm nay nào! 💪✈️✈️
1. Đặt vấn đề
Hãy tưởng tượng trong cuộc sống có rất nhiều thứ mà khi anh em muốn tạo ra nó anh em có thể lắp ráp, xây dựng từ nhiều thành phần (bộ phận) khác nhau.
Một vài trong số những thành phần (bộ phận) đó là bắt buộc nhưng một vài thì lại không! Và câu hỏi anh em phải trả lời là làm sao để tạo ra được đối tượng anh em mong muốn chỉ với những gì anh em cần?
Đó cũng là tư tưởng của Builder Pattern và mình sẽ lấy một ví dụ hết sức thực tế chính là ngôi nhà của chúng ta.
Vậy thế nào là một ngôi nhà? Mình con nhà nghèo thì chỉ cần có nóc nhà tránh mưa nắng, có sân, có tường, có đủ phòng để ngủ, có cửa sổ cho thoáng mát (nói chung là những thành phần cơ bản nhất của ngôi nhà 😁) là xong nhưng mấy anh em "rich kid" lại đòi cả bể bơi, garage ôtô hay thậm chí là sân golf...
Nếu đưa ý tưởng này vào code và theo tư duy thuần túy của việc khởi tạo đối tượng là dùng constructor thì anh em cùng xem đoạn code bên dưới có đúng ý anh em không nhé.
public class House {
private int windows;
private int doors;
private int walls;
private String roof;
private String yard;
private String swimmingPool;
private String garage;
public House(int windows, int doors, int walls, String roof, String yard,
String swimmingPool, String garage) {
this.windows = windows;
this.doors = doors;
this.walls = walls;
this.roof = roof;
this.yard = yard;
this.swimmingPool = swimmingPool;
this.garage = garage;
}
}
Và đây là cách chúng ta tạo ra các đối tượng tương ứng.
/* House for poor people include 2 windows, 1 doors, 4 walls and a roof which is made from bamboo */
House poorKidsHouse = new House(2, 1, 4, "made from bamboo", null, null, null);
/* House for common people include 4 windows, 1 doors, 4 walls, a roof which is made from steel and a yard for 30 square meters*/
House commonKidsHouse = new House(4, 1, 4, "made from steel", "30 square meters", null, null);
/* House for rich people include 8 windows, 2 doors, 4 walls, roof which is made from steel,
yard with 300 square meter, swimming pool with 40 square meters and a garage with 50 square meters */
House richKidsHouse = new House(8, 2, 4, "made from steel", "300 square meters", "40 square meters", "50 square meters");
Okay chỉ là một ví dụ vui thôi nhưng mình tin chắc là nhiều anh em vẫn hay khởi tạo đối tượng theo cách như lày 😁. Hoàn toàn không có gì là sai cả, thậm chí đây là cách tiêu chuẩn cho việc khởi tạo một đối tượng.
👉👉👉 Nhưng anh em có cảm thấy việc khởi tạo đối tượng như vậy có khá nhiều nhược điểm không? Nếu đồng ý thì cho mình một like và tiếp tục sang phần 2 để phân tích kỹ hơn nhé. ✈️
2. Phân tích
Bây giờ hãy nói về những nhược điểm khi chúng ta khởi tạo đối tượng bằng cách sử dụng constructor như ở phần 1 mình vừa đề cập.
Không rõ ràng
Không rõ ràng nghĩa là sao? Ở đây anh em có thể thấy những tham số mình truyền vào phải đúng theo thứ tự được định nghĩa trong constructor. Nếu sai thứ tự sẽ dẫn đến việc khởi tạo đối tượng không đúng mong muốn hoặc không khởi tạo được.
Hai nữa, khi sử dụng constructor như này các bạn cũng không biết được tên tham số cần truyền là gì, hoàn toàn phụ thuộc vào thứ tự tham số đó.
Truyền các tham số không cần thiết
Anh em có thể thấy 3 đối tượng poorKidsHouse
, commonKidsHouse
, richKidsHouse
đều sử dụng chung một constructor để khởi tạo. Đối với richKidsHouse
chúng ta cần đủ tham số, nhưng với commonKidsHouse
và poorKidsHouse
đâu cần đủ nhưng vẫn phải truyền null
cho các tham số không cần thiết.
👉👉Tuy nhiên chúng ta có thể khắc phục nhược điểm này bằng cách tạo các constructor tương ứng như sau:
public class House {
private int windows;
private int doors;
private int walls;
private String roof;
private String yard;
private String swimmingPool;
private String garage;
public House(int windows, int doors, int walls, String roof, String yard,
String swimmingPool, String garage) {
this.windows = windows;
this.doors = doors;
this.walls = walls;
this.roof = roof;
this.yard = yard;
this.swimmingPool = swimmingPool;
this.garage = garage;
}
public House(int windows, int doors, int walls, String roof) {
this.windows = windows;
this.doors = doors;
this.walls = walls;
this.roof = roof;
}
public House(int windows, int doors, int walls, String roof, String yard) {
this.windows = windows;
this.doors = doors;
this.walls = walls;
this.roof = roof;
this.yard = yard;
}
}
Nhưng xét cho cùng cũng chỉ là giải pháp tạm thời vì nếu sinh ra thêm vài đối tượng nữa chúng ta lại phải tạo các constructor tương ứng và như thế sẽ không tối ưu.
Chưa kể nếu đối tượng có quá nhiều thuộc tính thì việc khởi tạo thông qua constructor thực sự là sẽ rất rất cồng kềnh và thậm chí khiến cho code không còn "trong sáng" nữa.
Không tạo được các Immutable Object
Thế nào là các đối tượng immutable và mutable? Để giải thích khái niệm mình đã có một bài viết riêng tại đây, ngắn thôi anh em có thể đọc để hiểu hơn nhé!
Quay lại với vấn đề của chúng ta, thay vì sử dụng constructor chúng ta có thể dụng các hàm setter()
để gán giá trị cho các biến như bên dưới.
House poorPeopleHouse = new House();
poorPeopleHouse.setWindows(2);
poorPeopleHouse.setDoors(1);
System.out.println("Walls: " + poorPeopleHouse.getWalls());
poorPeopleHouse.setWalls(4);
poorPeopleHouse.setRoof("made from bamboo");
System.out.println("Walls: " + poorPeopleHouse.getWalls());
Rõ ràng, đối tượng của chúng ta lúc này là mutable vì nó không nhất quán cho đến khi toàn bộ các thuộc tính được gán giá trị hoặc cài đặt rõ ràng.
Hay nói cách khác nếu sử dụng các hàm setter()
chúng ta sẽ giải quyết được vấn đề không rõ ràng và việc phải truyền các tham số không cần thiết nhưng lại không thể đạt được mong muốn về inmutable object.
Và để giải quyết toàn bộ những vấn đề trên chúng ta có thể áp dụng Builder Pattern. Cụ thể như thế nào anh em tiếp tục theo dõi sẽ rõ nhé! ✈️✈️
3. Builder Pattern
Bản chất các design patterns được sinh ra để giải quyết các vấn đề đã có cách giải quyết nhưng theo một cách khoa học, tối ưu và đơn giản hơn!
Trong trường hợp của chúng ta hôm nay chính là Builder Pattern và đây là định nghĩa được mình trích từ trang: https://refactoring.guru/
"Builder is a creational design pattern that lets you construct complex objects step by step. The pattern allows you to produce different types and representations of an object using the same construction code."
Hiểu đơn giản,
+ Build Pattern giúp chúng ta khởi tạo các đối tượng phức tạp từng bước một.
+ Builder Pattern cũng cho phép chúng ta tạo ra nhiều loại và thể hiện của một đối tượng bằng cách sử dụng chung một hàm khởi tạo.
👉👉 Anh em sẽ hiểu rõ hai ý này sau khi mình triển khai code ở phần 4 của bài viết.
Vậy cài đặt Builder Pattern như thế nào?
Quay lại với ví dụ ban đầu, mình có vẽ một diagram về cách triển khai Builder Pattern cho ví dụ này. Anh em có thể theo dõi hình bên dưới.
Việc cài đặt Builder Pattern không quá phức tạp, thay vì sử dụng trực tiếp constructor của class chúng ta đẩy việc đó sang cho một lớp gọi là "builder".
Lớp này (HouseBuilderImpl
) có trách nhiệm khởi tạo từng thuộc tính và cuối cùng sẽ trả về đối tượng mà chúng ta mong muốn (House
) với các thuộc tính phù hợp đã được khởi tạo.
Trong phần 5 mình sẽ trình bày với anh em cách cài đặt khác một chút còn bây giờ cùng mình code xem Builder Pattern lợi hại như nào nhé!
4. Cài đặt
Đầu tiên chúng ta định nghĩa đối tượng mà chúng ta muốn khởi tạo, trong ví dụ của mình chính là lớp House
như bên dưới.
public class House {
private int windows;
private int doors;
private int walls;
private String roof;
private String yard;
private String swimmingPool;
private String garage;
public House(int windows, int doors, int walls, String roof, String yard, String swimmingPool, String garage) {
this.windows = windows;
this.doors = doors;
this.walls = walls;
this.roof = roof;
this.yard = yard;
this.swimmingPool = swimmingPool;
this.garage = garage;
}
@Override
public String toString() {
return "windows=" + windows +
", doors=" + doors +
", walls=" + walls +
", roof='" + roof + '\'' +
", yard='" + yard + '\'' +
", swimmingPool='" + swimmingPool + '\'' +
", garage='" + garage + '\'';
}
}
Note: Ở đây, do là ví dụ đơn giản nên mình sử dụng các thuộc tính với kiểu dữ liệu nguyên thủy, trên thực tế đối tượng của chúng ta có thể bao gồm các thuộc tính là các đối tượng khác phức tạp hơn.
Ngoài ra, mình có ghi đè phương thức toString()
để in ra thông tin đối tượng, anh em không cần quan tâm lắm mà chỉ cần quan tâm khai báo đủ các thuộc tính và khởi tạo một constructor với đầy đủ tham số là được.
Tiếp theo mình định nghĩa một interface là HouseBuilder
. Interface này sẽ chứa các phương thức giúp chúng ta định nghĩa từng thuộc tính của đối tượng.
public interface HouseBuilder {
HouseBuilder windows(int windows);
HouseBuilder doors(int doors);
HouseBuilder walls();
HouseBuilder roof(String roof);
HouseBuilder yard(String yard);
HouseBuilder swimmingPool(String swimmingPool);
HouseBuilder garage(String garage);
House build();
}
Lớp HouseBuilderImpl
sẽ triển khai interface HouseBuilder
và thực thi logic cho các hàm đã được định nghĩa.
public class HouseBuilderImpl implements HouseBuilder {
private int windows;
private int doors;
private int walls;
private String roof;
private String yard;
private String swimmingPool;
private String garage;
@Override
public HouseBuilder windows(int windows) {
this.windows = windows;
return this;
}
@Override
public HouseBuilder doors(int doors) {
this.doors = doors;
return this;
}
@Override
public HouseBuilderImpl walls() {
this.walls = 4;
return this;
}
@Override
public HouseBuilder roof(String roof) {
this.roof = roof;
return this;
}
@Override
public HouseBuilder yard(String yard) {
this.yard = yard;
return this;
}
@Override
public HouseBuilder swimmingPool(String swimmingPool) {
this.swimmingPool = swimmingPool;
return this;
}
@Override
public HouseBuilder garage(String garage) {
this.garage = garage;
return this;
}
@Override
public House build() {
return new House(windows, doors, walls, roof, yard, swimmingPool, garage);
}
}
Cuối cùng hãy xem cách client tương tác để tạo ra các đối tượng tương ứng sẽ như thế nào.
public class App {
public static void main(String[] args) {
/* House for poor people include 2 windows, 1 doors, 4 walls and a roof which is made from bamboo */
House poorKidsHouse = new HouseBuilderImpl()
.windows(2)
.doors(1)
.walls()
.roof("made from bamboo")
.build();
System.out.printf("%s%s%n", "POOR KID'S HOUSE: ", poorKidsHouse.toString());
/* House for common people include 4 windows, 1 doors, 4 walls, a roof which is made from steel and a yard for 30 square meters*/
House commonKidsHouse = new HouseBuilderImpl()
.windows(4)
.doors(1)
.walls()
.roof("made from steel")
.yard("30 square meters")
.build();
System.out.printf("%s%s%n", "COMMON KID'S HOUSE: ", commonKidsHouse.toString());
/* House for rich people include 8 windows, 2 doors, 4 walls, roof which is made from steel,
yard with 300 square meter, swimming pool with 40 square meter and a garage with 50 square meter */
House richKidsHouse = new HouseBuilderImpl()
.windows(8)
.doors(4)
.walls()
.roof("made from steel")
.yard("300 square meters")
.swimmingPool("40 square meters")
.garage("50 square meters")
.build();
System.out.printf("%s%s%n", "RICH KID'S HOUSE: ", richKidsHouse.toString());
}
}
Kết quả sau khi chạy chương trình:
POOR KID'S HOUSE: windows=2, doors=1, walls=4, roof='made from bamboo', yard='null', swimmingPool='null', garage='null'
COMMON KID'S HOUSE: windows=4, doors=1, walls=4, roof='made from steel', yard='30 square meters', swimmingPool='null', garage='null'
RICH KID'S HOUSE: windows=8, doors=4, walls=4, roof='made from steel', yard='300 square meters', swimmingPool='40 square meters', garage='50 square meters'
Nhận xét:
+ Đầu tiên là về tính rõ ràng, anh em có thể thấy khi cần tạo ra đối tượng nào mình sẽ sử dụng các thuộc tính phù hợp bằng cách gọi đến tên các hàm khởi tạo giá trị cho thuộc tính đó bên trong lớp HouseBuilderImpl
+ Bằng cách này mình cũng không cần phải truyền các thuộc tính không cần thiết và gán giá trị null cho thuộc tính đó. Nếu thuộc tính đó không sử dụng mình chỉ đơn giản không gọi hàm khởi tạo trong lớp HouseBuilderImpl
là xong.
Đó, về cơ bản Builder Pattern đã giúp chúng ta giải quyết được hai trong số những vấn đề được đặt ra ở phần 2 của bài viết.
Vậy các vấn đề còn lại thì sao, cùng mình qua phần 5 để khai thác vấn đề sâu hơn nhé.
5. Mở rộng
Như mình đã đề cập, việc sử dụng constructor theo cách thông thường sẽ không giúp chúng ta đạt được mục đích về immutable object. Và với cách triển khai mình giới thiệu ở phần 4 cũng không làm được điều đó.
Vậy làm sao để áp dụng Builder Pattern và đạt được mục đích về immutable object?
Đơn giản thôi, cùng xem cách mình triển khai code bên dưới nhé.
public class House {
private int windows;
private int doors;
private int walls;
private String roof;
private String yard;
private String swimmingPool;
private String garage;
public static class Builder {
// required parameters
private final int windows;
private final int doors;
private final int walls;
private final String roof;
// optional parameters
private String yard;
private String swimmingPool;
private String garage;
public Builder(int windows, int doors, int walls, String roof) {
this.windows = windows;
this.doors = doors;
this.walls = walls;
this.roof = roof;
}
Builder yard(String val) {
yard = val;
return this;
}
Builder swimmingPool(String val) {
swimmingPool = val;
return this;
}
Builder garage(String val) {
garage = val;
return this;
}
public House build() {
return new House(this);
}
}
private House(Builder builder) {
windows = builder.windows;
doors = builder.doors;
walls = builder.walls;
roof = builder.roof;
yard = builder.yard;
swimmingPool = builder.swimmingPool;
garage = builder.garage;
}
@Override
public String toString() {
return "windows=" + windows +
", doors=" + doors +
", walls=" + walls +
", roof='" + roof + '\'' +
", yard='" + yard + '\'' +
", swimmingPool='" + swimmingPool + '\'' +
", garage='" + garage + '\'';
}
}
Phía client sẽ tương tác như sau:
House richKidsHouse = new House
.Builder(2, 1, 4, "made from steel")
.yard("300 square meters")
.swimmingPool("40 square meters")
.garage("50 square meters")
.build();
System.out.println(richKidsHouse);
Đầu tiên anh em để ý cho mình class House
chỉ có một hàm khởi tạo duy nhất nhưng lại ở chế độ private. Điều này giúp ngăn việc khởi tạo đối tượng từ bên ngoài class và để khởi tạo được đối tượng chúng ta chỉ có một cách đó là gián tiếp thông qua class Builder
.
Class Builder
là một static nested class có đầy đủ các thuộc tính của class House
và nó có nhiệm vụ khởi tạo một immutabe object (trong trường hợp này chính là đối tượng richKidsHouse).
Nói cách khác chúng ta chỉ có thể tạo ra một đối tưởng hoàn chỉnh sau khi gọi hàm build()
và trạng thái của đối tượng sẽ không bị thay đổi trong suốt quá trình khởi tạo đó.
6. Nhược điểm của Builder Pattern
+ Nhược điểm chung của việc áp dụng các design patterns chính là việc tăng số lượng file dẫn đến tăng kích thước mã nguồn (tất nhiên không phải lúc nào tăng số lượng file cũng dẫn đến tăng kích thước mã nguồn) nhưng thường sẽ là như vậy.
+ Ngoài ra như anh em thấy toàn bộ các thuộc tính đều phải được khai báo ở cả đối tượng chúng ta muốn khởi tạo và cả bên trong builder. Điều này dẫn đến việc trùng lặp code nhưng xét cho cùng thì đó cũng không phải là vấn đề quá lớn.
7. Lời kết & Tham khảo
Túm cái váy lại, không biết trong bài viết hôm nay anh em có thông não được về pattern này chưa nhưng mình hi vọng bài viết phần nào giúp anh em có thêm thông tin để tham khảo.
Đừng quên tìm hiểu các patterns khác được mình tổng hợp tại đây nhé!
Tham khảo:
https://refactoring.guru/design-patterns/builder
https://sourcemaking.com/design_patterns/builder
https://gpcoder.com/4434-huong-dan-java-design-pattern-builder/
Hẹn gặp lại anh em trong các bài viết tiếp theo nhé! Byeee 👋👋👋
Không có nhận xét nào: