(URLHÌNHẢNH) Java ThiếtKếMẫu - ban ca ifish

Ngày 05 tháng 12 năm 2023 - Lập trình máy tính

Trong Java, vì không có khái niệm tham số đặt tên (named parameters), khi một lớp có quá nhiều tham số tùy chọn trong hàm tạo, mã nguồn sẽ trở nên kém dễ đọc. Do đó, mẫu xây dựng (Builder Pattern) đã ra đời để giải quyết vấn đề này.

Bài viết bắt đầu bằng một ví dụ thực tế nhằm dẫn dắt cách xử lý tình huống mà hàm tạo có quá nhiều tham số tùy chọn. Sau đó, phân tích những hạn chế của hai phương pháp truyền thống là mẫu hàm tạo thu nhỏ (Telescoping Constructor Pattern) và mẫu JavaBeans. Cuối cùng, bài viết giới thiệu về mẫu xây dựng, giải thích cách thiết kế và các ưu điểm vượt trội của nó.

Hãy xem xét một ví dụ: Giả sử chúng ta cần thiết kế hàm tạo cho một lớp cấu hình Redis (RedisConfig). Trong lớp này, chỉ có địa chỉ máy chủ (host) là trường bắt buộc. Các trường khác như cổng (port), số kết nối tối đa (maxTotal), số kết nối rảnh tối đa (maxIdle), thời gian chờ tối đa tính bằng miligây (maxWaitMillis) và kiểm tra trước khi mượn kết nối (testOnBorrow) đều là tùy chọn và có giá trị mặc định.

Làm thế nào để thiết kế hàm tạo cho lớp cấu hình Redis này?

1. Mẫu Hàm Tạo Thu Nhỏ

Đối với trường hợp có quá nhiều tham số tùy chọn, phương pháp truyền thống là sử dụng mẫu hàm tạo thu nhỏ. Cách tiếp cận này tạo ra một loạt các hàm tạo với số lượng tham số tăng dần từ thấp đến cao, bắt đầu từ hàm tạo chỉ chứa các tham số bắt buộc, sau đó lần lượt thêm từng tham số tùy chọn vào.

Dưới đây là đoạn mã thực hiện mẫu hàm tạo thu nhỏ cho RedisConfig:

 1import org.junit.jupiter.api.Test;
 2
 3public class TelescopingConstructorPatternTest {
 4    static class RedisConfig {
 5        private final String host; // Bắt buộc
 6        private Integer port = 6379; // Tùy chọn, mặc định là 6379
 7        private Integer maxTotal = 100; // Tùy chọn, mặc định là 100
 8        private Integer maxIdle = 10; // Tùy chọn, mặc định là 10
 9        private Integer maxWaitMillis = 60 * 1000 * 1000; // Tùy chọn, mặc định là 1 phút
10        private Boolean testOnBorrow = true; // Tùy chọn, mặc định là true
11        
12        public RedisConfig(String host) {
13            this.host = host;
14        }
15        
16        public RedisConfig(String host, Integer port) {
17            this(host);
18            this.port = port;
19        }
20        
21        public RedisConfig(String host, Integer port, Integer maxTotal) {
22            this(host, port);
23            this.maxTotal = maxTotal;
24        }
25        
26        public RedisConfig(String host, Integer port, Integer maxTotal, Integer maxIdle) {
27            this(host, port, maxTotal);
28            this.maxIdle = maxIdle;
29        }
30        
31        public RedisConfig(String host, Integer port, Integer maxTotal, Integer maxIdle, Integer maxWaitMillis) {
32            this(host, port, maxTotal, maxIdle);
33            this.maxWaitMillis = maxWaitMillis;
34        }
35        
36        public RedisConfig(String host, Integer port, Integer maxTotal, Integer maxIdle, Integer maxWaitMillis, Boolean testOnBorrow) {
37            this(host, port, maxTotal, maxIdle, maxWaitMillis);
38            this.testOnBorrow = testOnBorrow;
39        }
40    }
41
42    @Test
43    public void testConstruction() {
44        // Để thiết lập một trường cụ thể, bạn phải tìm hàm tạo ngắn nhất bao gồm trường đó
45        RedisConfig config = new RedisConfig("localhost", 6379, 100, 10, 60 * 1000 * 1000, false);
46    }
47}

