Một câu chuyện về Bridge Pattern
Posted On Friday, May 30, 2008 at at 5:16 PM by Unknown- Mẫu thiết kế Bridge được thiết kế với ý tưởng tách rời những xử lý của một lớp ra lớp khác, từ đó có thể dễ dàng biến đổi hoặc thay thế mà không làm ảnh hưởng đến những nơi có sử dụng lớp ban đầu. Điều này có thể hiểu như sau: bạn thiết kế một lớp với rất nhiều xử lý, bây giờ bạn không muốn để những xử lý đó trong lớp của bạn nữa, bạn sẽ tạo ra một lớp mới và move toàn bộ những xử lý đó sang lớp mới. Khi đó trong lớp cũ sẽ giữ một đối tượng thuộc về lớp mới, và đối tượng này sẽ chịu trách nhiệm xử lý thay cho lớp ban đầu. Tại sao chúng ta làm như vậy và cách thực hiện như thế nào? Hi vọng với bài viết này chúng ta tìm được lời giải thích cho 2 câu hỏi trên.
- Mẫu Bridge về khía cạnh nào đó khá giống với mẫu Adapter ở chỗ: người ta sẽ nhờ vào một lớp khác để thực hiện một số xử lý nào đó. Tuy nhiên, ý nghĩa và mục đích sử dụng của hai mẫu thiết kế này hoàn toàn khác nhau. Mẫu Adapter Pattern hay còn gọi là Wrapper pattern được dùng để biến đổi một lớp/interface sang một dạng khác có thể sử dụng được. Adapter Pattern giúp nhiều lớp có thể làm việc với nhau dễ dàng mà bình thường không thể. Một trường hợp tôi gặp phải và có thể áp dụng Adapter Pattern là khi tôi không thể kế thừa lớp A nhưng muốn làm một lớp B có những xử lý tương tự như lớp A. Khi đó tôi làm lớp B như sau, các xử lý của B sẽ gọi những xử lý của A khi cần
Hình 1: Ví dụ về mẫu Adapter
- Đó chỉ là một trong nhiều cách sử dụng có thể có của mẫu Adapter, tôi sẽ nói thêm về Adapter trong một bài viết khác, ở đây chỉ nêu ra ý nghĩa khác nhau của hai mẫu thiết kế này. Trước khi đi vào câu chuyện của mẫu Bridge, chúng ta hãy xem sơ đồ lớp của mẫu Bridge:
Hình 2: Sơ đồ lớp của mẫu Bridge
Câu chuyện về mẫu thiết kế Bridge (Bridge Pattern)
- Có một người đã làm nghề đầu bếp đã 20 năm. Ông ta đã đi rất nhiều nơi trên thế giới, làm việc cho rất nhiều nhà hàng khác nhau và đã học được cách nấu rất nhiều món ngon. Không những thế ông ta còn sáng tạo ra những món đặc biệt cho riêng mình nữa. Nhờ tài nghệ của ông, các nhà hàng nơi ông làm luôn luôn đông khách. Sau 20 năm nhìn lại, ông thấy đã đến lúc mình phải mở nhà hàng cho riêng mình. Đã để dành được một số vốn kha khá, ông quyết định chọn một trong những thành phố năng động của Việt Nam là TPHCM để bắt đầu sự nghiệp riêng. Thế là nhà hàng của ông ta ra đời và kinh doanh khá phát đạt nhờ vào danh tiếng và tài nghệ của mình. Ông ta dành hết tâm huyết để tự tay nấu mọi món ngon nhất cho các thực khách vì không tin tưởng vào các phụ bếp được thuê. Càng ngày danh tiếng của nhà hàng càng được nhiều người biết đến, không chỉ những người sành ăn trong nước mà cả những du khách nước ngoài cũng tìm đến nhà hàng này để thưởng thức khi có cơ hội ghé đến TPHCM. Ông ta rất vui mừng trước doanh thu ngày càng cao nhưng bên cạnh đó cũng rất lo cho sức khỏe của mình, vì ông phải đứng chế biến món ăn từ 6h sang đến 11h đêm hằng ngày
- Các con của ông rất thương bố mình và khuyên ông nên truyền nghề lại cho các học trò, các người ấy sẽ giúp ông làm việc cho nhà hàng. Suy nghĩ khá lâu, ông thấy rằng các con ông nói rất có lý. Ông ta chọn 1 đứa học trò ngoan và giỏi nhất truyền toàn bộ những gì mình biết. Sau khi đã lĩnh hội được toàn bộ khóa học, đứa học trò đã phụ ông nấu nướng cho nhà hàng, nhờ vậy ông được rảnh rổi về sớm đi tập thể dục thẩm mỹ và làm những việc quản lý, mở rộng kinh doanh.
- Thời gian trôi qua, ông muốn mở thêm một nhà hàng mới ở HN. Cũng trong giai đoạn này, ông nhận được email của một số khách ruột bảo rằng chất lượng món ăn bắt đầu kém đi, không đậm đà như trước đây. Sau khi tìm hiểu, ông phát hiện nguyên nhân là do thằng học trò đểu cáng đang âm mưu mở nhà hàng riêng nên nó lơ là việc nấu nướng , ngoài ra do ông dạy nó nhiều quá nên nó chỉ có thể nấu ngon được một số món ăn, phần lớn các món khác nấu không đạt yêu cầu. Rút kinh nghiệm, ông quyết định đăng báo dân trí tìm một số cô gái thích nấu nướng truyền cho mỗi cô 1 kĩ thuật nấu món ngon các miền. Sau này nếu có ý định mở nhà hàng ở miền Trung cũng sẽ có đầu bếp chuyên nấu món cay hợp khẩu vị của dân địa phương.
Hình 3: Bản kế hoạch mở 2 nhà hàng mới của ông
// Bridge pattern -- Structural example
using System;
namespace nthoai.blogspot.com.BridgePattern
{
// "Abstraction"
class NhaHang
{
protected DauBep _dauBep;
// Property
public DauBep DauBep
{
set { _dauBep = value; }
}
public virtual void CheBienMonAn()
{
_dauBep.CheBienMonAn();
}
}
// "Implementor"
abstract class DauBep
{
public abstract void CheBienMonAn();
}
// "Nhà hàng món Huế"
class NhaHangMonHue : NhaHang
{
public override void CheBienMonAn()
{
_dauBep.CheBienMonAn();
}
}
// "Nhà hàng món miền Nam"
class NhaHangMienNam : NhaHang
{
public override void CheBienMonAn()
{
_dauBep.CheBienMonAn();
}
}
// "ConcreteImplementorA"
class DauBepMonAnHue : DauBep
{
public override void CheBienMonAn()
{
Console.WriteLine("Đây là những món ăn Huế do đầu bếp Huế thực hiện");
}
}
// "ConcreteImplementorB"
class DauBepMonAnMienNam : DauBep
{
public override void CheBienMonAn()
{
Console.WriteLine("Đây là những món ăn miền Nam do đầu bếp miền Nam thực hiện");
}
}
// Ông đầu bếp mở nhà hàng như sau
class ÔngĐầuBếpGià
{
static void Main()
{
NhaHang _nhaHangMonHue = new NhaHangMonHue();
// Set implementation and call
_nhaHangMonHue.DauBep = new DauBepMonAnHue();
_nhaHangMonHue.CheBienMonAn();
NhaHang _nhaHangMonMienNam = new NhaHangMienNam();
// Change implemention and call
_nhaHangMonMienNam.DauBep = new DauBepMonAnMienNam();
_nhaHangMonMienNam.CheBienMonAn();
// Wait for user
Console.Read();
}
}
}
Hình 4: Kế hoạch mở nhà hàng :D
- Câu chuyện của ông đầu bếp già là một ví dụ về cách sử dụng của Bridge khi đưa những xử lý sang một lớp khác. Trong các tài liệu về mẫu Bridge, các tác giả thường sử dụng các thuật ngữ như: Abstraction, Implementor, RefinedAbstraction và ConcreteImplementor. Cá nhân tôi thấy những từ vô cùng chuyên môn như vậy hơi khó hiểu, tôi thích những ví dụ thực tế hơn, tuy nhiên những từ ấy hoàn toàn chính xác. Sử dụng mẫu Bridge trong lập trình không chỉ đơn giản là đưa những xử lý của một lớp sang lớp khác rồi gọi nó từ lớp mới, mọi việc không chỉ đơn giản như vậy. Ý tưởng trên chỉ là một vế của Bridge Pattern, ở đây chúng ta chưa nói đến vế: “từ đó có thể dễ dàng biến đổi hoặc thay thế mà không phải ảnh hưởng đến những nơi có sử dụng lớp ban đầu” (so you can vary or replace the implementation without changing the client code). Xin thưa với các bạn đây chỉ là cách diễn giải của tôi, không phải khái niệm hay định nghĩa tiếng Việt của Bridge Pattern cho nên nếu các bạn muốn xem nguồn tiếng Anh hãy chịu khó google. Quay lại chỗ “biến đổi, thay thế mà không ảnh hưởng”, tôi đã có viết về chuyện này ở loạt bài về Dependency Injection của Spring.NET. Trong trường hợp của mẫu Bridge, chúng ta có thể giảm sự phụ thuộc giữa các lớp với nhau bằng cách sử dụng Interface hoặc abstract class. Trong các tài liệu về Bridge Pattern, các Abstraction và Implementer là những lớp abstract hoặc interface. Trong khi đó, các RefinedAbstraction và ConcreteImplementor là những thể hiện cụ thể hay những lớp con của các abstract class/ interface nói trên. Như vậy trong câu chuyện ông đầu bếp của chúng ta thì Abstraction là NhàHàng, RefinedAbstraction chính là Nhà Hàng Món Huế, Nhà Hàng Món Nướng Nam Bộ, Nhà Hàng Châu Âu còn ConcreteImplementor là các đầu bếp: Đầu Bếp Món Âu, Đầu Bếp Món Huế, …
- Như vậy chữ Bridge (chiếc cầu) ở đây là quan hệ “use/have” giữa Abstraction và Implementation, giữa Nhà Hàng và Đầu Bếp. Một RefinedAbstraction sẽ không khai báo cụ thể một ConcreteImplementor nào đang được sử dụng mà chỉ biết nó đang có 1 Implementor nào đó; khi mở một Nhà Hàng mới ta sẽ mướn Đầu Bếp (biết nấu ăn), còn cụ thể đầu bếp nấu được món miền nào sẽ quyết định lúc chọn xong địa điểm
- Trong một số trường hợp, mẫu thiết kế Bridge được dùng như một cầu nối giữa 2 nhóm đối tượng khác nhau, giữa nhóm các dữ liệu và nhóm các cách thể hiện ra màn hình, đó là ví dụ thường được dùng trong các bài viết về mẫu Bridge. Như chúng ta biết có rất nhiều chuẩn hình ảnh như JPG, PNG, BMP, GIF, … và các hệ điều hành khác nhau như Windows, MacOS, UNIX đều hiểu và có thể hiển thị các định dạng ảnh này.
- Cấu trúc hình ảnh và cách hiển thị chúng là hai thành phần quan trọng của một định dạng ảnh. Cấu trúc hình chính là cách mà hình đó được lưu trữ, và cách thể hiện chúng sẽ tương đối khác nhau trên các chương trình xem hình trên các hệ điều hành khác nhau. Trong trường hợp này các file hình với các định dạng khác nhau sẽ thường xuyên thay đổi, và các hệ điều hành cần vẽ hình cũng nhiều tương tự. Nếu được yêu cầu thiết kế lớp cho bài toán trên sao cho tính tái sử dụng cao nhất, bạn sẽ làm thế nào. Như đã nói, có rất nhiều chuẩn hình, mỗi hình sẽ có những thông tin riêng của nó và mỗi hình nên được thiết kế để có khả năng tự vẽ nó (Information Expert) . Nhưng vì cách vẽ sẽ khác nhau trên các hệ điều hành khác nhau nên nếu chúng ta thiết kế lớp hình tự thực hiện hàm vẽ rồi khi cần thì sửa hàm vẽ để nó hoạt động được trên hệ điều hành khác là không nên. Bridge Pattern sẽ giúp ta đưa những hàm vẽ đó sang những lớp khác để vẽ trên các hệ điều hành tương ứng. Nhờ vậy, khi một chuẩn hình mới được tạo ra hay có một thuật toán vẽ khác được nghĩ ra, sẽ rất dễ dàng để ta code thêm một lớp vẽ mới cho chương trình.
- Một ví dụ khác cũng tương tự là khi bạn cần xây dựng một ứng dụng thể hiện ra màn hình thông tin khác nhau với những loại user khác nhau. Cùng một dữ liệu nhưng nếu là Admin bạn sẽ cho phép hiển thị chi tiết hơn, có thêm những button cho phép edit hoặc delete chẳng hạn. Trong khi đó user thường hoặc guest chỉ có thể xem mà không thể làm gì khác. Còn rất nhiều trường hợp khác mà chúng ta có thể dùng Bridge cũng như có nhiều mẫu thiết kế tương tự Bridge như Strategy, Adaptor. Nếu chúng ta nắm được ý nghĩa, sự khác nhau và khi nào nên áp dụng những thiết kế này của 4 lão tiền bối thì công việc lập trình sẽ hứng thú, đầy tính sáng tạo và không nhàm chán chút nào. Hi vọng đến đây chúng ta đã tìm được câu trả lời cho 2 câu hỏi trên. Tôi sẽ bàn thêm một chút về những lợi ích và trở ngại khi sử dụng Bridge:
Benefits in using Bridge Pattern
1/ Đặt vấn đề: chúng ta có một lớp A, và các lớp A1, A2, A3 kế thừa từ lớp A. Các lớp A1, A2, A3 sẽ được thừa hưởng các attribute và behavior của lớp A. Như vậy vô tình các xử lý của lớp A1, A2, A3 sẽ y như A trừ khi chúng phải override lại. Trong chương trình, khi cần sử dụng một trong các lớp A1, A2, A3 người ta thường khai báo chung chung là A thay vì khai báo cụ thể là ta sẽ gọi mày đó A1, A2, A3. Cách sử dụng lớp A ta gọi là abstraction hay là trừu tượng hóa và những xử lý của lớp A mà các A1, A2, A3 thừa hưởng gọi là implementation. Sử dụng thiết kế Bridge sẽ giúp chúng ta giảm sự phục thuộc giữa abstraction và implementation. Tính kế thừa trong hướng đối tượng thường gắn chặt abstraction và implementation lúc build chương trình. Bridge Pattern có thể được dùng để cắt đứt sự phụ thuộc này và cho phép chúng ta chọn implementation phù hợp lúc runtime.
2/ Giảm số lượng những lớp con không cần thiết. Một số trường hợp sử dụng tính inheritance sẽ tăng số lượng subclass vô tội vạ. Ví dụ trường hợp chương trình xem hình trên các hệ điều hành khác nhau, ta có 6 loại hình và 3 hệ điều hành. Sử dụng inheritance trong trường hợp này sẽ làm ta thiết kế 18 lớp trong khi áp dụng Bridge sẽ giảm số lượng lớp xuống 9
3/ Lợi ích thứ 2 dẫn đến một hệ quả: Code sẽ gọn gàn hơn và dẫn đến kích thước ứng dụng sẽ nhỏ hơn.
4/ Các Abstraction và Implementation của nó sẽ dễ dàng thay đổi lúc runtime cũng như khi cần thay đổi thêm bớt trong tương lai. Như vậy chương trình của chúng ta cũng sẽ dễ bảo trì hơn.
5/ Tương tự như lợi ích 4, nếu sử dụng Bridge, hệ thống sẽ dễ mở rộng về sau. Đây hầu như là một yêu cầu bắt buộc đối với các chương trình lớn. Nhiều công ty sẽ cùng làm trong từng giai đoạn phát triển. Một số công ty phần mềm ở VN thường làm outsource: maintain hoặc mở rộng một framework hay một chương trình đã được làm sẵn ở nước ngoài. Công ty khách hàng sẽ yêu cầu chúng ta thêm module cho ứng dụng có sẵn nhưng không được sửa đổi framework/ứng dụng có sẵn của họ vì các framework/ứng dụng đó có thể được công ty nâng cấp lên version mới. Đó là bài toán các bạn sẽ gặp phải khi làm ở những project trung bình và lớn, và ta sẽ vô cùng bực mình và cảm thấy khó khăn khi framework/chương trình đó được đám lập trình viên ở đâu đâu code vô cùng chuối giờ lại bắt mình viết thêm vào
6/ Những nơi cần sử dụng các abstraction sẽ hoàn toàn độc lập với các implementation.Ví dụ các lớp khác của chương trình xem ảnh sẽ độc lập với thuật toán vẽ ảnh trong các implementation. Như vậy ta có thể update chương trình xem ảnh khi có một thuật toán vẽ ảnh mới mà không cần phải sửa đổi nhiều thậm chí build lại toàn chương trình nếu có dùng các kĩ thuật Dependency Injection.
1 Drawbacks in using Bridge Pattern
- Khi sử dụng Bridge Pattern, chúng ta đã tăng số lần gọi gián tiếp lên hai. Trong ví dụ trên, với cách làm cũ, lớp Image sẽ thực hiện hàm vẽ của chính nó. Nếu áp dụng Bridge, hàm vẽ sẽ được gọi thông qua một lớp implementation tương tứng như vậy có thêm một lần khởi tạo đối tượng và gọi đối tượng, có thêm một lần hủy đối tượng và lượng bộ nhớ bị chiếm để chạy chương trình sẽ tăng lên một chút. Theo tôi những bất lợi này vô cùng không đáng kể so với những lợi ích mang lại khi chúng ta áp dụng Design Pattern nói chung và Bridge Pattern nói riêng.
-----------------------------------------------------------------
- Cuối cùng, ông đầu bếp già của chúng ta sẽ không có thời gian đi massage hay đi chơi gôn nếu cứ tiếp tục tự nấu ăn. Nhà hàng của ông sẽ rất khó khăn khi đầu bếp chính nghỉ việc nếu không có nhiều đầu bếp giỏi chuyên môn. Thử tưởng tượng trong thực tế chúng ta mua một chiếc xe gắn máy mà khi hư thì phải bỏ đi và mua xe mới thay vì có phải tìm phụ tùng thay thế. Tính độc lập, chuyên môn hóa giữa các bộ phận trong một hệ thống luôn được đề cao trên mọi lĩnh vực trong cuộc sống. Riêng trong thiết kế phần mềm, design pattern là những bài học kinh nghiệm mà khi áp dụng tốt sẽ biến người lập trình thành một nghệ sĩ. Một lần nữa, tôi hi vọng bài viết này không chỉ giúp các bạn biết Bridge Pattern là gì mà còn hiểu được tại sao và khi nào cần nó. Tóm lại, cách sử dụng inheritance một cách máy móc có thể vô tình dán chặt những abstraction và implementation với nhau trong khi một số ứng dụng cần điều ngược lại. Bridge Pattern có thể được sử dụng khi một abstraction có nhiều implementation và cả 2 nhóm có thể có nhiều thay đổi mà không phụ thuộc hoặc ảnh hưởng gì nhóm còn lại
Các tài liệu tham khảo:
Bridge Design Pattern in C# and VB.NET
http://www.informit.com/articles/article.aspx?p=30297
http://www.c-sharpcorner.com/UploadFile/rajeshvs/BridgePattern
http://en.wikipedia.org/wiki/Bridge_pattern
http://www.codeguru.com/cpp/cpp/cpp_mfc/patterns/article.php/c817/
http://en.wikipedia.org/wiki/Wrapper_pattern
http://www.dofactory.com/Patterns/PatternAdapter.aspx
Sử dụng Singleton Pattern trong C#
Posted On Tuesday, May 27, 2008 at at 8:18 PM by Unknown- 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ê .
- 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 . 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 , 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 " "
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”. 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. 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
- 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. 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 .
• 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 ( đâ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.
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à. 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. 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. Đố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, 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.
- 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ả.
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.
- 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 p6
Posted On Friday, May 23, 2008 at at 10:32 PM by Unknown(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 )
- Testing with Dependency Injection
- Một điểm thú vị nữa của Spring.NET là chúng ta có thể sử dụng Unit Test để test các component của chúng ta rất dễ dàng. Với khả năng thay đổi mối liên kết giữa các thành phần trong ứng dụng, người lập trình có thể tạo ra những “đối tượng giả” để test, vì vậy ta có thể test đoạn code họ đang viết mà không cần lo lắng đến những phần chưa hoàn thành của hệ thống. Điều này có thể lạ với một số bạn vì khái niệm unit testing không được dạy ở các trường đại học, đến khi đi làm ở một số công ty các bạn sẽ thường xuyên gặp khái niệm này. Ý tưởng cơ bản là những hàm, những đoạn code các bạn viết ra đều phải có một phần code khác để test ngay những hàm bạn tạo. Giả sử bạn viết một lớp tính điểm trung bình các môn Toán, Lý, Hóa của học sinh, trong lớp (class) của bạn sẽ gọi đến một lớp Utility tính trung bình của một người khác trong nhóm viết. Vấn đề bạn gặp phải là lớp Utility đó chưa được viết xong, như vậy không thể nào test được. Nhưng rất may là đã có Spring.NET sẽ giúp chúng ta gỡ rối vấn đề này.
- Một ví dụ khác, khi bạn đang viết một số lớp dịch vụ (service – BLL) cho ứng dụng nhưng tầng dữ liệu (DAL) vẫn chưa hoàn thành; hoặc có khi bạn đang viết phần mềm cho một khách hàng nước ngoài rất khó tính và họ không cho bạn truy cập vào server database của họ; với Spring.NET và Dependency Injection, bạn vẫn có thể test thử các lớp dịch vụ bằng cách tạo ra các Mock objects để điền dữ liệu chưa lấy được từ database mà không cần đến những đối tượng ở tầng dữ liệu, hay nói cách khác bạn cũng không cần đến database để test. Mock là một kĩ thuật lí thú để test ứng dụng rất linh động và tôi hi vọng có dịp sẽ chia sẽ những hiểu biết của mình với các bạn ở những bài viết sau. Trong phạm vi bài viết này tôi chỉ có thể giới thiệu Mock như là một framework giúp chúng ta tạo ra những object giả với những hàm hoạt động theo cách mà ta muốn. Trong trường hợp này nó sẽ tạo ra những object giả ở tầng dữ liệu và giả vờ như đã kết nối với database và trả về dữ liệu cho những lớp ở tầng dịch vụ. Sau này, khi chúng ta cần kết nối tầng dịch vụ với tầng dữ liệu, bạn chỉ cần thay đổi file cấu hình của Spring.
- Ngoài những điều kể trên, có một số kĩ thuật khác rất lí thú mà Spring.NET hỗ trợ bao gồm cả hệ thống test của chính Spring.NET. Vì vậy nếu bạn muốn tìm hiểu hãy đọc những bài viết khác liên quan đến vấn đề này ở trang web www.springframework.net
- More Spring.NET
- Conclusion
- Code Examples
- About the Author
- Work Experience
- Education
- About the Translator : Click here
(Các bạn đang xem phần 6)
Dependency Injection with Spring.Net p5
Posted On at at 4:12 PM by Unknown(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 )
- Như ở giới thiệu phần trước, trong phần này chúng ta sẽ xem một ví dụ khác về cách áp dụng phổ biến của dependency injection khi chương trình được tách thành nhiều tầng và các tầng này được kết nối với nhau bằng Spring.NET
Quick N-Tier Example
- Bất cứ ai từng làm việc với những ứng dụng khá lớn có thể đã biết đến kiến trúc N-tier. Ý tưởng cơ bản là ứng dụng được chia thành nhiều tầng chức năng. Mỗi tầng đảm nhiệm một chức năng nào đó và chỉ liên lạc với những tầng ở phía trên hoặc dưới nó. Ví dụ dễ gặp nhất là kiến trúc 3 tầng (lúc đi học được dạy là mô hình 3 lớp) như sau:
Hình 12: Mô hình 3 lớp
- Trong ví dụ này, tầng trên cùng là tầng giao diện (presentation/view) làm nhiệm vụ hiển thị dữ liệu trên trang web, trên một ứng dụng client, một ứng dụng di động, … Kế tiếp, ta có một tầng dịch vụ (Business Logic Layer – BLL hay Service layer) làm nhiệm vụ xử lý tính toán những logic của ứng dụng. Và cuối cùng là tầng tầng dữ liệu (DAL - Data Access Layer) chịu trách nhiệm giao tiếp với cơ sở dữ liệu (database), XML hoặc bất kì loại datasource nào mà chương trình có thể sử dụng. Mỗi tầng chỉ giao tiếp duy nhất với tầng khác thông qua một interface, mỗi tầng sẽ không biết các tầng khác hoạt động như thế nào và được lập trình như thế nào.
- Ví dụ tầng giao diện sẽ gọi tầng dịch vụ để tạo ra một business object để giữ những gì người dùng nhập vô, sau đó tầng dịch vụ sẽ tạo ra một đối tượng dựa trên những gì nhập vào, tính toán gì đó nếu cần thiết và tiếp tục gọi tầng dữ liệu để lưu trữ những thông tin này. Tầng dịch vụ không cần quan tâm tầng giao diện đang hiển thị dữ liệu trên trang web hay trên một windows application, nói cách khác nó không quan tâm ứng dụng thuộc loại nào, và nó cũng chẳng cần quan tâm làm thế nào tầng tầng dữ liệu lưu thông tin xuống dưới, nó cũng không quan tâm ứng dụng đang sử dụng loại cơ sở dữ liệu nào. Những cách làm này giúp chúng ta dễ dàng thay đổi chương trình để thích hợp cho nhiều loại người dùng khác nhau, trên desktop, trên web hoặc trên thiết bị di động đều được. Thêm vào đó khi có cần thay đổi loại cơ sở dữ liệu, chúng ta chỉ cần viết lại hoặc thay thế (nếu có sẵn) tầng dữ liệu. Nếu có một tầng nào đó cần thay đổi thì chỉ có tầng đó phải sửa đổi và những tầng khác có thể được sử dụng lại
- Spring.Net và Dependency Injection giúp chúng ta xây dựng những ứng dụng nhiều tầng bằng cách chia ứng dụng ra thành nhiều component. Với Spring.Net, chúng ta có thể giữ cho những components trong 1 tầng độc lập với những tầng khác và có thể inject dependency vào những tầng khác dễ dàng nhờ vào file cấu hình xml.
Các bạn hãy tham khảo đoạn code ví dụ cách liên kết một đối tượng từ tầng dịch vụ với một đối tượng từ tầng tầng dữ liệu:
public interface IServiceExample {
void doOperationOnObject(Object obj);
Object getObject(int objectID);
void saveObject(Object obj);
}
public class ServiceExample : IServiceExample {
public IDAOExample _daoExample;
public ServiceExample(IDAOExample daoExample) {
_daoExample = daoExample;
}
public void doOperationOnObject(Object obj) {
// Enter business logic for operation here
}
public Object getObject(int objectID) {
// Use DAO to retrieve object from the database
return _daoExample.getObject(objectID);
}
public void saveObject(Object obj) {
// Use DAO to save object to the database
_daoExample.saveObject(obj);
}
}
public interface IDAOExample {
Object getObject(int objectID);
void saveObject(Object obj);
}
public class DAOExample : IDAOExample {
public DAOExample() {
}
public Object getObject(int objectID) {
// Put database code to retrieve object here
}
public void saveObject(Object obj) {
// Put database code to save object here
}
}
Hình 13: Services & DAO objects
<object name="ServiceExample" type="ServiceExample, __Code" singleton="true">
<constructor-arg name="daoExample" ref="DAOExample" />
</object>
<object name="DAOExample" type="DAOExample, __Code" singleton="true">
</object>
Hình 14: Services & DAO mappings
- Trong ví dụ trên, bạn có một đối tượng thuộc tầng dịch vụ giao tiếp với một đối tượng thuộc tầng dữ liệu để lưu thông tin của một object xuống database. Đối tượng DAO được nhúng vào bên trong đối tượng tầng dịch vụ nhờ Spring.NET. Bằng cách này, sẽ không có sự liên kết cụ thể nào trong code giữa các tầng (layers).
- Nếu một ngày đẹp trời nào đó khách hàng yêu cầu bạn đổi từ MySQL sang sử dụng Oracle, tất cả những gì bạn cần làm là tạo mới những lớp DAO cho Oracle, sau đó sửa file config của Spring.NET sang Oracle DAO object. Không có dòng nào trong code của tầng dịch vụ (BLL) cần phải thay đổi hay build lại.
- Bạn cũng nên chú ý là trong trường hợp này, tầng giao diện (Presentation) khi sử dụng những đối tượng dịch vụ cũng có thể nhờ Spring.NET để lấy; cũng tương tự như những đối tượng ở tầng dịch vụ sử dụng Spring.NET để gọi các đối tượng ở tầng dữ liệu. Một điều cần chú ý nữa là các đối tượng ở tầng dịch vụ và tầng dữ liệu đều là singletons bởi vì chúng được sử dụng để tính toán và lưu trữ trong chương trình, chúng chẳng cần giữ lại dữ liệu gì cả. Hiện nay, có rất nhiều framework .NET hỗ trợ các bạn sẵn tầng DAL với các chức năng có sẵn như lazy load, caching, optimized queries,… Các bạn chỉ cần viết một số store procedure hoặc config một chút xíu là có thể sử dụng được mà không cần viết code nhiều. Các framework đó cũng rất linh hoạt khi cần phải chuyển đổi loại database. Hiện có mốt số framework rất nổi tiếng như Ibatis, NHibernate, Active Record, LinQ,… tùy vào nhu cầu và thói quen bạn có thể lựa chọn framework thích hợp nhất cho mình. Nếu người là người cổ điển có thể NHibernate thích hợp với bạn, còn nếu là dân sành điệu thích thời trang thì LinQ là lựa chọn tốt .Các bạn có thể tìm hiểu thêm thông tin về các Object-relational mapping (O/R Mapping) framework từ đây, theo tôi đó là một trong những bài viết rõ ràng súc tích và dễ hiểu nhất.
Dependency Injection with Spring.Net p4
Posted On Thursday, May 22, 2008 at at 4:47 PM by Unknown(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 )
- 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.
- Ở 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 :
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:
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 :
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
- 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.
(Các bạn đang xem phần 4)