Showing posts with label Singleton. Show all posts
Showing posts with label Singleton. Show all posts

Sử dụng Singleton Pattern trong C#

- Trong loạt bài dịch về Spring.NET tôi đã có cơ hội chia sẽ với các bạn một mẫu Design Pattern khá hấp dẫn là Singleton. Sau bài viết đó, tôi cũng muốn viết thêm một ít về mẫu thiết kế này. Thật may khi tôi vô tình tìm được một bài viết rất hay về chủ đề này. Trong bài, tác giả đề cập đến khá nhiều vấn đề lí thú khác có liên quan đến lập trình, đặc biệt là tính lazy-load và thread-safe của đối tượng. Vì vậy, tôi quyết định nấu nướng nó, 1 là để chia sẻ với những ai quan tâm, 2 là để nâng cao khả năng đọc hiểu của tôi , và tất nhiên là khả năng viết lách nữa hê hê :bbpchoc:.

- Trước khi đọc bài này, các bạn nên nắm trước và hiểu một số thuật ngữ, từ khóa và kĩ thuật trong C# như : base class, sub class, constructor, member, seal, private, public, protected, static, instance, reference, property, performance, thread, multi-thread, lock, lazy-load. Đa số từ khóa trên theo tôi nghĩ các bạn học lập trình đều đã biết đến, cá nhân tôi cũng phải tự tìm hiểu trong quá trình làm việc khi lần đầu tiên gặp những kĩ thuật như lock và lazy-load :bbpbuon:. Cho nên trước khi viết, tôi xin giới thiệu sơ qua hai kĩ thuật này. Ngoài ra, trong bài viết có khi tôi sử dụng chữ “instance” và “đối tượng”, hai từ này đều có ý nghĩa vai trò như nhau. Tôi nghĩ có một số từ tôi sẽ giữ nguyên tên tiếng Anh mà không dịch ra tiếng Việt như: thread, multi-thread, performance, …. vì khi được dịch ra đọc thấy nó rất chuối :bbpnghi2:, hơn nữa những từ tiếng Anh này cũng khá quen thuộc với chúng ta, ai làm lập trình cũng có thể hiểu được. Và cuối cùng, trong bài dịch tôi có thêm vào một số ý kiến của mình, các bạn sẽ dễ dàng phân biệt các ý kiến của tôi với phần dịch khi thấy icon sau "http://img145.imageshack.us/img145/5899/commentdz0.gif "



Singleton Pattern
Singleton Pattern


- Khi lập trình multi-thread, chắc chắn các bạn sẽ gặp phải vấn đề đau đầu gọi là đồng bộ hóa tiến trình. Lúc trước tôi học về vấn đề này trong trường đại học ở môn “Hệ điều hành nâng cao”. :bbpcuoi3:He he lúc trước lười lắm nên khi đi thi không được điểm cao, cả lớp toàn 9 với 10 riêng tôi được 7. Vì đua đòi với chúng bạn, tôi quyết định thi lại môn này với hi vọng nâng điểm lên. Xui thay đến lần thi thứ 2 tôi bị bệnh khá nặng, thế là phải bỏ thi. Đến học kì sau thi lại thì chỉ được 5 điểm, chả hiểu:bbpktay2:. Thi lần nữa để hi vọng nâng điểm thì lại được 7. Trời ko thương người hiền lành mà, nhưng cũng nhờ vậy mà tôi nắm được khá rõ kĩ thuật lập trình thread, lỡ đứa nào có hỏi cũng hãnh diện nói :”Tao đã từng lập trình MultiThread, biết viết chương trình chat qua mạng nhé”. Viết xàm hơi nhiều rồi, giờ viết thiệt : Lock là một kĩ thuật trong lập trình multi-thread, gọi là “Đồng bộ hóa tiến trình”, hiểu đơn giản là kĩ thuật đó giúp hai hoặc nhiều thread trong chương trình có thể làm việc nhịp nhàng với nhau. Giả sử trong chương trình của bạn có một biến đếm toàn cục, nhiều thread trong chương trình sẽ thay phiên nhau truy xuất giá trị biến đó và tăng giá trị nó lên 1 đơn vị. Nếu có 2 thread cùng truy xuất giá trị một lúc và cùng tăng điểm đồng thời thì sẽ có trường hợp biến đếm chỉ tăng được 1 đơn vị, vì lần tăng của thread này sẽ đè giá trị mà thread kia vừa cập nhật cho biến đếm. Do đó đám đồ đệ của anh Bill Gate mới nghĩ ra cách để giải quyết vấn đề trên cho trong .NET, và Lock chỉ là một trong rất nhiều kĩ thuật giúp ta đồng bộ hóa tiến trình. Tuy nhiên trong phạm vi bài này chúng ta chỉ nói chính về mấu thiết kế Singleton nên tôi hi vọng với những giải thích ngắn gọn trên, các bạn đã có được khái niệm cơ bản về lock, hiểu nó là gì và nó có thể làm gì cho chúng ta.

