Loading Now

Singleton Design Pattern – Chuyên Nghiệp Hơn Trong Lập Trình

Design pattern là gì? Có quan trọng không? Tại sao một lập trình viên chuyên nghiệp lại cần tìm hiểu về Design pattern? Câu trả lời sẽ có trong bài viết này.

Design patterns có quan trọng?

Đã là một lập trình viên, chúng ta ít nhiều cũng đã nghe về khái niệm design patterns. Hầu hết trong các Job description đề có nhắc đến phải biết các giải thuật, phải nắm vững design pattern. Vậy design patterns có thực sự quan trọng khi ngoài kia có rất nhiều lập trình viên ở đủ mọi level vẫn có thể xây dựng sản phẩm phần mềm mà không cần kiến thức căn bản về design patterns.
Design pattern không phải là cái bắt buộc phải có cho một lập trình viên nhưng nó là thứ cần thiết để trở thành một lập trình viên chuyên nghiệp. Nếu bạn muốn tiến xa hơn, đi sâu hơn vào con đường của một chuyên gia trong lĩnh vực lập trình, bạn cần phải nắm vững design patterns và linh hoạt vận dụng chúng vào dự án của mình.

Design pattern là gì?

Dưới đây là định nghĩa từ wiki

In software engineering, a software design pattern is a general, reusable solution to a commonly occurring problem within a given context in software design. It is not a finished design that can be transformed directly into source or machine code. Rather, it is a description or template for how to solve a problem that can be used in many different situations. Design patterns are formalized best practices that the programmer can use to solve common problems when designing an application or system.

Có thể hiểu design pattern đơn giản là tập hợp của những patterns (mẫu) phổ biến được sử dụng để giải quyết các vấn đề thường gặp trong quá trình phát triển sản phẩm phần mềm. Đây là tập hợp những pattern cốt lõi đã được tối ưu trong suốt quá trình hình thành và phát triển của ngành lập trình. Theo mình thì những gì đã có sẵn rồi thì đừng cố “reinvent the wheel” (vì bạn không giỏi như bạn nghĩ đâu :))), hãy học cách ứng dụng sao cho phù hợp với nhu cầu của chúng ta. Ông tổ Ctrl + C Ctrl V đã dạy như vậy rồi. Okay, nếu phân nhóm design patterns thì chúng ta có 3 nhóm như sau

Phân nhóm design patterns

  1. Creational patterns: cách thức tạo object một cách linh hoạt và có khả năng tái sử dụng cao.
  2. Structural patterns: định nghĩa cách thức để kết hợp các objects theo một cấu trúc nhất định.
  3. Behavioral patterns: mô tả các hành vi mà các object giao tiếp và phân chia nhiệm vụ.

Hôm nay chúng ta sẽ bắt đầu với pattern đầu tiên. Rất quen thuộc, một pattern trong nhóm creational patterns, một trong những pattern đơn giản nhất đó là Singleton

Mình sẽ giúp các bạn trả lời các câu hỏi sau

  1. Thế nào là singleton pattern
  2. Vấn đề mà singleton giải quyết
  3. Cách thức implement cơ bản với Java
  4. Các biến thể của singleton

Singleton pattern là gì?

Q: Thế nào là singleton pattern?

A: Đơn giản pattern này phải thỏa mãn 2 tiêu chí

  1. Giới hạn chỉ có duy nhất 1 instance của class đó tồn tại ở bất kỳ thời điểm nào class đó được request
  2. Cho phép quyền truy cập global

Singleton dùng để làm gì?

Q: Vậy với định nghĩa trên, nó giải quyết vấn đề gì?

A: Vấn đề mà singleton giải quyết là

Làm sao để đảm bảo rằng 1 class chỉ có 1 instance duy nhất và nó luôn sẵn sàng để sử dụng ở bất kỳ thời điểm hoặc vị trí nào trong phần mềm của chúng ta. Có thể lấy ví dụ như:

  • Những tài nguyên được chia sẻ như DB để truy suất, file vật lý được sử dụng chung. Những tài nguyên này nên chỉ có 1 instance tồn tại để sử dụng và theo dõi trạng thái của nó trong suốt quá trình sử dụng.
  • Hoặc những kiểu object không cần thiết phải tạo instance mới mỗi lần sử dụng như logging object, hãy tưởng tượng chuyện gì xảy với mỗi dòng log chúng ta lại tạo mới một logging object.

Cách thức implement cơ bản với Java

Q: Okay, đến đây chúng ta đã nắm được vấn đề mà singleton tập trung giải quyết. Muốn implement nó thì như thế nào?

A: Trước khi vào cách thức implement cụ thể, chúng ta thử review thử đoạn code phía dưới

// Not really a singleton implementation
public class Singleton {
	private Singleton singletonInstance = new Singleton();

	// Public constructor
	public Singleton() {
	}

	// Setter & getter
	public Singleton getSingletonInstance() {
		return singletonInstance;
	}

	public void setSingletonInstance(Singleton singletonInstance) {
		this.singletonInstance = singletonInstance;
	}

}

Chắc hẳn một trong số các bạn cũng đã ít nhất một lần implement class như ví dụ trên. Nó có thể coi là một ví dụ cho singleton pattern nếu chúng ta tinh chỉnh thêm một chút nữa để phù hợp với định nghĩa của singleton.

