4. Sử dụng Singleton trong thực tế
4.1. Quản lý kết nối cơ sở dữ liệu
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:
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List; public class ConnectionPool { private static ConnectionPool instance; private List<Connection> pool; private static final int MAX_CONNECTIONS = 10; private ConnectionPool() { pool = new ArrayList<>(); for(int i = 0; i < MAX_CONNECTIONS; i++) { try { Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/mydatabase", "user", "password"); pool.add(conn); } catch (SQLException e) { e.printStackTrace(); } } } public static synchronized ConnectionPool getInstance() { if(instance == null) { instance = new ConnectionPool(); } return instance; } public synchronized Connection getConnection() { if(pool.isEmpty()) { return null; // All connections are busy } Connection conn = pool.remove(pool.size() - 1); return conn; } public synchronized void releaseConnection(Connection conn) { pool.add(conn); }
}
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.
4.2. Ghi Log
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:
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.time.LocalDateTime; public class Logger { private static Logger instance; private PrintWriter writer; private Logger() { try { FileWriter fileWriter = new FileWriter("log.txt", true); writer = new PrintWriter(fileWriter, true); } catch (IOException e) { e.printStackTrace(); } } public static synchronized Logger getInstance() { if(instance == null) { instance = new Logger(); } return instance; } public void log(String message) { writer.println(LocalDateTime.now() + ": " + message); }
}
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.
4.3. Quản lý cấu hình ứng dụng
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:
import java.io.FileInputStream;
import java.io.IOException;
import java.util.Properties; public class AppConfig { private static AppConfig instance; private Properties properties; private AppConfig() { properties = new Properties(); try { FileInputStream fileInputStream = new FileInputStream("app.config"); properties.load(fileInputStream); } catch (IOException e) { e.printStackTrace(); } } public static synchronized AppConfig getInstance() { if(instance == null) { instance = new AppConfig(); } return instance; } public String getProperty(String key) { return properties.getProperty(key); }
}
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.
5. Ưu và nhược điểm của Singleton Pattern
5.1. Ưu điểm
- Kiểm soát truy cập đến tài nguyên duy nhất: Singleton Pattern cung cấp một cách để kiểm soát quyền truy cập tới một đối tượng duy nhất, do đó giúp đảm bảo rằng không có hai phần của mã cùng lúc thao tác trên đối tượng đó. Điều này đặc biệt hữu ích trong việc quản lý các tài nguyên chia sẻ, như kết nối cơ sở dữ liệu hoặc tệp log.
- Tiết kiệm tài nguyên: Khi sử dụng Singleton Pattern, chỉ có một instance của lớp được tạo ra, giúp giảm việc sử dụng tài nguyên không cần thiết. Điều này có thể tiết kiệm bộ nhớ và tài nguyên CPU, đặc biệt quan trọng trong các ứng dụng lớn và phức tạp.
- Toàn cục dễ truy cập: Singleton cung cấp một điểm truy cập toàn cục, giúp tạo sự tiện lợi khi muốn truy cập tới đối tượng từ bất kỳ nơi nào trong ứng dụng.
- Dễ dàng thay đổi lớp: Nếu bạn muốn thay đổi lớp của đối tượng Singleton, bạn chỉ cần thay đổi ở một nơi - phương thức
getInstance()
. Điều này giúp giảm thiểu lỗi và làm cho mã dễ dàng bảo dưỡng hơn. - Thực thi Lazy Initialization: Singleton Pattern cho phép chúng ta thực hiện lazy initialization, tức là tạo instance khi nó thực sự cần thiết. Điều này có thể cải thiện hiệu suất của ứng dụng, đặc biệt khi việc khởi tạo instance là quá trình tốn kém.
Tuy nhiên, cũng cần lưu ý rằng mặc dù Singleton có nhiều ưu điểm như đã nêu trên, nó cũng có một số nhược điểm và cần được sử dụng cẩn thận để không gây ra các vấn đề.
5.2. Nhược điểm
- Vi phạm nguyên tắc Single Responsibility: Singleton Pattern thường vi phạm nguyên tắc Single Responsibility trong SOLID, bởi lớp Singleton cung cấp chức năng truy cập toàn cục và kiểm soát số lượng instance của nó, đồng thời còn thực hiện chức năng chính của nó.
- Khó khăn trong việc kiểm tra và bảo dưỡng: Vì Singleton cung cấp một điểm truy cập toàn cục, nó có thể được sử dụng ở bất kỳ đâu trong chương trình, làm cho việc theo dõi và kiểm tra mã trở nên khó khăn hơn. Nó cũng có thể tạo ra sự phụ thuộc giữa các phần của mã, gây khó khăn cho việc bảo dưỡng.
- Vấn đề với đa luồng: Khi sử dụng Singleton trong môi trường đa luồng, có thể phát sinh các vấn đề liên quan đến việc đồng bộ hóa. Nếu không cẩn thận, việc tạo ra các instance có thể xảy ra nhiều lần, dẫn đến các hậu quả không mong muốn.
- Khó mở rộng: Singleton Pattern có thể gây khó khăn trong việc mở rộng. Nếu bạn muốn có nhiều instance thay vì chỉ một, bạn sẽ phải thay đổi mã nguồn.
- Các vấn đề liên quan đến tình trạng toàn cục: Singleton thường được chỉ trích vì tạo ra tình trạng toàn cục, điều này có thể dẫn đến các vấn đề như tình trạng chia sẻ không phù hợp, khó khăn trong việc kiểm soát vòng đời của các đối tượng, và tăng khả năng xảy ra lỗi.
Mặc dù có những nhược điểm này, Singleton vẫn là một mẫu thiết kế hữu ích trong một số trường hợp cụ thể. Điều quan trọng là phải hiểu rõ về mẫu thiết kế này và biết cách sử dụng nó một cách hợp lý.
6. So sánh Singleton với các Design Pattern khác
Dưới đây là bảng so sánh Singleton với một số design pattern khác:
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. |
7. Một số ý kiến về Singleton Pattern
7.1. Singleton có phải là "anti-pattern"?
Singleton Pattern thường bị chỉ trích là một anti-pattern. Một anti-pattern là một thiết kế phổ biến nhưng thường dẫn đến các vấn đề không mong muốn. Điều này không có nghĩa là Singleton Pattern luôn luôn là một lựa chọn kém - nó chỉ đơn giản là có những hạn chế và nguy cơ mà bạn cần cân nhắc trước khi sử dụng.
Singleton có thể dẫn đến các vấn đề như trạng thái toàn cục, khó kiểm thử, và vi phạm một số nguyên tắc thiết kế tốt như SOLID. Điều này có thể gây khó khăn khi cần mở rộng, sửa đổi, hoặc kiểm thử phần mềm của bạn.
Tuy nhiên, điều này không có nghĩa là bạn nên tránh hẳn Singleton. Trong một số trường hợp, Singleton có thể là một lựa chọn hợp lý. Điều quan trọng là bạn cần hiểu rõ về các hạn chế và nguy cơ của nó, và cân nhắc kỹ trước khi quyết định sử dụng Singleton trong tình huống cụ thể nào.
Ví dụ, nếu bạn cần một cách thuận tiện để chia sẻ dữ liệu hoặc tài nguyên giữa nhiều phần khác nhau của ứng dụng mà không muốn truyền dữ liệu đó qua lại, Singleton có thể là một giải pháp tốt. Tuy nhiên, hãy chắc chắn rằng bạn hiểu và quản lý được các nguy cơ liên quan đến việc sử dụng Singleton.
7.2. Lý do một số lập trình viên tránh sử dụng Singleton
- Khó kiểm thử (Testing): Singleton thường tạo ra trạng thái toàn cục, điều này có thể gây khó khăn khi kiểm thử. Do các test case có thể chạy đồng thời và chia sẻ đối tượng Singleton, điều này có thể dẫn đến các hiệu ứng phụ không mong muốn.
- Vi phạm nguyên lý một (Single Responsibility Principle): Singleton thường phục vụ nhiều mục đích, điều này vi phạm nguyên lý một trong các nguyên lý thiết kế SOLID.
- Phụ thuộc (Dependencies): Việc sử dụng Singleton có thể tạo ra các phụ thuộc ẩn giữa các lớp, điều này dễ dẫn đến mã nguồn khó hiểu hơn và khó bảo dưỡng.
- Khó mở rộng (Scalability): Singleton có thể gây khó khăn khi bạn cần mở rộng ứng dụng. Đặc biệt là trong môi trường đa luồng, việc điều chỉnh và đồng bộ hóa trạng thái của Singleton có thể trở nên phức tạp.
Tuy nhiên, điều quan trọng là phải nhận ra rằng không có Design Pattern nào là hoàn hảo và phù hợp cho mọi tình huống. Mỗi Design Pattern, bao gồm cả Singleton, đều có ưu và nhược điểm của riêng mình. Việc chọn sử dụng một Design Pattern nên dựa trên việc hiểu rõ về tình huống cụ thể và cân nhắc kỹ lưỡng về các hạn chế và nguy cơ mà Pattern đó mang lại.
7.3. Khi nào nên sử dụng Singleton Pattern?
Việc sử dụng Singleton Pattern phụ thuộc vào nhiều yếu tố, và nó không phải lúc nào cũng là lựa chọn tốt nhất. Tuy nhiên, dưới đây là một số tình huống mà Singleton Pattern có thể phát huy hiệu quả:
- Khi cần quản lý tài nguyên chung: Singleton Pattern thường được sử dụng khi cần quản lý một tài nguyên chung nào đó, ví dụ như một kết nối cơ sở dữ liệu hoặc một tệp log.
- Khi cần kiểm soát việc truy cập vào một tài nguyên chung: Singleton Pattern đảm bảo rằng chỉ có một thực thể duy nhất được truy cập vào tài nguyên, giúp giảm thiểu xung đột và tăng hiệu suất.
- Khi cần giữ trạng thái toàn cục: Singleton Pattern có thể được sử dụng để lưu giữ trạng thái toàn cục của ứng dụng. Tuy nhiên, cần cẩn thận với việc sử dụng Singleton để lưu giữ trạng thái, vì nó có thể tạo ra các hiệu ứng phụ không mong muốn và làm giảm khả năng kiểm thử.
Tóm lại, việc sử dụng Singleton Pattern cần cân nhắc kỹ lưỡng, dựa trên đặc điểm cụ thể của ứng dụng và nhu cầu cụ thể. Một quyết định thông minh sẽ giúp tận dụng được lợi ích của Singleton, đồng thời giảm thiểu những hạn chế và rủi ro tiềm ẩn.
8. Thay thế cho Singleton Pattern
8.1. Dependency Injection
Dependency Injection (DI) là một cách tiếp cận mà các đối tượng không tạo ra các phụ thuộc của chúng, thay vào đó, chúng nhận các phụ thuộc đó từ một nguồn bên ngoài. Điều này giúp tăng khả năng kiểm soát và kiểm thử.
-
Cách hoạt động của Dependency Injection
- Với DI, bạn không tạo ra một đối tượng của một class bên trong class khác. Thay vào đó, bạn sẽ truyền (inject) các đối tượng này vào class qua hàm tạo (constructor) hoặc thông qua các phương thức setter. Điều này làm cho code của bạn trở nên dễ kiểm soát hơn và dễ kiểm thử hơn, vì bạn có thể dễ dàng thay thế các phụ thuộc đó với các phiên bản giả mạo (mock) khi kiểm thử.
-
Dependency Injection đối với Singleton
- DI có thể được xem là một giải pháp thay thế cho Singleton Pattern trong một số tình huống. Đặc biệt, nó có thể giúp giải quyết một số vấn đề mà Singleton gặp phải, chẳng hạn như việc khó khăn trong việc kiểm thử và việc tăng cường sự phụ thuộc giữa các phần của mã.
- Với DI, bạn có thể tạo ra một đối tượng Singleton và sau đó inject nó vào các class khác. Điều này giúp giảm bớt sự phụ thuộc giữa các class và giúp mã của bạn trở nên dễ kiểm soát và kiểm thử hơn.
Tuy nhiên, cần lưu ý rằng DI không phải lúc nào cũng là một giải pháp thay thế cho Singleton. Trong một số trường hợp, Singleton vẫn là lựa chọn tốt nhất, đặc biệt khi bạn cần đảm bảo rằng chỉ có một thực thể duy nhất của một class tồn tại trong toàn bộ chương trình.
8.2. Multiton Pattern
Multiton Pattern là một phiên bản tổng quát hóa của Singleton Pattern, nơi mà bạn muốn cho phép tồn tại nhiều hơn một instance của một class nhưng mỗi instance đó lại là unique theo một cách nào đó.
-
Cách hoạt động của Multiton Pattern
- Multiton Pattern sử dụng một map để lưu trữ các instances. Key của map là thuộc tính đặc biệt mà bạn muốn dùng để phân biệt giữa các instances. Ví dụ, nếu bạn muốn có một instance duy nhất cho mỗi loại cơ sở dữ liệu mà bạn đang kết nối, thì key có thể là tên của loại cơ sở dữ liệu đó.
- Khi cần một instance, bạn kiểm tra xem instance với key tương ứng đã tồn tại trong map chưa. Nếu chưa, bạn tạo một instance mới và lưu nó vào map. Nếu rồi, bạn chỉ cần lấy instance đó từ map.
-
Multiton Pattern đối với Singleton:
- Multiton Pattern có thể được xem như một giải pháp thay thế cho Singleton Pattern trong trường hợp bạn muốn cho phép có nhiều instances nhưng vẫn muốn kiểm soát chúng một cách chặt chẽ.
Tuy nhiên, cần lưu ý rằng Multiton cũng có nhược điểm của riêng mình. Cụ thể, nó có thể dẫn đến việc sử dụng quá nhiều tài nguyên nếu có quá nhiều instances. Ngoài ra, việc sử dụng map để lưu trữ các instances cũng có thể dẫn đến các vấn đề về thread-safety tương tự như trong Singleton Pattern.
9. Tổng Kết
Qua bài viết, chúng ta đã cùng nhau tìm hiểu về Singleton Pattern, một trong những mẫu thiết kế được sử dụng rộng rãi trong lập trình. Singleton Pattern giúp đảm bảo rằng một class chỉ có một instance và cung cấp một điểm truy cập toàn cầu đến nó.
Chúng ta đã khám phá các cách triển khai Singleton Pattern trong Java, từ phương pháp cơ bản đến các cách cải tiến như Lazy Initialization, Thread-Safe Singleton, Double-Checked Locking và Bill Pugh Singleton Implementation. Chúng ta cũng đã tìm hiểu về những vấn đề có thể phá vỡ cấu trúc Singleton như Reflection, Serialization và cách chúng ta có thể ngăn chặn nó.
Đặc biệt, chúng ta đã tìm hiểu các trường hợp thực tế mà Singleton có thể được sử dụng như quản lý kết nối cơ sở dữ liệu, ghi log và quản lý cấu hình ứng dụng.
Tuy nhiên, Singleton không phải lúc nào cũng là lựa chọn tốt nhất. Mẫu thiết kế này có những ưu điểm như kiểm soát chặt chẽ việc tạo instance, tiết kiệm tài nguyên nhưng cũng có nhược điểm như khó kiểm thử, phụ thuộc cao và khó mở rộng.
Chúng ta cũng đã so sánh Singleton với các Design Pattern khác như Prototype Pattern, Factory Pattern và Builder Pattern để thấy rõ hơn về sự khác biệt giữa chúng.
Cuối cùng, chúng ta đã xem xét một số ý kiến trái chiều về Singleton Pattern và một số giải pháp thay thế cho Singleton như Dependency Injection và Multiton Pattern.
Hy vọng thông qua bài viết này, bạn đã có cái nhìn sâu sắc hơn về Singleton Pattern và cách áp dụng nó một cách linh hoạt trong các dự án lập trình của mình.
10. Tài liệu tham khảo
-
Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides. Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley Professional, 1994.
-
Joshua Bloch. Effective Java (3rd Edition). Addison-Wesley Professional, 2018.
-
Martin Fowler. Patterns of Enterprise Application Architecture. Addison-Wesley Professional, 2002.
-
Robert C. Martin. Clean Code: A Handbook of Agile Software Craftsmanship. Prentice Hall, 2008.
-
GoF (Gang of Four). Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley Professional, 1994.
-
Freeman, E., Robson, E., Bates, B., & Sierra, K. Head First Design Patterns: A Brain-Friendly Guide. O'Reilly Media, 2004.
-
"Singleton pattern" - Wikipedia, The Free Encyclopedia, https://en.wikipedia.org/wiki/Singleton_pattern
-
"Design Patterns in Java Tutorial" - TutorialsPoint, https://www.tutorialspoint.com/design_pattern/singleton_pattern.htm
-
"Singleton Design Pattern" - GeeksforGeeks, https://www.geeksforgeeks.org/singleton-design-pattern/
-
"The Singleton Pattern" - The Java™ Tutorials, Oracle, https://docs.oracle.com/javase/tutorial/designpatterns/singleton.html
-
"Singleton Design pattern" - Java2Blog, https://java2blog.com/singleton-design-pattern-in-java/
-
"Singleton Pattern & its issues" - Medium, https://medium.com/@cscalfani/singleton-the-root-of-all-evil-8af748a4251d
-
"Dependency Injection" - Martin Fowler, https://martinfowler.com/articles/injection.html
-
"Java Singleton Design Pattern Practices with Examples" - JournalDev, https://www.journaldev.com/1377/java-singleton-design-pattern-best-practices-examples