- Kế tiếp, xin nói sơ qua về lazy-load hay lazy initialization. Đó là một kĩ thuật giúp chúng ta delay lại qua trình tạo ra đối tượng, quá trình tính toán, … và chỉ thực thi quá trình đó đến khi nào nó thực sự cần đến. Về nguyên tắc, kĩ thuật này có thể đạt được bằng cách sử dụng một cờ để kiểm tra xem quá trình của chúng ta có diễn ra hay chưa, nếu chưa thì sẽ thực hiện còn nếu đã làm rồi thì chỉ cần trả về giá trị được lưu của lần thực thi trước đó hoặc đơn giản chẳng làm gì cả. Trong thực tế thì kĩ thuật này sẽ được áp dụng nhiều trong các framework ORM. Tôi xin nói một ví dụ: giả sử bạn có 1 database với 2 bảng lưu thông tin lớp học và học sinh với ràng buộc một lớp học có thể có nhiều học sinh. Tương ứng với 2 bảng trong cơ sở dữ liệu thì trong chương trình của bạn có 2 lớp là Class và Student. Trong lớp Class sẽ có một member StudentList thuộc kiểu List. Như vậy khi chương trình của bạn đọc dữ liệu từ database lên các bạn sẽ có một đối tượng _class thuộc kiểu Class và danh sách các Student tương ứng. Thực sự lúc này đối tượng _class này chỉ giữ số lượng student thôi chứ thực sự _class.StudentList[i] vẫn chưa có giá trị. Đến khi nào bạn cần truy xuất thông tin một student nào đó trong mảng thì nó mới thực sự load dữ liệu tương ứng từ database. Nếu các bạn muốn tìm hiểu thêm hãy google với từ khóa lazy-load sẽ ra rất nhiều bài viết hay. Hiện nay, các framework ORM đều hỗ trợ tính năng này và các bạn chỉ việc download về , đọc manual và sử dụng cho đúng cách. Đến đây hi vọng các bạn đã nắm được lazy-load là gì và một số ứng dụng trong thực tế của lazy-load. Tôi sẽ dừng việc giới thiệu ở đây và sẽ trình bày vào vấn đề chính của chúng ta: Sử dụng Singleton trong C# :bbpnen:

- Mẫu thiết kế Singleton là một trong những mẫu thông dụng và được sử dụng rộng rãi nhất trong kĩ thuật lập trình. Về nguyên tắc, một Singleton là một lớp chỉ cho phép một đối tượng tương ứng được tạo, và tất nhiên nó cũng phải có một public member để những đối tượng khác có thể truy xuất instance Singleton này. Thông thường, cách thiết kế lớp theo kiểu Singleton sẽ không sử dụng hàm khởi tạo có tham số, nếu không thì một yêu cầu khởi tạo đối tượng với các tham số khác nhau sẽ gây rắc rối. (Nếu cùng một instance được gọi bằng hàm getInstance với những tham số khác nhau thì ta dùng mẫu thiết kế Factory Method sẽ hợp lý hơn) Bài viết này chỉ đề cập đến những Singletons với hàm khởi tạo không tham số, hay hàm getInstance() không có tham số. Nói chung, khi người ta sử dụng Singleton vì tính lazy-load của nó, instance của lớp sẽ không cần được tạo đến khi nào nó thật sự cần thiết.

- Có khá nhiều cách để thiết kế Singleton trong C#. Tác giả sẽ thể hiện từng cách từ đơn giản đến phức tạp, bắt đầu bằng cách thông dụng nhất- cách không có tính thread-safe (an toàn khi sử dụng với thread) đến những cách có tính thread-safe và hỗ trợ lazy-loaded; đơn giản cũng như performance cao. Trong những ví dụ code mẫu ở trong bài viết, tác giả sẽ sử dụng từ khóa private mặc định cho các member của lớp. Trong nhiều ngôn ngữ khác như Java, có thể sẽ khác đi đôi chút tuy nhiên từ khóa private vẫn nên được sử dụng.

- Dù có nhiều cách thực hiện, tất cả những phương pháp tôi giới thiệu ở trong bài viết này đều có thể sử dụng một trong những điều sau để thiết kế Singleton:

• Có sử dụng một hàm khởi tạo không tham số và được khai báo private, làm như vậy sẽ không cho những lớp khác khởi tạo nó. Ngoài ra, khai báo hàm khởi tạo private cũng không cho những lớp con kế thừa, vì nếu một lớp được thiết kế theo kiểu Singleton thì nó có thể có hai hoặc nhiều lớp con và nếu mỗi lớp con đều có thể tạo ra instance thì mẫu thiết kế Singleton sẽ không còn đúng. http://img145.imageshack.us/img145/5899/commentdz0.gif Tôi chưa hiểu rõ lắm ý tác giả là thế nào. Có thể nếu một lớp Singleton có nhiều lớp con thì trong một lúc hệ thống có thể có nhiều hơn 1 đối tượng cùng thuộc về base class là Singleton, tất nhiên trong số những đối tượng đó sẽ có những instance là kiểu của subclass và chỉ một instance duy là kiểu base class thôi (Tính đa hình - polimorphism của OOP). Dù sao đi nữa, tôi vẫn thích cách khai báo protected hơn. Nếu chúng ta dùng đúng cách thì không có gì phải lo ngại, cũng giống như điện vậy thôi, có thể gây hỏa hoạn hoặc chết người nhưng nhà nhà đều dùng điện, người người dùng điện :bbpcuoi5:.

• Có sử dụng lớp sealed (một lớp được khai báo với từ khóa seal sẽ không cho phép tạo lớp con kế thừa nó). Điều này đôi khi không cần thiết theo như cách làm tác giả nói ở trên nhưng nó có thể giúp trình dịch JIT (Just – In – Time) tối ưu hóa chương trình.

• Có thể sử dụng một biến static để giữ instance duy nhất của lớp.

• Có thể sử dụng một biến public static để giúp các lớp khác sử dụng biến instance Singleton duy nhất được nói ở cách 3.