Eager Initialization Singleton

public class Singleton {
	// 1. Set instance thành static
	private static Singleton singletonInstance = new Singleton();

	// 2. Constructor set về private
	private Singleton() {
	}

	// 3. Getter set thành static method
	public static Singleton getSingletonInstance() {
		return singletonInstance;
	}

	// 4. Loại bỏ setter
	/*
	 * public void setSingletonInstance(Singleton singletonInstance) {
	 * this.singletonInstance = singletonInstance; }
	 */
}

Okay, giờ thì nó đã thỏa mãn các tiêu chí của singleton. Như vậy cách thức implement tổng quát là

  1. Tạo một private static instance của singleton class đó, đây chính là instance duy nhất của class này
  2. Tạo private constructor, không cho phép tạo mới instance từ bên ngoài singleton class
  3. Tạo một public static method giúp những class bên ngoài lấy ra instance duy nhất của singleton class này

Các biến thể của singleton

Các bạn hãy check lại đoạn định nghĩa class trên và đưa ra một vài vấn đề mà cách implement này sẽ gặp phải. Có thể list ra một số vấn đề như sau

  1. Có thể thấy với cách implement trên, singletonInstance luôn được khởi tạo ngay cả khi nó chưa được sử dụng (hay còn gọi là Eager Singleton). Nếu chúng ta muốn tối ưu vùng nhớ, khi cần thì mới khởi tạo thì phải sửa lại code như thế nào (Lazy initilization)?
  2. Trong trường hợp Lazy initialization, nếu mình chạy nhiều thread và các thread này đồng thời khởi tạo thì có còn thỏa mãn ràng buộc chỉ có 1 instance tại một thời điểm?

    Lazy Initialization

    Chỉ khởi tạo khi được request

    public class LazyInitializationSingleton {
    
    	// Không khởi tạo instance ở đây
    	private static LazyInitializationSingleton lazyInitializationSingletonInstance;
    
    	private LazyInitializationSingleton() {
    	}
    
    	public static LazyInitializationSingleton getLazyInitializationSingleton() {
    		if (Objects.isNull(lazyInitializationSingletonInstance)) { // Khởi tạo instance chỉ khi instance này được request lần
    															// đầu
    			lazyInitializationSingletonInstance = new LazyInitializationSingleton();
    		}
    		return lazyInitializationSingletonInstance;
    	}
    }
    

    Vấn đề của cách implement này là trong trường hợp nhiều thread cùng gọi hàm getLazyInitializationSingleton() và đều thấy rằng instance chưa được khởi tạo. Điều này dẫn đến việc có khả năng các thead sẽ cùng gọi hàm khởi tạo một new instance, đồng nghĩa với việc nó đã phá vỡ tiêu chí của singleton về việc chỉ có một instance duy nhất ở mọi thời điểm.

    Thread-safe Singleton

    Để giải quyết vấn đề của lazy initialization singleton, chúng ta sử dụng từ khóa synchronized. Việc set method về synchronized sẽ lock tất cả các thread không cho thread đó gọi method threadSafeSingletonInstance() cho đến khi thread đang gọi thực thi xong.

    public class ThreadSafeSingleton {
    
    	private static ThreadSafeSingleton threadSafeSingletonInstance;
    
    	private ThreadSafeSingleton() {
    	}
    
    	// Set method thành synchronized method
    	public static synchronized ThreadSafeSingleton getThreadSafeSingletonInstance() {
    		if (Objects.isNull(threadSafeSingletonInstance)) {
    			threadSafeSingletonInstance = new ThreadSafeSingleton();
    		}
    		return threadSafeSingletonInstance;
    	}
    }

    Vấn đề của cách implement trên là trong trường hợp nhiều thead cùng gọi, các thread phải xếp hàng tuần tự thực hiện việc request instance. Chúng ta có thể giải quyết vấn đề trên bằng cách chỉ lock khi threadSafeSingletonInstance này chưa được khởi tạo.

    public class ThreadSafeSingleton {
    
    	private static ThreadSafeSingleton threadSafeSingletonInstance;
    
    	private ThreadSafeSingleton() {
    	}
    
    	public static ThreadSafeSingleton getThreadSafeSingletonInstance() {
    		// Chỉ lock khi instance chưa được khởi tạo
    		synchronized (ThreadSafeSingleton.class) {
    			if (Objects.isNull(threadSafeSingletonInstance)) {
    				threadSafeSingletonInstance = new ThreadSafeSingleton();
    			}
    		}
    		return threadSafeSingletonInstance;
    	}
    }

    Tổng kết

    Mình hi vọng sau post này các bạn có thể nắm được

    • Singleton pattern là gì
    • Các vấn đề mà nó giải quyết
    • Cách implement như thế nào

    Một khi nhắc đến singleton pattern hãy nhớ đến bài hát One and only của Adele

    Các bạn có đóng góp, ý kiến hoặc đã từng giải quyết vấn đề thực tế bằng singleton như thế nào thì hãy comment bên dưới nhé.

    Tuần sau, mình sẽ tiếp tục series về design pattern với một creational pattern khác – Factory pattern. Mong các bạn bạn ủng hộ. Thank you~

    Post Comment

    Contact