Giới thiệu
Singleton là một design pattern thuộc nhóm Creational Design Pattern, cho phép bạn đảm bảo rằng một lớp chỉ có một instance, đồng thời cung cấp một điểm truy cập toàn cục vào instance này.
Mẫu Singleton giải quyết hai vấn đề cùng lúc, nhưng vi phạm Single Responsibility Principle:
- Đảm bảo rằng một lớp chỉ có một instance duy nhất: cho dù bạn tạo bao nhiêu đối tượng đi nữa, thì tất cả đều chỉ về lại một instance.
- Cung cấp một điểm truy cập toàn cục vào thể hiện đó: cho phép bạn truy cập một số đối tượng từ bất kỳ đâu trong chương trình. Tuy nhiên, nó cũng bảo vệ instance đó khỏi bị ghi đè bởi mã khác.
Kiến trúc
Để triển khai Singleton ta có các bước chung sau:
- Đặt constructor mặc định là private, để ngăn các đối tượng khác sử dụng toán tử new với class Singleton.
- Tạo một biến static private để lưu instance của class đó đảm bảo rằng nó là duy nhất và chỉ được tạo một lần.
- Tạo một public static method trả về instance vừa khởi tạo bên trên, đây là cách duy nhất để các class khác có thể truy cập vào instance của class này.

Ưu & nhược điểm
Ưu điểm | Nhược điểm |
Có thể chắc chắn rằng một lớp chỉ có một instance duy nhất. Có được điểm truy cập toàn cục vào instance đó. Đối tượng singleton chỉ được khởi tạo khi được yêu cầu lần đầu tiên. | Vi phạm Single Responsibility Principle. Mẫu giải quyết hai vấn đề cùng một lúc. Mẫu Singleton có thể che giấu thiết kế kém, ví dụ, khi các thành phần của chương trình biết quá nhiều về nhau. Mẫu yêu cầu xử lý đặc biệt trong môi trường đa luồng để nhiều luồng không tạo ra một đối tượng singleton nhiều lần. Có thể sinh ra khó khăn trong việc unit test client code của Singleton bởi nhiều test frameworks dựa vào kế thừa khi sản sinh mock objects. Hoặc chỉ cần không viết unit test, hoặc không sử dụng mẫu Singleton. |
Triển khai code
No Thread-safe Singleton
// Dùng từ khoá sealed để ngăn các class khác kế thừa class này.
public sealed class Singleton1
{
// Chỉ cho phép khởi tạo từ trong đây.
private Singleton1() { }
private static Singleton1? _instance = null;
public static Singleton1 GetInstance()
{
// Ở lần gọi hàm đầu tiên, _instance ban đầu là null và lớp Singleton1 được khởi tạo.
if (_instance == null) _instance = new Singleton1();
return _instance;
}
public void SomeBusinessLogic()
{
// ...
}
}
internal class Program
{
static void Main(string[] args)
{
Singleton1 s1 = Singleton1.GetInstance();
Singleton1 s2 = Singleton1.GetInstance();
if (s1 == s2)
Console.WriteLine("Singleton works, both variables contain the same instance.");
else
Console.WriteLine("Singleton failed, variables contain different instances.");
Console.ReadKey();
}
}
C#Thread-safe Singleton
public sealed class Singleton2
{
private Singleton2() { }
private static Singleton2? _instance = null;
// Chúng ta sẽ có một lock object được dùng để đồng bộ hóa các luồng trong lần truy cập đầu tiên vào Singleton.
private static readonly object _lock = new object();
public static Singleton2 GetInstance(string value)
{
if (_instance == null)
{
// Bây giờ, hãy tưởng tượng rằng chương trình vừa mới được khởi chạy.
// Vì chưa có phiên bản Singleton nào, nhiều luồng có thể đồng thời
// vượt qua điều kiện trước đó (_instance == null) và vào trong bước này
// gần như cùng lúc. Luồng đầu tiên sẽ có được khóa và sẽ tiến hành tiếp,
// trong khi các luồng còn lại sẽ đợi ở đây.
lock (_lock)
{
// Luồng đầu tiên có được khóa, thoả điều kiện _instance == null,
// đi vào trong và tạo ra instance Singleton. Khi nó chạy xong và bỏ lock,
// một luồng khác có thể đã chờ khóa để vào đây. Nhưng vì instance đã tạo,
// luồng sẽ không tạo thêm đối tượng mới.
if (_instance == null)
{
_instance = new Singleton2();
_instance.Value = value;
}
}
}
return _instance;
}
// Biến này dùng để kiểm tra xem khi tạo 2 đối tượng nó có thật sự khác nhau không.
public string? Value { get; set; } = null;
}
internal class Program
{
static void Main(string[] args)
{
Thread process1 = new Thread(() =>
{
Singleton2 s1 = Singleton2.GetInstance("Hi");
Console.WriteLine("s1 Value: " + s1.Value);
});
Thread process2 = new Thread(() =>
{
Singleton2 s2 = Singleton2.GetInstance("Hello");
Console.WriteLine("s2 Value: " + s2.Value);
});
Thread process3 = new Thread(() =>
{
Singleton2 s3 = Singleton2.GetInstance("Hola");
Console.WriteLine("s3 Value: " + s3.Value);
});
process1.Start();
process2.Start();
process3.Start();
process1.Join();
process2.Join();
process3.Join();
Console.ReadKey();
}
}
C#Chạy chương trình nhiều lần, chúng ta sẽ thấy nó đều ra cùng 1 kết quả đầu tiên!
Thử nghiệm No Thread-safe trong đa luồng
Ở đây chúng ta sẽ sửa lại class Singleton1 để kiểm tra xem với đa luồng khi gán giá trị bất kì ở mỗi luồng có thay đổi ở mỗi lần chạy không.
public sealed class Singleton1
{
private Singleton1() { }
private static Singleton1? _instance = null;
public static Singleton1 GetInstance(string value)
{
if (_instance == null) _instance = new Singleton1();
_instance.Value = value;
return _instance;
}
public string? Value { get; set; } = null;
public void SomeBusinessLogic()
{
// ...
}
}
internal class Program
{
static void Main(string[] args)
{
Thread process1 = new Thread(() =>
{
Singleton1 s1 = Singleton1.GetInstance("Hi");
Console.WriteLine("s1 Value: " + s1.Value);
});
Thread process2 = new Thread(() =>
{
Singleton1 s2 = Singleton1.GetInstance("Hello");
Console.WriteLine("s2 Value: " + s2.Value);
});
Thread process3 = new Thread(() =>
{
Singleton1 s3 = Singleton1.GetInstance("Hola");
Console.WriteLine("s3 Value: " + s3.Value);
});
process1.Start();
process2.Start();
process3.Start();
process1.Join();
process2.Join();
process3.Join();
Console.ReadKey();
}
}
C#Và ở mỗi lần chạy là không có cái nào cùng ra 1 kết quả!
Bài viết đến đây là kết thúc, mình sẽ cố gắng giới thiệu những Design Pattern hay ho trong các bài viết tiếp theo nhé!