- Xin nhắc lại một lần nữa là trong tất cả các cách làm trên đều có sử dụng một public static property tên là Instance ( http://img145.imageshack.us/img145/5899/commentdz0.gif đây chỉ là cách đặt tên cho property, bạn có thể đặt tên nào tùy thích nhưng tên Instance có vẽ như ngắn gọn và hay nhất) với mục đích giúp chúng ta sử dụng instance duy nhất của lớp. Tuy nhiên nếu thích sử dụng method theo kiểu getInstance() thì vẫn có thể chuyển qua cách đấy dễ dàng mà không ảnh hưởng đến tính thread-safety hay performance.

First version – not thread-safe.



public sealed class Singleton
{

static Singleton instance=null;
Singleton()
{
}
public static Singleton Instance
{

get
{

if (instance==null)
{

instance = new Singleton();

}
return instance;

}

}

}




- Như đã giới thiệu trước đây, cách làm trên là không đạt được nguyên tắc thread-safe. Hai thread khác nhau có thể cùng một lúc chạy đến đoạn code trên, kiểm tra xem biến instance có bằng null hay không và thấy là nó null thật nên cùng tạo ra instance trong mỗi thread, như vậy là không đúng với ý tưởng thiết kế của Singleton. Chúng ta cũng dễ dàng thấy rằng biến instance trong đoạn code trên cũng có thể được khởi tạo trước khi biểu thức kiểm tra null được tính, nhưng vì các thread khác nhau đều có thể đọc bộ nhớ cùng một lúc, nếu không có một cơ chế kiểm soát để các thread khi truy xuất vùng nhớ tuần tự thì sẽ có trường hợp 2 hoặc nhiều thread đều kiểm tra và thấy biến instance đang là null và tạo ra hàng loạt các instance mới. Thiên hạ đại loạn.:bbpnodo:


Second version – simple thread safety



public sealed class Singleton
{

static Singleton instance=null;
static readonly object padlock = new object();
Singleton()
{
}
public static Singleton Instance
{

get
{

lock (padlock)
{

if (instance==null)
{

instance = new Singleton();

}
return instance;

}

}

}

}



- Cách làm thứ hai này là hoàn toàn thread-safe. Nếu bạn đã đọc phần giới thiệu ở trên của tôi về lock , bạn sẽ thấy trong đoạn code có sử dụng một shared object để lock phần code kiểm tra null của biến instance. Cách làm này sẽ giải quyết được sự xung đột có thể có giữa các thread mà chúng ta đã đề cập ở cách một. Với kĩ thuật này chúng ta sẽ chắc chắn chỉ có duy nhất một thread được phép tạo ra instance, duy nhất một thread được xử lý đoạn code trong phần lock ở một thời điểm, sau đó các thread khác mới được nhảy vào và thấy rằng đã tồn tại một instance khác null được khởi tạo trong thread đầu tiên. Tuy nhiên, cách làm này lại không có lợi về performance cho lắm vì mỗi lần biến Instance được sử dụng thì hàm lock sẽ được gọi và sẽ làm chương trình chạy chậm đi một ít.

- Điểm thứ hai cần lưu ý là ta có thể sử dụng bất kì object nào để dùng cho kĩ thuật lock, có thể sử dụng ngay biến Singleton mà một số lập trình viên vẫn sử dụng. Tuy nhiên tác giả sử dụng một 1 biến private static khác để dùng gọi trong lock. Locking trên những đối tượng mà các lớp khác trong chương trình có thể thấy và sử dụng được là điều vô cùng nguy hiểm, thậm chí có thể gây ra hiện tượng dead-lock. Sử dụng một shared object là cách tôi thích dùng bất khi nào có thể và tạo ra những đối tượng chỉ để dùng nó cho locking. Thường thì những đối tượng này nên được khai báo private trong lớp mà nó được sử dụng để tránh vấn đề nói trên. Cách làm đơn giản này giúp chúng ta có thể viết những chương trình thread-safe dễ dàng hơn nhiều.


Third version – attempted thread-safety using double-check locking


public sealed class Singleton
{

static Singleton instance=null;
static readonly object padlock = new object();
Singleton()
{
}
public static Singleton Instance
{

get
{

if (instance==null)
{

lock (padlock)
{

if (instance==null)
{

instance = new Singleton();

}

}

}
return instance;

}

}

}




- Phương pháp trên sẽ bảo đảm tính thread-safe của chương trình mà không cần phải gọi hàm lock mỗi lần sử dụng biến Instance. Nhưng thật không may, làm như vậy sẽ dẫn đến 4 trở ngại sau:

• Không áp dụng được cho Java. Điều này dường như hơi thừa khi bàn tới vì chúng ta đang đề cập đến Singleton cho .NET, tuy nhiên biết đâu sau này chúng ta cần áp dụng cho Java, lập trình viên C# cũng có thể trở thành lập trình viên Java cơ mà:bbpraroi:. Mô hình bộ nhớ trong Java không bảo đảm hàm khởi tạo được kết thúc trước khi địa chỉ vùng nhớ của đối tượng được gán cho biến instance. Mô hình vùng nhớ của Java đã được cải tiến ở version 1.5 (hiện tại có sự nâng cấp nào chưa thì các bạn cần kiểm tra lại), nhưng cách làm double-check locking vẫn chưa thể áp dụng được cho Java như C#.

• Cách làm này tuy an toàn (trên lý thuyết) với .NET framework 2.0 nhưng tôi vẫn không thích những gì trên lý thuyết, đặc biệt khi có sự nghi ngờ về tính an toàn của hệ thống.

• Ngoài ra cách này rất dễ sai, cách thiết kế trên buộc bạn phải theo chính xác từng bước như code mẫu.

• Và cuối cùng cách này vẫn chưa phải là cách hiệu quả nhất.


Fourth version – not quite as lazy, but thread-safe without using locks


public sealed class Singleton
{

static readonly Singleton instance=new Singleton();

// Explicit static constructor to tell C# compiler
// not to mark type as beforefieldinit
static Singleton()
{
}

public static Singleton Instance
{

get
{

return instance;

}

}

}



- Các bạn có thể dễ dàng thấy rằng cách làm trên đơn giản hơn 3 cách trước rất nhiều, nhưng liệu cách này có bảo đảm tính thread-safe và lazy-load như mong muốn? Tất nhiên, hàm khởi tạo static của 1 lớp C# được thiết kế để chạy khi bất kì instance nào của lớp được tạo hoặc là có một hàm static nào đó của lớp được gọi. Ngoài ra hàm khởi tạo static chỉ được gọi một lần duy nhất trong mỗi AppDomain. Tính chất này của C# sẽ giúp chúng ta kiểm tra xem một type có được khởi tạo chưa và nó sẽ tự động gọi hàm khởi tạo static bất cứ khi nào cần thiết, như vậy sẽ nhanh hơn là dùng những bước kiểm tra như những phương pháp đầu. Tuy nhiên, cách này vẫn vướng phải một số khiếm khuyết sau đây:

• Cách làm này không bảo đảm tính lazy-load. Trong trường hợp đặc biệt, nếu bạn dùng một static member thay vì dùng biến public static property Instance, lần gọi đầu tiên đến những static member này sẽ gọi hàm khởi tạo static và tạo ra instance của lớp. Nhược điểm này sẽ được khắc phục ở cách kế tiếp.

• Các hàm khởi tạo static nếu có gọi lẫn nhau sẽ bị lặp trong một vòng lặp vô tận. http://img145.imageshack.us/img145/5899/commentdz0.gif Thực sự thì tôi vẫn chưa thể reproduce được vấn đề này nên cũng không biết nhận định của tác giả là đúng hay sai, tuy nhiên tôi nghĩ chẳng ai lại đi thiết kế những constructor đệ quy gọi vòng vòng lẫn nhau như thế cả.

• Tính lazy-load trong quá trình khởi tạo đối tượng chỉ được bảo đảm trong môi trường .NET khi lớp không bị xem là “beforefieldinit”. Thật không may khi trong bộ biên dịch của C#, nó sẽ xem mọi lớp không có static constructor là beforefieldinit. Tác giả có một bài viết về điều này ở đây dành cho ai muốn tìm hiểu về chủ đề này.

- Một điểm cần lưu ý nữa với cách làm này (chỉ có cách này) là khai báo biến Instance là public static readonly. Như vậy code của chúng ta sẽ đơn giản và gọn hơn. Tuy nhiên, rất nhiều người vẫn thích sử dụng cách Property vì có thể chúng ta sẽ cần tới Property trong một số trường hợp.


Fifth version – fully lazy instantiation



public sealed class Singleton
{

Singleton()
{
}

public static Singleton Instance
{

get
{

return Nested.instance;

}

}

class Nested
{

// Explicit static constructor to tell C# compiler
// not to mark type as beforefieldinit
static Nested()
{
}
internal static readonly Singleton instance = new Singleton();

}

}



- Trong cách này, quá trình khởi tạo đối tượng được gọi ở lần truy xuất đầu tiên đến static member của lớp con, quá trình này chỉ xảy ra trong biến Instance. Như vậy cách làm này là hoàn toàn có tính lazy-load và đồng thời vẫn đảm bảo performance tốt như ở cách 4. Bạn cũng nên chú ý rằng mặc dù lớp con (lớp Nested) sẽ không thể được truy xuất từ bên ngoài lớp Singleton của chúng ta. Kĩ thuật này sẽ không gây bất kì vấn đề gì ngoài việc đoạn code của chúng ta sẽ phức tạp hơn một chút để bảo đảm tính lazy-load. http://img145.imageshack.us/img145/5899/commentdz0.gif Đối với cá nhân tôi hơi bất ngờ khi xem đến cách thứ năm này, có một chút gì đó nghệ thuật trong lập trình phải không các bạn và điều đó thể hiện rõ ở đây.

Performance vs laziness

- Trong nhiều trường hợp, bạn không thật sự cần đến lazy-load trừ khi quá trình khởi tạo của lớp có làm nhiều việc gì đó mất nhiều thời gian. Nếu không đòi hỏi những yêu cầu phức tạp, chúng ta thấy rằng cách sử dụng hàm khởi tạo static sẽ làm ứng dụng chạy nhanh hơn. Nó sẽ tăng performance cho ứng dụng vì cách làm đó cho phép trình biên dịch JIT chỉ kiểm tra một lần duy nhất để xem instance của lớp có được tạo ra hay chưa. Nếu instance Singleton của bạn được truy xuất bên trong một vòng lặp lớn, bạn có thể sẽ thấy chậm hơn đôi chút, http://img145.imageshack.us/img145/5899/commentdz0.gif tuy nhiên chúng ta nên cân nhắc khi nào cần lazy-load để áp dụng cách thích hợp nhất. Theo tôi không có cách nào tuyệt hảo nhất, chúng ta nên biết nhiều cách để áp dụng đúng nơi đúng chỗ.

Exception

- Thỉnh thoảng, một số xử lý của bạn trong hàm khởi tạo của Singleton có thể tung ra exception, và có thể làm ảnh hưởng đến toàn bộ chương trình. Rất có thể trong trường hợp gặp exception khi khởi tạo, ứng dụng của bạn sẽ thử khởi tạo đối tượng một lần nữa trong phần catch exception. Và một trong những cách có thể làm điều đó là nhờ vào kĩ thuật gọi là “type initializers”, tuy nhiên nó vẫn có thể gây nhiều vấn đề khác. Những lần chạy chương trình khác nhau có thể sẽ xử lý vấn đề này theo các cách khác nhau, tuy nhiên tôi vẫn không biết lần nào chạy đúng như yêu cầu. Và cho dù được như vậy, code của bạn vẫn có thể bị lỗi ở lần chạy khác. Để tránh chuyện này, tôi đề nghị chúng ta sử dụng cách thứ hai.

A word on performance

- Nhiều người cho rằng sử dụng locking tốn rất nhiều resource và làm cho ứng dụng chạy chậm đi. Tác giả đã thực hiện một chương trình để test thử bằng cách truy xuất biến instance của Singleton trong vòng lặp 1 tỉ lần. Tuy không khoa học lắm nhưng trong thực tế bạn có thể muốn kiểm tra xem nó nhanh chậm thế nào. Kết quả cho thấy thực sự không quan trọng. Và kết quả test với phương pháp chậm nhất (cách 2) là mất 40s để hoàn thành xong vòng lặp 1 tỉ lần. Điều này có nghĩa là bạn có thể truy xuất 25 triệu lần biến instance trong một giây, cho nên những cố gắng nâng cao hiệu xuất ứng dụng bằng cách tối ưu hóa mẫu Singleton là không đáng kể trong một số trường hợp.

- http://img145.imageshack.us/img145/5899/commentdz0.gif Theo ý kiến của Nguyễn Thoại thì cách dùng nào cũng được hết và tôi vẫn sử dụng thường xuyên cách đầu tiên. Bời vì tôi chắc rằng chương trình của tôi không sử dụng multi-thread nên chẳng có thread nào xung đột với nhau. Ngoài ra performance của ứng dụng còn phụ thuộc vào nhiều yếu tố khác nữa, chúng ta nên tìm hiểu xem nguyên nhân làm chậm ứng dụng và fix đúng chỗ. Dù sao đi nữa thì việc sử dụng cách nào là tùy ý thích của chúng ta, như tôi đã nói tôi ưu hóa chương trình khi có thể thì không lúc nào là thừa cả.

http://img145.imageshack.us/img145/5899/commentdz0.gif Tôi thường sử dụng Singleton ở đâu?
- Trong gần 3 năm làm lập trình, tôi rất thích sử dụng Singleton cho những lớp chỉ mang tính chất Utility, những lớp mà nhiều nơi có thể gọi nó. Một số lần tôi sử dụng Singleton cho một số form trong các ứng dụng desktop. Trong windows, nếu để ý các bạn sẽ thấy có một số cứa sổ chỉ hiện một lần cho dù bạn click vào shotcut của nó như thế nào. Điển hình là cửa sổ Find của notepad hay cửa sổ Run của Windowz. Form cũng là một lớp trong nhiều ngôn ngữ lập trình nên áp dụng Singleton cho nó là điều hoàn toàn bình thường, nếu bạn muốn một form chỉ xuất hiện một lần cho dù bạn gọi nó bằng cách click cả chục cái thì đấy là lúc bạn có thể áp dụng Singleton.


Run utility in Windows


Find function in notepad


- Nấu nướng từ bài viết Implementing the Singleton Pattern in C#

- Các link tham khảo khác:

Dependency Injection with Spring.Net p4

(Bài viết dịch từ bài Dependency Injection with Spring.Net của David Consdorf, thêm hành thêm tiêu rồi nấu lại bởi Nguyễn Thoại :bbpraroi:)



- Trong phần 3, tôi đã sử dụng một ví dụ khác với các ví dụ truyền thống để giải thích mẫu thiết kế Strategy Pattern. Nếu bạn muốn tìm hiểu thêm những ví dụ khác, các bạn có thể tham khảo bài viết của tác giả Dadiv Consdorf ở đây hoặc trên wiki. Như đã nói ở bài viết trước, mẫu thiết kế Strategy Pattern sẽ giúp bạn trừu tượng hóa (abstract) những xử lý (behavior) tương tự nhau của đổi tượng(object) bằng cách đưa những xử lý đó ra những lớp độc lập, và những lớp này sẽ cùng implement một interface.:bbpnen:


- Ở những ví dụ truyền thống, các bài viết thường sử dụng bài toán về các thuật toán sắp xếp để minh hoạ mẫu thiết kế Strategy. Trong đó, một lớp NumberArray sẽ có khả năng sắp xếp danh sách các số bằng nhiều thuật toán khác nhau. Các thuật toán này sẽ được đóng gói trong các lớp Sort và chính những lớp BubbleSort, HeapSort,… này lại implement cùng 1 interface ISortStrategy. Lớp NumberArray không sử dụng một thuật toán cụ thể nào để sắp xếp, nó sẽ giữ một object thuộc về kiểu ISortStrategy và các thuật toán sort sẽ được chuyển đổi dễ dàng trong lúc chương trình chạy và ngoài ra nếu sử dụng Spring.NET ta có thể thay đổi những lớp Strategy bằng file config.

- Quay lại ví dụ ở bài trước, các bạn sẽ thấy sự tương tự. Nếu xem lớp “ThuBayTuanToi” là chương trình chính thì các bạn sẽ thấy lớp NguyenThoai có thể chọn cách DiXemPhim hoặc DiAnToi một cách dễ dàng bằng cách khởi tạo với một đối tượng thuộc kiểu tương ứng. Tuy nhiên, việc này sẽ làm mất đi tính loose coupling của chương trình vì lớp “NguyenThoai” phải reference đến những lớp DiXemPhim, DiAnToi. Nói cách khác nó phải biết DiXemPhim là như thế nào, DiAnToi là như thế nào. Spring.NET sẽ giúp tôi giải quyết vấn đề đó, Spring.NET sẽ giữ tất cả những chiến thuật đi chơi của tôi, và NguyenThoai với Spring.NET chỉ cần thoả thuận với nhau các kí hiệu : “ĐiĂn”, “XemPhim”, “Cafe”,... Bản thỏa thuận như sau:


<object name="DiAnToi" type="
DiAnToi, __Code" singleton="true">
</object>

<object name="DiXemPhim" type="
DiXemPhim, __Code" singleton="true">
</object>

<object name="DiUongCafe" type="
DiUongCafe, __Code" singleton="true">
</object>


<object name="XemPhim" type="NguyenThoai, __Code" singleton="false">
<constructor-arg name="cachDiChoi" ref="DiXemPhim" />
</object>

<object name="
DiAnToi" type="NguyenThoai, __Code" singleton="false">
<constructor-arg name="
cachDiChoi" ref="DiAnToi" />
</object>

<object name="
Cafe" type="NguyenThoai, __Code" singleton="false">
<constructor-arg name="
cachDiChoi" ref="DiUongCafe" />
</object>

Hình 8: Mapping các objects trong file config.


- Như vậy ít nhất tôi với Spring.NET đã thoả thuận với nhau 3 mật hiệu, nếu cần đi chơi theo cách nào thì thôi chỉ nói cho Spring.NET biết mật hiệu và Spring sẽ lấy ra những thứ tôi cần. Cách làm như trên giúp chúng ta thay đổi các chiến thuật cần sử dụng trong lúc runtime. Còn việc map đối tượng chính với các lớp cài đặt các behavior đã được định nghĩa trong file config như hình 8. Trong phần 5 của loạt bài này, chúng ta sẽ nói thêm một ví dụ nữa về ứng dụng Spring.NET để map các đối tượng với nhau. Còn bây giờ, chúng ta hãy xem tôi sẽ sử dụng Spring như thế nào trong lần đi chơi thứ bảy tuần tới :bbpsdieu2::



public class
ThuBayTuanToi {
public void main() {
IApplicationContext ctx = ContextRegistry.GetContext()
NguyenThoai _thoai = (NguyenThoai)ctx.GetObject("XemPhim");
_thoai.Money = 100.000 VND;
_thoai.Vehicle = new Vehicle("Xe Bus");
_thoai.DanBanGaiDiChoi();
}
}

Hình 9: Sử dụng Spring.NET để lấy đối tượng được map.



- Một điều quan trọng nữa nên nói đến là việc sử dụng mẫu thiết kế Singleton bên trong Spring.NET. Bạn có thấy rằng ở những ví dụ trước đều có một property là singleton ở phần object mapping trong các file config. Theo tôi, Singleton là mẫu thiết kế đơn giản nhất, thông dụng nhất và thường được sử dụng nhất. Để hiểu rõ hơn về thiết kế này, các bạn hãy xem sơ đồ lớp bên dưới:


Singleton Pattern

Hình 10: Sơ đồ lớp của mẫu Singleton.


- Theo hình thì chúng ta thấy lớp singleton có một hàm GetInstance() để trả về instance của object Singleton. Tuy nhiên tôi thích cách sử dụng Public Property hơn :bbpxtay::



public class
Singleton {
private static Singleton _instance = null;

protected Singleton() {
}

public static Singleton Instance
{
get
{
if ( _instance == null )
_instance = new Singleton();
return _instance;
}
}
}

Hình 11: Ví dụ mẫu Singleton.



- Mẫu thiết kế singleton giúp chúng ta bảo đảm rằng lúc nào cũng tồn tại nhiều nhất 1 instance của một lớp tương ứng. Như các bạn thấy trong sơ đồ lớp, chúng ta sẽ làm được chuyện đó bằng cách thiết kế một lớp với 1 protected constructor (hàm khởi tạo), và vì là protected nên nó chỉ có thể tạo ra instance bên trong. Chúng ta nên sử dung protected ở đây mà không dung private vì khi khai báo là protected chúng ta có thể sử dụng unit test để test và những lớp con có thể kế thừa constructor này. Với cách làm như vậy, không một chỗ nào trong chương trình có thể tạo ra instance của lớp. Do đó, sẽ chỉ có 1 instance duy nhất của đối tượng được tạo ra và đối tượng đó sẽ tồn tại trong xuốt quá trình chạy của chương trình.


- Lớp singleton sẽ phải cung cấp một public static Property hoặc một hàm static với kiểu trả về là chính lớp đó để các đối tượng khác có thể truy xuất được object mà nó đóng gói bên trong. Mấu thiết kế này có vẻ như rất giống với cách chúng ta sử dụng những hàm static, nhưng nó khác biệt ở chỗ singleton cho chúng ta sự linh động hơn trong lập trình vì chúng ta vẫn có một instance thực sự của một đối tượng để làm việc. Ngoài ra có một số điểm khác biệt khác mà tôi sẽ đề cập đến trong bài viết khác. Việc áp dụng mẫu singleton đúng chỗ đúng lúc trong ứng dụng rất có lợi, chương trình của bạn sẽ chạy nhanh hơn vì không cần phải liên tục tạo ra những đối tượng mới và cũng không cần huỷ những đối tượng sau khi đã sử dụng xong. Ngoài ra cũng sẽ tiết kiệm được bộ nhớ của máy tính vì không cần phải lưu trữ nhiều đối tượng trong hệ thống. Ngày nay, lợi ích thứ hai này không thể hiện rõ vì bộ nhớ máy tính của chúng ta không ngừng được tăng lên với giá ngày càng rẻ, nhưng theo tôi tối ưu hoá ứng dụng khi có thể là không bao giờ thừa nhất là khi chúng ta áp dụng design pattern :bbpnodo:

- Spring.NET đã sử dụng nguyên tắc này của mẫu singleton bằng cách cho phép người dùng quyết định xem 1 lớp có duy nhất một instance hay không. Vì vậy, trong ví dụ trên, lớp NguyenThoai không cần phải là singleton, bởi vì sao? Bởi vì thường thì tình hình tài chính và tâm trạng tôi hoàn toàn khác nhau ở mỗi ngày thứ 7. Tôi sẽ có nhiều lần đi chơi khác nhau và với mỗi lần ấy có thể tôi sẽ có số tiền khác nhau. Ngoài ra tôi là một người rất chung thuỷ, tôi thích đến những chỗ cũ, ăn ở những quán ăn cũ nên theo tôi những lớp DiAnToi, DiXemPhim, … nên là những lớp singleton. Ngoài nhiệm vụ thực hiện quá trình đi chơi và biến IsHappy của tôi thành true thì nó chẳng nên có vai trò gì khác. Tương tự, ở ví dụ của tác giả với những thuật toán sort, những lớp BubleSort, HeapSort, QuickSort,... cũng nên là singleton vì ngoài chuyện sắp xếp một mảng các số, nó chẳng cần giữ giá trị sau khi sắp xếp của mảng làm gì. Do đó mình chỉ cần khởi tạo một lần và sử dụng suốt cho những lần sau.

- Hi vọng kết thúc phần 4 này, các bạn có thể hiểu được đôi chút về Spring.NET, hiểu được Spring có thể giúp các bạn làm gì và vì sao nên làm như vậy. Nói chung khi thiết kế lớp cho chương trình, nếu bạn gặp phải tình huống một lớp A có giữ 1 instance của lớp B thì bạn đang đối mặt với tính loose coupling. Lúc đấy, bạn nên nghĩ xem sau này có khi nào cần phải làm một lớp B2 tương tự lớp B hay không. Nêú có, chính là lúc bạn cần nghĩ đến Dependency Injection và Spring là một trong những lựa chọn để giải quyết bài toán của bạn. Để giúp bạn hiểu rõ hơn về khả năng mapping objects của Spring, ở phần 5, tác giả sẽ nói đến một ví dụ khác lý thú hơn, thông dụng và thực tế hơn trong việc áp dụng Spring.NET để kết nối các đối tượng với nhau.

Phần 1

Phần 2

Phần 3

Phần 5

Phần 6

(Các bạn đang xem phần 4)


rss
 

About Me

Place I've live
Near Bossley Park, Sydney, NSW, Australia
Place I've work
  • Freelancer (from 06/2010 to present)
  • Harvey Nash (from 05/2008 to 06/2010)
  • DataDesign Vietnam (10/2005 to 04/2008)
Place I've studied
  • University of Natural Science (Bachelor of Science HoChiMinh City Vietnam From 2001 to 2005)
  • Le Hong Phong High School (HoChiMinh City Vietnam From 1997 to 2000)