🐴with Java
Java Singleton (Creational Design Patterns), Adapter Pattern (Structural Design Patterns), Chain of Responsibility Pattern (Behavioral Design Patterns)
Last updated
Java Singleton (Creational Design Patterns), Adapter Pattern (Structural Design Patterns), Chain of Responsibility Pattern (Behavioral Design Patterns)
Last updated
Thông số | Singleton Pattern | Prototype Pattern | Factory Pattern | Builder Pattern |
---|---|---|---|---|
Mục đích | Đảm bảo chỉ có một instance của lớp và cung cấp một điểm truy cập toàn cục đến nó. | Tạo ra các đối tượng mới bằng cách sao chép một instance của đối tượng đã tồn tại (prototype) thay vì tạo mới từ đầu. | Tạo một interface cho việc tạo đối tượng, nhưng để cho lớp con quyết định lớp nào sẽ được khởi tạo. | Tách rời việc xây dựng một đối tượng phức tạp từ phần đại diện của nó, để cùng một quy trình xây dựng có thể tạo ra các đối tượng khác nhau. |
Số lượng đối tượng | Chỉ có một instance duy nhất. | Số lượng đối tượng không giới hạn và dựa trên đối tượng prototype. | Số lượng đối tượng không giới hạn và tạo ra dựa trên yêu cầu. | Số lượng đối tượng không giới hạn và tạo ra dựa trên yêu cầu. |
Đồng bộ | Đồng bộ là một yếu tố quan trọng để đảm bảo chỉ có một instance. | Đồng bộ không cần thiết trừ khi đối tượng prototype chứa trạng thái chia sẻ. | Đồng bộ không cần thiết trừ khi có yêu cầu đặc biệt. | Đồng bộ không cần thiết trừ khi có yêu cầu đặc biệt. |
Thực thi | Thực thi trong quá trình tải hoặc khi được gọi lần đầu. | Thực thi khi cần tạo đối tượng mới từ đối tượng prototype. | Thực thi mỗi khi cần tạo đối tượng mới. | Thực thi mỗi khi cần tạo đối tượng mới với cấu trúc phức tạp. |
Hiệu suất | Hiệu suất tốt do chỉ cần tạo một instance. | Hiệu suất có thể kém hơn do cần sao chép đối tượng mỗi khi tạo mới. | Hiệu suất phụ thuộc vào quá trình tạo đối tượng mới. | Hiệu suất phụ thuộc vào quá trình xây dựng đối tượng. |
Mở rộng | Khó khăn trong việc mở rộng. | Dễ dàng mở rộng bằng cách tạo các prototype khác nhau. | Dễ dàng mở rộng bằng cách thêm các lớp con mới vào Factory. | Dễ dàng mở rộng bằng cách thêm các Builder mới để xây dựng các đối tượng khác nhau. |
Ứng dụng | Data Connection Pool, Ghi Log, Application Config Manager | Clone User mới đăng nhập để sử dụng cho các yêu cầu khác, Clone 1 Mail Object để Send cho nhiều người, Caching đối tượng | Trong một ứng dụng web, bạn có thể sử dụng Builder để xây dựng đối tượng User với các thuộc tính như tên, tuổi, địa chỉ email và số điện thoại. Bạn có thể sử dụng Builder để thiết lập các thuộc tính bắt buộc như tên và địa chỉ email, trong khi các thuộc tính tùy chọn như tuổi và số điện thoại có thể được thiết lập theo ý muốn. |
Template Pattern nói rằng “Định nghĩa một bộ khung của một thuật toán trong một chức năng, chuyển giao việc thực hiện nó cho các lớp con. Mẫu Template Method cho phép lớp con định nghĩa lại cách thực hiện của một thuật toán, mà không phải thay đổi cấu trúc thuật toán“.
Điều này có nghĩa là Template method giúp cho chúng ta tạo nên một bộ khung (template) cho một vấn đề đang cần giải quyết. Trong đó các đối tượng cụ thể sẽ có cùng các bước thực hiện, nhưng trong mỗi bước thực hiện đó có thể khác nhau. Điều này sẽ tạo nên một cách thức truy cập giống nhau nhưng có hành động và kết quả khác nhau.
Template Method Pattern được sử dụng khá nhiều trong mô hình Abstract – Concrete Class. Khi chúng ta muốn các Concrete class tự thực thi xử lí theo cách của nó, nhưng đồng thời vẫn đảm bảo tuận theo những ràng buộc nhất định từ Abstract class. Ví dụ như ràng buộc về thứ tự các bước thực hiện, hay ràng buộc về dữ liệu đầu vào, đầu ra, …
Trong Template method pattern, Abstract class định nghĩa ra một template method để thực hiện một chức năng nào đó. Template method này sẽ gọi đến các method khác bên trong Abstract class để tạo dựng nên bộ khung. Nhưng có thể các method đó sẽ không được thực thi bên trong Abstract class, mà sẽ được override và thực thi lại bên trong các Concrete class.
Các thành phần tham gia Template Method Pattern:
Abstract Class :
Định nghĩa các phương thức trừu tượng cho từng bước có thể được điều chỉnh bởi các lớp con.
Cài đặt một phương thức duy nhất điều khiển thuật toán và gọi các bước riêng lẻ đã được cài đặt ở các lớp con.
Concrete Class : là một thuật toán cụ thể, cài đặt các phương thức của AbstractClass. Các thuật toán này ghi đè lên các phươ ng thức trừu tượng để cung cấp các triển khai thực sự. Nó không thể ghi đè phương thức duy nhất đã được cài đặt ở AbstractClass (templateMethod).
Singleton đảm bảo chỉ duy nhất môt new instance được tạo ra và nó sẽ cung cấp cho bạn một method để truy cập đến thực thể đó.
private constructor để hạn chế truy cập từ class bên ngoài
đặt private static variable đảm bảo biến chỉ được khởi tạo trong class.
có một method public để return instance được khởi tạo ở trên.
recommend cách 2 cho single-thread và Bill Pugh Singleton Implementation cho multi-thread (đạt performance cao nhất).
Cách này khá đơn giản nhưng nó có thể được khởi tạo mà không bao giờ được dùng tới.
Tương tự eager nhưng cung cấp thêm static block để handle
Khắc phục được nhược điểm của 2 cách trên, chỉ khi nào geInstance được gọi thì instance mới được khởi tạo. Tuy nhiên, cách này chỉ sử dụng tốt trong trường hợp đơn luồng, trường hợp nếu có 2 luồng cùng chạy và cùng gọi hàm getInstance tại cùng một thời điểm thì đương nhiên chúng ta có ít nhất 2 thể hiện của instance.
Cách đơn giản nhất là chúng ta gọi phương thức synchronized của hàm getInstance() và như vậy hệ thống đảm bảo rằng tại cùng một thời điểm chỉ có thể có 1 luồng có thể truy cập vào hàm getInstance(), và đảm bảo rằng chỉ có duy nhất 1 thể hiện của class Tuy nhiên một menthod synchronized sẽ chạy rất chậm và tốn hiệu năng vì vậy chúng ta cần cải tiến nó đi 1 chút.
Thay vì chúng ta Thread Safe cả menthod getInstance() chúng ta chỉ Thread Safe một đoạn mã quan trọng.
5. Bill Pugh Singleton Implementation
Với cách làm này bạn sẽ tạo ra static nested class
với vai trò 1 Helper khi muốn tách biệt chức năng cho 1 class function rõ ràng hơn.
Mẫu thiết kế này đảm bảo sự an toàn khi sử dụng đa luồng bởi vì đối tượng Singleton được tạo vào thời điểm JVM tải lớp tĩnh (static class). Dưới đây là giải thích chi tiết:
private BillPughSingleton(){}
: Định nghĩa hàm tạo (contructor) là private để ngăn không cho các lớp khác trực tiếp tạo đối tượng BillPughSingleton từ bên ngoài.
private static class SingletonHelper
: Định nghĩa một lớp static nội (inner class) tên là SingletonHelper. Lớp này có nhiệm vụ tạo và sử dụng đối tượng BillPughSingleton.
private static final BillPughSingleton INSTANCE = new BillPughSingleton();
: Trong lớp SingletonHelper, tạo một đối tượng BillPughSingleton duy nhất là INSTANCE với từ khóa final. Điều này đảm bảo đối tượng được tạo ngay khi SingletonHelper được nạp vào bộ nhớ và khởi tạo là an toàn đa luồng (Thread Safe).
public static BillPughSingleton getInstance()
: Phương thức này cung cấp cho các lớp khác cách truy cập vào đối tượng Singleton. Nó trả về biến INSTANCE bên trong lớp SingletonHelper.
Cách triển khai này cho phép thực hiện Singleton Pattern với tính năng Khởi tạo lười biếng (Lazy Initialization) và Thread Safe. Do đó, nó phù hợp và hiệu quả khi sử dụng trong môi trường đa luồng.
Singleton Pattern đảm bảo rằng chỉ có một phiên bản của một đối tượng được tạo trong toàn bộ ứng dụng. Tuy nhiên, việc sử dụng Reflection (phản ánh) có thể phá vỡ nguyên tắc này bằng cách tạo ra thêm đối tượng Singleton mới không mong muốn. Đây là cách thức phá vỡ Singleton Pattern thông qua Reflection:
7. Enum Singleton
Cách tiếp cận này được coi là một trong những phương pháp đơn giản và hiệu quả nhất để đảm bảo tính Thread Safe và không cần phải sử dụng synchronization. Ví dụ:
public enum EnumSingleton
: Định nghĩa một kiểu liệt kê (enum) tên là EnumSingleton. Mỗi kiểu liệt kê đều được coi là một lớp tĩnh và bản thân chúng tự động là Singleton.
INSTANCE;
: Tạo một thuộc tính enum duy nhất tên là INSTANCE. Đây là đối tượng Singleton duy nhất của cấu trúc này.
public static void doSomething()
: Đây là một phương thức tĩnh (static method) để thực hiện các thao tác mong muốn. Nếu bạn muốn sử dụng các biến hoặc phương thức phi tĩnh, bạn có thể thêm chúng vào cấu trúc enum và sử dụng thông qua INSTANCE.
Cách triển khai này đảm bảo rằng chỉ có một đối tượng Singleton được tạo, và tính Thread Safe luôn được đảm bảo. Cách thức này được đề nghị bởi Joshua Bloch trong cuốn sách "Effective Java" và được coi là cách triển khai Singleton Pattern tốt nhất đến thời điểm hiện tại.
Serialization là một kỹ thuật sắp xếp đối tượng cần lưu trữu một cách tuần tự. Dưới đây là quá trình đọc ghi dữ liệu khi tích hợp singleton.
Singleton Pattern rất thích hợp để tạo ra các lớp quản lý tài nguyên như quản lý kết nối cơ sở dữ liệu (Database Connection Pool).
Đối với một ứng dụng, việc tạo và đóng kết nối cơ sở dữ liệu là một tác vụ tốn kém về mặt thời gian và tài nguyên. Vì vậy, nó không hiệu quả nếu chúng ta tạo một kết nối mới mỗi khi cần truy vấn dữ liệu. Thay vào đó, chúng ta có thể sử dụng một pool
kết nối sẵn có, và mỗi khi cần, chúng ta chỉ cần lấy một kết nối từ pool này.
Dưới đây là một cách triển khai cơ bản của Database Connection Pool sử dụng Singleton Pattern:
Trong ví dụ trên, ConnectionPool
là một lớp Singleton. Khi được khởi tạo, nó sẽ tạo một pool với một số lượng kết nối tối đa. Mỗi khi một phần của ứng dụng cần truy cập cơ sở dữ liệu, nó sẽ gọi ConnectionPool.getInstance().getConnection()
để lấy một kết nối từ pool. Khi hoàn thành, nó sẽ gọi ConnectionPool.getInstance().releaseConnection(conn)
để trả kết nối về pool.
Lưu ý rằng đây chỉ là một ví dụ đơn giản và có thể cần phải tinh chỉnh để đáp ứng nhu cầu của ứng dụng thực tế. Ví dụ, chúng ta có thể muốn tạo thêm kết nối nếu tất cả các kết nối hiện có đều đang bận, hoặc chúng ta có thể muốn thêm cơ chế timeout để không giữ kết nối quá lâu nếu không sử dụng.
Ngoài ra, việc quản lý kết nối cơ sở dữ liệu cũng cần phải cẩn trọng về việc đảm bảo an toàn thread. Trong ví dụ trên, chúng ta đã sử dụng từ khóa synchronized
để đảm bảo rằng chỉ có một thread có thể lấy hoặc trả kết nối tại một thời điểm. Tuy nhiên, trong một ứng dụng thực tế với nhiều thread, việc sử dụng synchronized
có thể dẫn đến hiệu suất giảm sút. Chúng ta có thể cần tìm cách tối ưu hóa việc đồng bộ hóa này, ví dụ, bằng cách sử dụng java.util.concurrent.locks.ReentrantLock
hoặc một cấu trúc dữ liệu thread-safe khác.
Cuối cùng, hãy lưu ý rằng nhiều thư viện và framework đã cung cấp sẵn các cơ chế quản lý kết nối cơ sở dữ liệu, nên trong hầu hết các trường hợp, bạn không cần phải tự triển khai từ đầu. Tuy nhiên, hiểu cách hoạt động của một connection pool và cách triển khai nó sử dụng Singleton Pattern vẫn là một kỹ năng hữu ích.
Singleton Pattern cũng thường được sử dụng để triển khai các lớp Logger. Logger là một thành phần rất quan trọng trong hầu hết các ứng dụng, giúp ghi lại các thông tin về hoạt động của hệ thống như các thông báo lỗi, thông tin về quá trình thực thi, các sự kiện quan trọng, v.v.
Thành phần Logger thường được thiết kế theo mô hình Singleton, vì nó chỉ cần một instance duy nhất trong toàn bộ ứng dụng, đồng thời cũng đảm bảo hiệu suất và tài nguyên sử dụng tối ưu.
Dưới đây là một cách triển khai cơ bản của một Logger sử dụng Singleton Pattern:
Trong ví dụ trên, lớp Logger
chỉ có một instance, được khởi tạo khi gọi Logger.getInstance()
. Phương thức log(String message)
sẽ ghi tin nhắn vào một tệp log, kèm theo thời gian hiện tại. Với thiết kế này, mọi phần của ứng dụng đều có thể ghi log mà không cần quan tâm đến việc mở và đóng tệp, cũng như quản lý các tài nguyên liên quan.
Lưu ý rằng trong một ứng dụng thực tế, Logger thường cần phải hỗ trợ nhiều tính năng phức tạp hơn, như ghi log ở nhiều mức độ (debug, info, error, v.v.), ghi log vào nhiều đích khác nhau (tệp, console, v.v.), định dạng log, và cả việc đồng bộ hóa khi ghi log từ nhiều thread. Tuy nhiên, Singleton Pattern vẫn là nền tảng cho thiết kế của Logger.
Singleton còn có thể được sử dụng để quản lý cấu hình toàn cục của ứng dụng. Hãy tưởng tượng bạn có một tệp cấu hình chứa nhiều thông số khác nhau như chuỗi kết nối cơ sở dữ liệu, API keys, các thông số môi trường, v.v. Việc đọc tệp cấu hình này và giữ nó trong bộ nhớ có thể là một tác vụ tốn kém và bạn không muốn thực hiện điều đó nhiều lần trong quá trình chạy ứng dụng. Thay vào đó, bạn có thể đọc tệp cấu hình một lần, lưu trữ các giá trị trong một đối tượng Singleton và sử dụng đối tượng đó ở bất kỳ đâu trong ứng dụng.
Dưới đây là một ví dụ về cách bạn có thể triển khai một lớp AppConfig
sử dụng Singleton Pattern:
Trong đoạn mã trên, AppConfig
đọc tệp cấu hình app.config
khi được khởi tạo và lưu trữ tất cả các giá trị trong một đối tượng Properties
. Phương thức getProperty(String key)
sau đó có thể được sử dụng để lấy các giá trị cấu hình từ bất kỳ nơi nào trong ứng dụng.
Như vậy, thông qua việc sử dụng Singleton Pattern, chúng ta đã tạo ra một cấu trúc giúp quản lý cấu hình ứng dụng một cách hiệu quả và tiện lợi.
Adapter pattern chuyển đổi interface của một class thành interface mà client yêu cầu.
Adapter ở giữa gắn kết các lớp làm việc với nhau dù cho có những interface không tương thích với nhau.
Ví dụ: Tạo 2 class Volt là Socket
Tiếp theo tạo ra interface Adapter với mong muốn đầu ra được lazy hơn.
Chúng ta sẽ có 2 cách để triển khai Adapter pattern:
Chain of Responsibility cho phép nhiều đối tượng cơ hội xử lý yêu cầu. Yêu cầu được truyền dọc theo chuỗi các đối tượng tiếp nhận, và khi nào tìm được đối tượng phù hợp sẽ xử lý yêu cầu đó. Mỗi đối tượng tiếp nhận liên kết với nhau qua tham chiếu, và dùng message để truyền yêu cầu nếu không tự xử lý được.
Ví dụ các ATM sử dụng Chain of Responsibility để xử lý cho cơ chế rút tiền.
Khi người dùng rút 2 triệu từ ATM, máy trả về 3 tờ 500k, 4 tờ 100k và 2 tờ 50k. Có thể do cách phân bổ tờ tiền từ ATM - nó không trả về toàn bộ tờ 500k để đảm bảo sự phân bố hợp lý giữa các mệnh giá.
Nếu tất cả người rút tiền đầu tiên (với số tiền > 500k) đều nhận tờ 500k, ATM sẽ sớm hết tờ tiền này. Khi đó, rút 5 triệu mà chỉ nhận được tờ tiền 10k sẽ gây khó khăn cho việc kiểm tra tổng số tiền của người rút.
Việc phân bổ các mệnh giá khác nhau giúp ATM duy trì đủ các tờ tiền để phục vụ nhiều khách hàng khác nhau và hạn chế trường hợp hết tờ tiền cụ thể nào đó quá sớm.
Nó xác định các interface để xử lý yêu cầu. Có nghĩa là phân công nhiệm vụ xử lý cụ thể cho từng đối tượng tiếp nhận yêu cầu.
Thực hiện giao diện của "Handler". Xử lý yêu cầu hoặc nếu nó không xử lý được yêu cầu thì gửi yêu cầu đến đối tượng xử lý tiếp theo .
Áp dụng "Chain Of Responsibility", tạo ra các yêu cầu và yêu cầu đó sẽ được gửi đến các đối tượng tiếp nhận.
Quay trở lại với ví dụ về ATM ta có thể tổ chức code như sau:
B1: Tạo model Currency
là số tiền mỗi lần thanh toán được dùng bởi chuỗi implement interface DispenseChain
B2: Tạo các processor cho từng loại tiền 50K, 20K, 10K.
B3: Vận hành máy ATM này chúng ta cần lưu ý: các processor xử lý request lần lượt là: Dispenser 50K
>> Dispenser 20K
>> Dispenser 10K