"A class of which only a single instance can exist"
The Singleton pattern ensures that a class has only one instance and
provides a global point of access to that instance. It is named after
the singleton set, which is defined to be a set containing one
element. The office of the President of the United States is a
Singleton. The United States Constitution specifies the means by which
a president is elected, limits the term of office, and defines the
order of succession. As a result, there can be at most one active
president at any given time. Regardless of the personal identity of
the active president, the title, "The President of the United States"
is a global point of access that identifies the person in the office.
Rules of thumb
- Abstract Factory, Builder, and Prototype can use Singleton in their
implementation.
- Facade objects are often Singletons because only one Facade object is
required.
- State objects are often Singletons.
- The advantage of Singleton over global variables is that you
are absolutely sure of the number of instances when you use
Singleton, and you can change your mind and manage any number
of instances.
- The Singleton design pattern is one of the most inappropriately
used patterns. Singletons are intended to be used when a class
must have exactly one instance, no more, no less. Designers
frequently use Singletons in a misguided attempt to replace
global variables. A Singleton is, for intents and purposes,
a global variable. The Singleton does not do away with the
global; it merely renames it.
- When is Singleton unnecessary? Short answer: most of the
time. Long answer: when it's simpler to pass an object
resource as a reference to the objects that need it, rather than
letting objects access the resource globally. The real problem
with Singletons is that they give you such a good excuse not to
think carefully about the appropriate visibility of an object.
Finding the right balance of exposure and protection for
an object is critical for maintaining flexibility.
- Our group had a bad habit of using global data, so I did a study
group on Singleton. The next thing I know Singletons appeared everywhere and none of the problems related to global
data went away. The answer to the global data question
is not, "Make it a Singleton." The answer is, "Why in the
hell are you using global data?" Changing the name doesn't
change the problem. In fact, it may make it worse because
it gives you the opportunity to say, "Well I'm not doing that,
I'm doing this" – even though this and that are the same thing.
Là một nhà tư vấn được trả lương hậu hĩnh tại MegaGigaco, bạn phải xử
lý các sự cố về hiệu năng hệ thống. “Hệ thống hình như ngày càng chậm
chạp hơn.” Các lập trình viên nói:
“Hmm,” bạn nói, “Tôi lưu ý các bạn rằng chúng ta đang có một đối tượng dữ liệu có kích thước khá lớn, khoảng 20Mb”
“Vâng”, họ nói.
“Cùng một thời điểm, các bạn sử dụng bao nhiêu đối tượng này?”
“Khoảng 219”, các lập trình viên nói
“Trời, vậy các bạn sử dụng 219 đối tượng 20Mb trong lúc chương trình
hoạt động?” Bạn nói. “Chẳng lẽ không ai thấy được vấn đề ở đây à?”
“Không”, họ đồng thanh nói.
Bạn nói với họ “Các bạn sử dụng quá nhiều tài nguyên hệ thống. Các
bạn có hàng trăm đối tượng to lớn mà máy tính phải xử lý. Các bạn có
thật sự cần tất cả chúng?”
“Vâng…” họ nói.
“Tôi nghĩ là không,” bạn nói. “Tôi sẽ sửa chữa vấn đề này bằng cách sử dụng mẫu duy nhất Singleton.”
Chương này nói về việc kiểm soát số lượng đối tượng mà bạn phải tạo
ra trong mã nguồn của mình. Có hai mẫu thiết kế đặc biệt giúp ích cho
bạn: mẫu duy nhất Singleton và mẫu “hạng ruồi” flyweight.
Với mẫu duy nhất Singleton, bạn luôn đảm bảo rằng chỉ có duy nhất một
đối tượng cho một lớp cụ thể trong suốt ứng dụng. Với mẫu “hạng ruồi”
flyweight, bạn cũng có duy nhất một đối tượng cho một lớp, nhưng khi
nhìn vào mã của bạn, ta có thể thấy giống như đang có nhiều đối tượng
vậy. Đây là một thủ thuậ t khéo léo.
Tạo một đối tượng duy nhất với mẫu duy nhất Singleton.
Tôi bắt đầu với mẫu Singleton và xử lý rắc rối mà lập trình viên
MegaGigaCo gặp phải. Họ muốn chắn chắc rằng chỉ tạo duy nhất một đối
tượng cho một lớp cụ thể mặc cho người khác có cố gắng tạo bao nhiêu đối
tượng đi nữa.
Các lập trình viên đang tạo ra hàng trăm đối tượng Database trong mã
nguồn, và rắc rối là từng đối tượng này có kích thước rất lớn. Đâu là
giải pháp? Mẫu duy nhất Singeton là câu trả lời.
Mẫu
duy nhất Singleton chắc chắn rằng bạn có thể khởi tạo chỉ duy nhất một
đối tượng cho một lớp. Nếu bạn không sử dụng mẫu thiết kế này, toán tử
new như thường sử dụng, sẽ tạo ra liên tiếp nhiều đối tượng mới như sau:
Ghi nhớ: Để chắc chắn rằng bạn chỉ có duy nhất một đối tượng,
mặc cho người khác có hiện thực bao nhiêu phiên bản đi nữa, hãy sử dụng
mẫu duy nhất Singleton. Cuốn sách GoF nói rằng, mẫu Singleton “Đảm bảo
rằng một lớp chỉ có duy nhất một thể hiện và cung cấp một biến toàn cục
để truy cập nó”
Bạn sử dụng mẫu Singleton khi bạn muốn hạn chế việc sử dụng tài
nguyên (thay vì việc tạo không hạn chế số lượng đối tượng) hoặc khi bạn
cần phải xử lý một đối tượng nhạy cảm, mà dữ liệu của nó không thể chia
sẻ cho mọi thể hiện, như registry của Windows chẳng hạn.
Gợi ý: Ngoài đối tượng bản ghi registry, bạn có thể sử dụng
mẫu Singleton khi bạn muốn hạn chế số lượng các thể hiện được tạo bởi vì
bạn muốn chia sẻ dữ liệu của các đối tượng này. Ví dụ như khi bạn có
một đối tượng cửa sổ window hay hộp thoại dialog, cần phải hiện thị và
thay đổi dữ liệu, bạn sẽ không muốn tạo nhiều thể hiện của đối tượng
này, vì bạn sẽ bị bối rối trong việc phải truy cập dữ liệu của thể hiện
nào.
Việc tạo một đối tượng duy nhất cũng rất quan trọng khi bạn sử dụng
đa luồng và khi bạn không muốn sự đụng độ dữ liệu xảy ra. Ví dụ bạn đang
làm việc với một đối tượng cơ sở dữ liệu, và các thể hiện khác cũng làm
việc trên cùng cơ sở dữ liệu đó, việc đụng độ có thể gây ra các vấn đề
nghiêm trọng. Tôi sẽ thảo luận cách làm việc với mẫu Singleton và đa
luồng trong chương này.
Bất cứ khi nào bạn thật sự cần duy nhất một thể hiện của một lớp, hãy nghĩ tới mẫu Singleton ( thay vì dùng toán tử new).
Tạo lớp cơ sở dữ liệu Database dựa trên kiểu Singleton
Giờ là lúc bạn bắt tay vào viết mã nguồn. Bạn sẽ tạo một lớp tên
Database mà các lập trình viên trong công ty sẽ sử dụng. Lớp này có một
hàm khởi dựng đơn giản, như mã sau:
public class cDatabase
{
private int _record;
private string _name;
public cDatabase(string n)
{
_name = n;
_record = 0;
}
}
Bạn cần phải thêm vào hai hàm editRecord, cho phép bạn chỉnh sửa một bản ghi, và hàm getName, trả về tên gọi của Database.
public class cDatabase
{
private int _record;
private string _name;
public cDatabase(string n)
{
_name = n;
_record = 0;
}
public void editRecord(string operation)
{
Console.Writeln("Performing a: " + operation +
" opration on record: " + _record + " in a database: " + _name);
}
public string getName()
{
return _name;
}
}
Tới giờ mọi việc vẫn tốt đẹp. Bất cứ khi nào bạn tạo một đối tượng bằng
toán tử new, một đối tượng mới sẽ được tạo ra. Nếu bạn tạo 3 database,
bạn sẽ có 3 đối tượng như sau:
cDatabase dataone = new cDatabase("products");
cDatabase dataone = new cDatabase("products Also");
cDatabase dataone = new cDatabase("products Again");
Làm sao để bạn có thể tránh việc tạo một đối tượng mới khi sử dụng toán
tử new? Đây là một giải pháp – làm cho hàm khởi dụng từ toàn cục
(public) trở thành cục bộ (private)
private cDatabase(string n)
{
_name = n;
_record = 0;
}
Điều này ngăn cản mọi người sử dụng hàm khởi dựng, ngoài trừ chính
trong lớp này gọi tới. Nhưng đợi một chút, có gì không ổn ở đây? Ai ở
trên trái đất này lại cần có một hàm khởi dựng riêng tư vậy? Làm sao bạn
có thể tạo một đối tượng khi bạn không thể gọi hàm khởi tạo nó?
Bạn đã làm cho hàm khởi dựng trở nên riêng tư và cách duy nhất để
phần còn lại của thế giới khởi tạo đối tượng đó là thêm vào một hàm tạo
đối tượng, và gọi nó khi bạn chắn chắn muốn tạo một đối tượng duy nhất
cho lớp này.
Hãy xem đoạn mã sau:
public class cDatabase
{
private int _record;
private string _name;
private cDatabase(string n)
{
_name = n;
_record = 0;
}
}
OK. Đầu tiên bạn ngăn chặn việc khởi tạo bằng toán tử new. Và bây giờ
cách duy nhất là tạo một hàm để gọi việc khởi tạo đối tượng, thông
thường hàm này có tên getInstance (hay createInstance hoặc một cái tên
cụ thể như createDatabase cũng được).
Chú ý rằng hàm này được gán phạm
vi công cộng và là một phương thức tĩnh để bạn có thể truy cập tới nó
thông qua tên lớp (ví dụ như Database.getInstance()) (ND: public và
static là hai khái niệm trong OOP. Public giúp hàm có thể được sử dụng
từ bất kì lớp khác, static giúp ta có thể sử dụng hàm trực tiếp từ tên
lớp, không cần thông qua một đối tượng lớp. )
public class cDatabase
{
private int _record;
private string _name;
private cDatabase(string n)
{
_name = n;
_record = 0;
}
public static cDatabase getInstance(string n)
{
}
}
Hàm này sẽ trả về một đối tượng Database, nhưng hàm chỉ hoạt động khi có
ít nhất một đối tượng đã tồn tại. Vì thế đầu tiên ta cần kiểm tra đối
tượng này, tôi gọi nó là singleObject, xem nó đã tồn tại chưa? Nếu chưa,
tôi sẽ tạo nó. Và sau đó trả giá trị nó về cho hàm.
public class cDatabase
{
private static cDatabase _singleObj;
private int _record;
private string _name;
private cDatabase(string n)
{
_name = n;
_record = 0;
}
public static cDatabase getInstance(string n)
{
if (_singleObj == null)
_singleObj = new cDatabase(n);
return _singleObj;
}
}
Vấn đề đã được giải quyết. Bây giờ chỉ có duy nhất một đối tượng
Database tồn tại trong cùng một thời điểm. (Vấn đề đa luồng ta sẽ giải
quyết trong phần sau của chương). Việc gọi hàm getInstance sẽ cho ta một
đối tượng như hình sau:
Khi bạn gọi getInstance lần nữa, bạn sẽ nhận được cùng một đối tượng như lần đầu.
Không quan tâm đến việc bạn gọi bao nhiêu lần getInstance, bạn luôn
nhận được cùng một đối tượng. Đó chính là cách bạn phải làm với mẫu
singleton.
Chạy thử ví dụ với mẫu Singleton
Bắt đầu bằng việc tạo một đối tượng Database với tên là products, sau đó gọi hàm getName:
public static void main()
{
cDatabase database;
database = cDatabase.getInstance("products");
Console.Writeline("This is the: " + cDatabase.getName() + " database");
}
Sau đó bạn tiếp tục tạo một đối tượng Database với tên là employees, và gọi lại hàm getName để kiểm tra:
public static void main()
{
cDatabase database;
database = cDatabase.getInstance("products")
Console.Writeline("This is the " + database.getName() + " database");
database = cDatabase.getInstace("employees");
Console.Writeline("This is the " + database.getName() + " database");
}
Tuy nhiên đối tượng Database đã được tạo, vì vậy trong lần thứ hai, hàm
getInstance vẫn trả về đối tượng Database cũ, và kết quả là bạn nhận
được thông báo:
This is the products database
This is the products database
Quá rõ ràng. Bạn đã nhận được duy nhất một đối tượng cho dù đã thực hiện
việc tạo hai lần. Cách thức bạn làm việc như sau: ngăn cản việc khởi
tạo bằng toán tử new, và tạo một hàm mới để tạo đối tượng theo ý bạn. Đó
chính là cách mẫu Singleton hoạt động.
Đối với vấn đề đa luồng!
Hãy xem lại hàm getInstance trong ví dụ trên:
Có một lỗ hổng tiềm tàng ở đây, tuy nhỏ nhưng là một lỗ hổng rõ ràng, đó
là khi làm việc với đa luồng. Hãy nhớ rằng, bạn muốn đảm bảo rằng chỉ có
duy nhất một đối tượng Database tồn tại. Nhưng khi bạn có nhiều luồng
chương trình chạy cùng lúc, bạn sẽ gặp rắc rối. Cụ thể là, hãy chú ý
đoạn mã kiểm tra sự tồn tại của đối tượng Database:
Nếu có hai luồng cùng thực hiện hàm kiểm tra này một lúc, hai luồng
này đều thỏa điều kiện của hàm if ( tức chưa có đối tượng nào được tạo),
và điều này có nghĩa là cả hai luồng đều tạo ra một đối tượng Database.
Làm sao để chỉnh sửa chỗ này? xem đoạn mã sau:
private static object _obj = new object();
public static cCustomer createInstance(string name)
{
// double check to avoid multiThreading
if (_singleObj == null)
{
lock (_obj)
{
if (_singleObj == null)
_singleObj = new cCustomer(name);
}
}
return _singleObj;
}
sử dụng lock để khóa việc truy cập vào hàm getInstance, trong khi hàm getInstance được chạy. Bất cứ luồng nào muốn gọi hàm
getInstance, đều phải đợi hàm này hoạt động xong. và kỹ thuật này đã giải quyết được vấn đề đa luồng.
Kể từ khi bạn sử dụng kỹ thuật đồng bộ hóa trên hàm getInstance, bạn
không còn lo lắng về vấn đề đa luồng nữa. Chỉ duy nhất một luồng được
gọi hàm getInstance. Nó ngăn chặn việc tạo đối tượng bằng một bức tường
an toàn, việc kiểm tra ở trên cho thấy, nếu đối tượng muốn tạo đã tồn
tại, hàm sẽ không tạo, ngược lại, sẽ tạo đối tượng cho lớp.
Ref:
https://sourcemaking.com/design_patterns/singleton
https://haihth.wordpress.com/2013/02/23/dp-chapter5/
public class cDatabase
{
private int _record;
private string _name;
public cDatabase(string n)
{
_name = n;
_record = 0;
}
}
Bạn cần phải thêm vào hai hàm editRecord, cho phép bạn chỉnh sửa một bản ghi, và hàm getName, trả về tên gọi của Database.
public class cDatabase
{
private int _record;
private string _name;
public cDatabase(string n)
{
_name = n;
_record = 0;
}
public void editRecord(string operation)
{
Console.Writeln("Performing a: " + operation +
" opration on record: " + _record + " in a database: " + _name);
}
public string getName()
{
return _name;
}
}
Tới giờ mọi việc vẫn tốt đẹp. Bất cứ khi nào bạn tạo một đối tượng bằng toán tử new, một đối tượng mới sẽ được tạo ra. Nếu bạn tạo 3 database, bạn sẽ có 3 đối tượng như sau:
cDatabase dataone = new cDatabase("products");
cDatabase dataone = new cDatabase("products Also");
cDatabase dataone = new cDatabase("products Again");
Làm sao để bạn có thể tránh việc tạo một đối tượng mới khi sử dụng toán tử new? Đây là một giải pháp – làm cho hàm khởi dụng từ toàn cục (public) trở thành cục bộ (private)
private cDatabase(string n)
{
_name = n;
_record = 0;
}
Điều này ngăn cản mọi người sử dụng hàm khởi dựng, ngoài trừ chính
trong lớp này gọi tới. Nhưng đợi một chút, có gì không ổn ở đây? Ai ở
trên trái đất này lại cần có một hàm khởi dựng riêng tư vậy? Làm sao bạn
có thể tạo một đối tượng khi bạn không thể gọi hàm khởi tạo nó?
Bạn đã làm cho hàm khởi dựng trở nên riêng tư và cách duy nhất để
phần còn lại của thế giới khởi tạo đối tượng đó là thêm vào một hàm tạo
đối tượng, và gọi nó khi bạn chắn chắn muốn tạo một đối tượng duy nhất
cho lớp này.
Hãy xem đoạn mã sau:
public class cDatabase
{
private int _record;
private string _name;
private cDatabase(string n)
{
_name = n;
_record = 0;
}
}
OK. Đầu tiên bạn ngăn chặn việc khởi tạo bằng toán tử new. Và bây giờ
cách duy nhất là tạo một hàm để gọi việc khởi tạo đối tượng, thông
thường hàm này có tên getInstance (hay createInstance hoặc một cái tên
cụ thể như createDatabase cũng được).
Chú ý rằng hàm này được gán phạm
vi công cộng và là một phương thức tĩnh để bạn có thể truy cập tới nó
thông qua tên lớp (ví dụ như Database.getInstance()) (ND: public và
static là hai khái niệm trong OOP. Public giúp hàm có thể được sử dụng
từ bất kì lớp khác, static giúp ta có thể sử dụng hàm trực tiếp từ tên
lớp, không cần thông qua một đối tượng lớp. )
public class cDatabase
{
private int _record;
private string _name;
private cDatabase(string n)
{
_name = n;
_record = 0;
}
public static cDatabase getInstance(string n)
{
}
}
Hàm này sẽ trả về một đối tượng Database, nhưng hàm chỉ hoạt động khi có
ít nhất một đối tượng đã tồn tại. Vì thế đầu tiên ta cần kiểm tra đối
tượng này, tôi gọi nó là singleObject, xem nó đã tồn tại chưa? Nếu chưa,
tôi sẽ tạo nó. Và sau đó trả giá trị nó về cho hàm.
public class cDatabase
{
private static cDatabase _singleObj;
private int _record;
private string _name;
private cDatabase(string n)
{
_name = n;
_record = 0;
}
public static cDatabase getInstance(string n)
{
if (_singleObj == null)
_singleObj = new cDatabase(n);
return _singleObj;
}
}
Vấn đề đã được giải quyết. Bây giờ chỉ có duy nhất một đối tượng
Database tồn tại trong cùng một thời điểm. (Vấn đề đa luồng ta sẽ giải
quyết trong phần sau của chương). Việc gọi hàm getInstance sẽ cho ta một
đối tượng như hình sau:
Khi bạn gọi getInstance lần nữa, bạn sẽ nhận được cùng một đối tượng như lần đầu.
Không quan tâm đến việc bạn gọi bao nhiêu lần getInstance, bạn luôn nhận được cùng một đối tượng. Đó chính là cách bạn phải làm với mẫu singleton.
Chạy thử ví dụ với mẫu Singleton
Bắt đầu bằng việc tạo một đối tượng Database với tên là products, sau đó gọi hàm getName:
public static void main()
{
cDatabase database;
database = cDatabase.getInstance("products");
Console.Writeline("This is the: " + cDatabase.getName() + " database");
}
Sau đó bạn tiếp tục tạo một đối tượng Database với tên là employees, và gọi lại hàm getName để kiểm tra:
public static void main()
{
cDatabase database;
database = cDatabase.getInstance("products")
Console.Writeline("This is the " + database.getName() + " database");
database = cDatabase.getInstace("employees");
Console.Writeline("This is the " + database.getName() + " database");
}
Tuy nhiên đối tượng Database đã được tạo, vì vậy trong lần thứ hai, hàm
getInstance vẫn trả về đối tượng Database cũ, và kết quả là bạn nhận
được thông báo:
This is the products database
This is the products database
Quá rõ ràng. Bạn đã nhận được duy nhất một đối tượng cho dù đã thực hiện
việc tạo hai lần. Cách thức bạn làm việc như sau: ngăn cản việc khởi
tạo bằng toán tử new, và tạo một hàm mới để tạo đối tượng theo ý bạn. Đó
chính là cách mẫu Singleton hoạt động.
Đối với vấn đề đa luồng!
Hãy xem lại hàm getInstance trong ví dụ trên:
Có một lỗ hổng tiềm tàng ở đây, tuy nhỏ nhưng là một lỗ hổng rõ ràng, đó
là khi làm việc với đa luồng. Hãy nhớ rằng, bạn muốn đảm bảo rằng chỉ có
duy nhất một đối tượng Database tồn tại. Nhưng khi bạn có nhiều luồng
chương trình chạy cùng lúc, bạn sẽ gặp rắc rối. Cụ thể là, hãy chú ý
đoạn mã kiểm tra sự tồn tại của đối tượng Database:
Nếu có hai luồng cùng thực hiện hàm kiểm tra này một lúc, hai luồng
này đều thỏa điều kiện của hàm if ( tức chưa có đối tượng nào được tạo),
và điều này có nghĩa là cả hai luồng đều tạo ra một đối tượng Database.
Làm sao để chỉnh sửa chỗ này? xem đoạn mã sau:
private static object _obj = new object();
public static cCustomer createInstance(string name)
{
// double check to avoid multiThreading
if (_singleObj == null)
{
lock (_obj)
{
if (_singleObj == null)
_singleObj = new cCustomer(name);
}
}
return _singleObj;
}
sử dụng lock để khóa việc truy cập vào hàm getInstance, trong khi hàm getInstance được chạy. Bất cứ luồng nào muốn gọi hàm
getInstance, đều phải đợi hàm này hoạt động xong. và kỹ thuật này đã giải quyết được vấn đề đa luồng.
Kể từ khi bạn sử dụng kỹ thuật đồng bộ hóa trên hàm getInstance, bạn
không còn lo lắng về vấn đề đa luồng nữa. Chỉ duy nhất một luồng được
gọi hàm getInstance. Nó ngăn chặn việc tạo đối tượng bằng một bức tường
an toàn, việc kiểm tra ở trên cho thấy, nếu đối tượng muốn tạo đã tồn
tại, hàm sẽ không tạo, ngược lại, sẽ tạo đối tượng cho lớp.
Ref:
https://sourcemaking.com/design_patterns/singleton
https://haihth.wordpress.com/2013/02/23/dp-chapter5/
public static void main()
{
cDatabase database;
database = cDatabase.getInstance("products");
Console.Writeline("This is the: " + cDatabase.getName() + " database");
}
public static void main()
{
cDatabase database;
database = cDatabase.getInstance("products")
Console.Writeline("This is the " + database.getName() + " database");
database = cDatabase.getInstace("employees");
Console.Writeline("This is the " + database.getName() + " database");
}
private static object _obj = new object();
public static cCustomer createInstance(string name)
{
// double check to avoid multiThreading
if (_singleObj == null)
{
lock (_obj)
{
if (_singleObj == null)
_singleObj = new cCustomer(name);
}
}
return _singleObj;
}