Mặc dù mẫu hàm tạo thu nhỏ là một giải pháp khả thi, nhưng nó khá cồng kềnh. Ví dụ, nếu muốn đặt testOnBorrow thành false, bạn phải tìm hàm tạo ngắn nhất chứa tham số này, ngay cả khi các tham số trước đó đều sử dụng giá trị mặc định. Điều này khiến bạn phải nhập lại các giá trị mặc định một cách thủ công, gây mất thời gian và dễ mắc lỗi.

Ngoài ra, nếu hai tham số liên tiếp có cùng kiểu dữ liệu, rất dễ nhầm lẫn giữa chúng, dẫn đến các lỗi nghiêm trọng.

1// Đặt sai giá trị maxTotal và maxIdle có thể gây ra vấn đề
2RedisConfig config = new RedisConfig("localhost", 6379, 10, 100, 60 * 1000 * 1000, false);

2. Mẫu JavaBeans

Một phương pháp khác để giải quyết vấn đề quá nhiều tham số tùy chọn là sử dụng mẫu JavaBeans. Theo cách tiếp cận này, tất cả các trường được khởi tạo thông qua các phương thức setters sau khi gọi hàm tạo rỗng.

Dưới đây là đoạn mã sử dụng mẫu JavaBeans cho RedisConfig:

 1import org.junit.jupiter.api.Test;
 2
 3public class JavaBeansPatternTest {
 4    static class RedisConfig {
 5        private String host; // Bắt buộc
 6        private Integer port = 6379; // Tùy chọn, mặc định là 6379
 7        private Integer maxTotal = 100; // Tùy chọn, mặc định là 100
 8        private Integer maxIdle = 10; // Tùy chọn, mặc định là 10
 9        private Integer maxWaitMillis = 60 * 1000 * 1000; // Tùy chọn, mặc định là 1 phút
10        private Boolean testOnBorrow = true; // Tùy chọn, mặc định là true
11        
12        public RedisConfig() {}
13
14        public void setHost(String host) {
15            this.host = host;
16        }
17
18        public void setPort(Integer port) {
19            this.port = port;
20        }
21
22        public void setMaxTotal(Integer maxTotal) {
23            this.maxTotal = maxTotal;
24        }
25
26        public void setMaxIdle(Integer maxIdle) {
27            this.maxIdle = maxIdle;
28        }
29
30        public void setMaxWaitMillis(Integer maxWaitMillis) {
31            this.maxWaitMillis = maxWaitMillis;
32        }
33
34        public void setTestOnBorrow(Boolean testOnBorrow) {
35            this.testOnBorrow = testOnBorrow;
36        }
37    }
38
39    @Test
40    public void testConstruction() {
41        RedisConfig config = new RedisConfig();
42        config.setHost("localhost");
43        config.setPort(6380);
44        config.setMaxTotal(200);
45        config.setMaxIdle(20);
46        config.setMaxWaitMillis(120 * 1000 * 1000);
47        config.setTestOnBorrow(false);
48    }
49}

Mẫu JavaBeans giúp khắc phục nhược điểm của mẫu hàm tạo thu nhỏ: bạn không cần thiết lập các trường trước khi đặt giá trị cho một trường cụ thể. Tuy nhiên, nó cũng mang lại một số vấn đề mới: việc tạo đối tượng bị phân tán qua nhiều lần gọi phương thức, làm tăng khả năng xảy ra trạng thái không nhất quán của đối tượng.

3. Mẫu Xây Dựng

Giới thiệu mẫu xây dựng (Builder Pattern), đây là một giải pháp vừa đảm bảo an toàn như mẫu hàm tạo thu nhỏ, vừa giữ được tính dễ đọc như mẫu JavaBeans. Nó hoạt động bằng cách sử dụng một lớp trung gian Builder để tạo đối tượng đích. Khi tạo Builder, bạn phải cung cấp tất cả các tham số bắt buộc. Đối với các tham số tùy chọn, bạn có thể sử dụng cách tiếp cận tương tự như setters. Sau khi thiết lập xong các tham số, bạn gọi phương thức build không tham số của Builder để tạo ra đối tượng cuối cùng, bất biến.

Dưới đây là đoạn mã áp dụng mẫu xây dựng cho RedisConfig:

 1import org.junit.jupiter.api.Test;
 2
 3public class BuilderPatternTest {
 4    static class RedisConfig {
 5        private final String host;
 6        private final Integer port;
 7        private final Integer maxTotal;
 8        private final Integer maxIdle;
 9        private final Integer maxWaitMillis;
10        private final Boolean testOnBorrow;
11
12        static class Builder {
13            private String host; // Bắt buộc
14            private Integer port = 6379; // Tùy chọn, mặc định là 6379
15            private Integer maxTotal = 100; // Tùy chọn, mặc định là 100
16            private Integer maxIdle = 10; // Tùy chọn, mặc định là 10
17            private Integer maxWaitMillis = 60 * 1000 * 1000; // Tùy chọn, mặc định là 1 phút
18            private Boolean testOnBorrow = true; // Tùy chọn, mặc định là true
19
20            public Builder(String host) {
21                this.host = host; [các game quay  uy tín](/post/04.html) 
22            }
23
24            public Builder port(Integer port) {
25                this.port = port;
26                return this;
27            }
28
29            public Builder maxTotal(Integer maxTotal) { [Sam86.Vip Game Bài Nhiều Người Chơi Nhất](/post/14_1.html) 
30                this.maxTotal = maxTotal;
31                return this;
32            }
33
34            public Builder maxIdle(Integer maxIdle) {
35                this.maxIdle = maxIdle;
36                return this;
37            }
38
39            public Builder maxWaitMillis(Integer maxWaitMillis) {
40                this.maxWaitMillis = maxWaitMillis; [ngoai hang anh 2025 bao nhieu vong dau](/post/23_3.html) 
41                return this;
42            }
43
44            public Builder testOnBorrow(Boolean testOnBorrow) {
45                this.testOnBorrow = testOnBorrow;
46                return this;
47            }
48
49            public RedisConfig build() {
50                return new RedisConfig(this);
51            }
52        }
53
54        private RedisConfig(Builder builder) {
55            this.host = builder.host;
56            this.port = builder.port;
57            this.maxTotal = builder.maxTotal;
58            this.maxIdle = builder.maxIdle;
59            this.maxWaitMillis = builder.maxWaitMillis;
60            this.testOnBorrow = builder.testOnBorrow;
61        }
62    }
63
64    @Test
65    public void testConstruction() {
66        RedisConfig config = new RedisConfig.Builder("localhost")
67            .port(6380)
68            .maxTotal(200)
69            .maxIdle(20)
70            .maxWaitMillis(120 * 1000 * 1000)
71            .testOnBorrow(false)
72            .build();
73    }
74}

Tóm lại, bài viết đã khám phá cách xử lý tình huống mà hàm tạo có quá nhiều tham số tùy chọn. So sánh giữa mẫu hàm tạo thu nhỏ, mẫu JavaBeans và mẫu xây dựng, ta thấy rằng hai phương pháp đầu tiên gặp phải vấn đề về độ dễ đọc hoặc độ an toàn. Ngược lại, mẫu xây dựng mang lại cả hai ưu điểm này, phù hợp hơn để sử dụng trong lập trình hàng ngày.

Toàn bộ mã nguồn minh họa đã được đăng tải trên GitHub cá nhân của tôi, mời bạn theo dõi hoặc Fork.

[1] Tạo và Hủy Đối Tượng: Hãy cân nhắc sử dụng Builder khi đối mặt với nhiều tham số trong hàm tạo | Effective Java (Ấn bản thứ 3), bởi Joshua Bloch
[2] Khám Phá Mẫu Thiết Kế Builder của Joshua Bloch trong Java | Java Magazine - blogs.oracle.com

#Java #ThiếtKếMẫu