XÂY DỰNG FRAMEWORK LOOKUP BẰNG IOC

Lâu rồi không post bài lên, cũng chưa biết viết gì. Thôi thì post bài article trước đây của mình. Mong các bạn góp ý.

XÂY DNG FRAMEWORK LOOKUP BẰNG IOC

Nguyễn Minh Dũng

8-2005

Đặt vấn đề

Trong bất cứ hệ thống enterprise nào, việc lấy dữ liệu từ các external resource (file, …) là rất quan trọng. Ví dụ: một hệ thống quản lý với giao diện cây sẽ cần lấy các icon để thể hiện cho các node trên cây. Một chương trình giao diện GUI luôn cần hiển thị các thông báo lỗi cho user. Các nguồn dữ liệu này thường lưu trong file (phổ biến là xml). Do đó, các hệ thống sẽ có các module thực hiện việc lookup các nguồn dữ liệu này. Thông thường các module này nằm rải rác, nếu quy trình thiết kế không tốt, sẽ rất khó quản lý. Do đó, bài viết này hướng dẫn cách xây dựng 1 framework nhỏ, giúp quản lý các module lookup. Framework cho phép developer tạo ra module lookup theo yêu cầu nghiệp vụ và plug vào framework. Sau đó sử dụng framework để invoke các chức năng của module lookup.

IoC (Inversion of Control) là một khái niệm nghe có vẻ mới lạ nhưng thực ra nó đã có từ lâu dưới cái tên DIP (Dependency Inversion Principle). Ngoài ra, IoC còn có 1 cái tên khác là Dependency Injection (DI). Trong lập trình OOP, nguyên tắc cơ bản và quan trọng nhất khi thiết kế class là nguyên tắc OCP (Open-Closed Principle). Và IoC là phương pháp để ta đạt được nguyên tắc OCP (mặc dù, gần như không thể đạt được OCP 100%).

Bài viết này sẽ trình bày về OCP, IoC – 2 trong những nguyên tắc then chốt khi thiết kế class và sẽ sử dụng phương pháp này để xây dựng 1 framework lookup như một ví dụ minh họa. Trong bài viết, mã nguồn minh họa sẽ là C#.

Open-Closed Principle

Có thể nói, OCP là nguyên tắc quan trọng nhất trong class design. Ý tưởng của nó như sau: Một module phải có tính mở (open) khi mở rộng (extension) nhưng phải đóng (closed) khi thay đổi. Nghĩa là khi thay đổi module, ta sẽ mở rộng nó chứ không thay đổi mã nguồn. Điều này nghe có vẻ mâu thuẫn, nhưng trong OOP, abstraction là chìa khóa để thực hiện OCP. OCP là mục đích cần đạt được trong OOP và IoC là phương pháp để đạt được điều đó.

Chúng ta sẽ lấy một ví dụ minh họa cho OCP. Giả sử hệ thống cần xây dựng là 1 ứng dụng quản lý tài nguyên với giao diện dạng cây. Trên cây có các nút sẽ hiển thị cấu trúc của các tài nguyên theo kiểu folders-files. Do có nhiều loại folder và file nên mỗi nút sẽ có 1 icon riêng tương ứng cho từng loại tài nguyên. Giả sử hiện tai ở giai đoạn đầu, chỉ có 2 loại là folder, file. Do đó sẽ có 2 loại icon tương ứng. Để lấy được tên file icon, ta có mã nguồn như sau:

enum ResourceType{

FOLDER = 1,

FILE = 2;

}

class Helper{

public static string GetIcon(ResourceType resType){

if (resType == ResourceType.FOLDER)

return “FOLDER.gif”; // hard code do để minh họa

else if (resType == ResourceType.FILE)

return “FILE.gif”;

else

return “UNKNOWN.gif”;

}

}

Bạn thấy sao về cách thiết kế như trên. Có vẻ như hệ thống hoạt động không có vấn đề gì. Chương trình chỉ có vẻ bị phân mảnh bởi đoạn code if/else. Nhưng thực sự nó đã vi phạm nguyên tắc OCP nghiêm trọng. Giả sử sau khi hệ thống phát sinh yêu cầu mới, sẽ quản lý thêm một vài loại resource mới, thì lại phải thêm một loạt các đoạn code if/else hoặc đổi thành switch/case. Tồi tệ hơn nữa nếu như cách thức lấy tên icon của resource cũng thay đổi thì chúng ta lại phải sửa một loạt code trong cả khối if/else đó. Như vậy là, chúng ta đã phải sửa mã nguồn chương trình khi muốn mở rộng hệ thống.

Rõ ràng, OCP là rất quan trọng. Để giải quyết vấn để ở trên, sử dụng 1 interface hoặc abstract class và áp dụng 1 số pattern cơ bản sẽ giúp giải quyết phần nào vấn đề. Nói tóm lại, abstraction là chìa khóa để giải quyết OCP. Tuy nhiên, nhiều vấn đề phức tạp khác xảy ra nên IoC sẽ giúp chúng ta xây dựng được hệ thống ổn định và robust hơn.

Inversion of Control

Bây giờ, chúng ta sẽ tiếp tục tìm hiểu về IoC. Bản chất của IoC có thể tóm gọn trong câu sau: “Phụ thuộc vào abstract chứ không phụ thuộc vào concrete”. Nghĩa là, khi thiết kế class, 1 class phụ thuộc vào 1 interface hoặc 1 abstract class chứ không nên phụ thuộc vào 1 class riêng rẽ. Để hình dung rõ hơn, chúng ta hãy xem xét mô hình thiết kế lập trình hướng thủ tục (POP). Cấu trúc phụ thuộc của POP là từ trên xuống những phần chi tiết hơn (xem hình 1).

Photobucket - Video and Image Hosting

Hình 1

 

Các module mức cao phụ thuộc module mức thấp hơn, các module mức thấp lại phụ thuộc các module mức thấp hơn nữa… Như vậy, các module mức cao chỉ cần quan tâm đến logic mức cao của ứng dụng, lại phụ thuộc trực tiếp vào các module mức thấp hơn, phụ vào chi tiết thực thi của các module mức thấp. Chỉ cần các module mức thấp thay đổi hay cần thêm các module mức thấp, sẽ phải thay đổi hàng loạt các module mức cao.

Khi thiết kế OOP, việc thiết kế sao cho các class ở mức cao không phụ thuộc trực tiếp vào các class ở mức thấp là cực kỳ quan trọng. Xét mô hình sau:
Hình 2

Hình 2

Như vậy, các class mức cao sẽ phụ thuộc vào interface hoặc abstract class hơn là các class mức dưới. Bản thân các class implement các abstract interface sẽ phụ thuộc vào abstraction. Bất cứ thay đổi nào ở các module mức thấp cũng không hề ảnh hưởng tới module mức cao. Do đó, so với POP, sự phụ thuộc đã được đảo ngược. Do đó, ta gọi nguyên tắc này là đảo ngược phụ thuộc (Dependency inversion hay Inversion of Control.

IoC đóng vai trò là cơ chế để đạt được OCP, đồng thời nâng cao tính reuse. Nguyên tắc này là trung tâm của thiết kế framework.

Để minh họa cho IoC, ta sẽ refactor ví dụ đã nêu ở OCP. Bài toán cần giải quyết là cần lấy được đường dẫn của file ảnh chứa icon cho các nút trên cây tùy thuộc vào loại nút. Giải pháp tìm được phải đảm bảo tính OCP tức là khi có thêm loại resource cần thể hiện trên cây, mã nguồn của class Helper không phải thay đổi. Ta thấy rằng, method GetIcon của Helper hiện phụ thuộc vào loại resource và phụ thuộc vào chi tiết các lấy tên icon tương ứng với loại resource. Áp dụng IoC, ta tạo 1 Interface tên là IIconLookup, có khai báo method Lookup(). Mô hình của hệ thống sau khi refactor như sau:

Hình 3

Hình 3

Bây giờ, Helper sẽ chỉ giao tiếp với IIconLookup. Và nếu có sự thay đổi về resource type, Helper sẽ không cần quan tâm đến điều đó, chúng ta chỉ việc sửa mã nguồn trong implementation class tương ứng hoặc thêm implementation class tương ứng với resource type cần thêm. Tuy nhiên, còn 1 vấn đề nữa, rõ ràng Helper không quan tâm đến resource type. Nhưng class sử dụng Helper sẽ phải khởi tạo implementation class tương ứng, như vậy là vẫn phải phụ thuộc vào concrete class khi khởi tạo, điều này là không tránh khỏi. Giải pháp để giải quyết vấn đề này là áp dụng pattern Factory. Đây là 1 trong các Creational Pattern. Theo ví dụ, chúng ta có thể tạo ra 1 class là IconLookupFactory, có method static IIconLookup GetIconLookup(string resourceType). Class ở mức cao sẽ sử dụng IconLookupFactory để lấy được IIconLookup mà không phải khởi tạo từng implementation class (xem hình 4).

Hình 4

Hình 4

Áp dụng IoC để xây dựng framework lookup4net

Với việc áp dụng IoC vào bài toán Lookup Icon, chúng ta đã phần nào đạt được OCP. Tuy nhiên, với 1 hệ thống lớn, việc có nhiều module Lookup khác (lấy message theo region, lấy giá trị của Datasource…) là chuyện thường xuyên xảy ra. Mỗi lần thực hiện xây dựng 1 module lookup như vậy, chúng ta lại phải tạo 1 class Factory tương ứng. Thường thì các module lookup sẽ sử dụng file resource là dạng xml, lại được tạo ra rải rác nên rất dễ duplicated code trong các module.

Hơn nữa, để phục vụ cho việc reuse và nâng cao tính pluggable cho các module lookup, 1 framework để quản lý các module lookup là rất cần thiết. Tiếp theo đây, chúng ta sẽ thực hiện việc xây dựng framework lookup4net, giúp quản các module lookup. Các developer có thể xây dựng các module lookup rồi plug vào framework bằng cách thiết lập file lookup config (dưới dạng xml). Framework lookup4net ban đầu hỗ trợ cho các config dạng xml. Bất cứ developer nào cũng có thể xây dựng bộ config riêng cho lookup4net và plug vào framework để chạy.

Mô hình của framework lookup4net được trình bày ở hình 6. Hệ thống gồm các class và interface chính như sau:

ILookup: Interface lookup, khai báo method Lookup với tham số Param truyền vào tương ứng với thông tin để lookup. Các module lookup sẽ cung cấp các ILookup để lookup4net container quản lý và đăng ký vào framework thông qua file config xml hoặc thông qua module config viết riêng.

Param: super class của lookup4net. Các module lookup sẽ cung cấp các Param mang thông tin để lookup cho từng ILookup của module đó.

StringParam: Đây là 1 class Param mà lookup4net cung cấp sẵn. StringParam wrap 1 string bên trong để chứa thông tin dạng string.

LookupManager: đây là class cho phép client lấy được Lookup mà client cần sử dụng. LookupManager cung cấp các method để lấy được Lookup với các tham số liên quan đến Lookup đó

IConfigurator: Interface khai báo các method để config Lookup. Look4net cung cấp XmlConfigurator thực hiện việc nạp thông tin về lookup thông qua file XML. Các thông tin về các module lookup đăng ký với lookup4net thông qua file xml sẽ được XmlConfigurator sử dụng để tạo Lookup Container.

ConfiguratorFactory: class quản lý các IConfigurator. Các configurator có sẵn của lookup4net (XmlConfigurator) hoặc các configurator được plug vào lookup4net đều được khai báo trong file xml. ConfiguratorFactory sẽ lấy các thông tin trong đó để tạo ra Configurator Container.

IDataSource: chứa thông tin để IConfigurator thực hiện config. Lookup4net cung cấp sẵn FileDataSource chứa thông tin về file config.

Hình 5

Hình 5

ConfiguratorFactory sẽ xây dựng configurator container nhờ thông tin trong file config dưới dạng xml. Sau đây là ví dụ về file config:

<?xml version=”1.0″ encoding=”utf-8″ ?>

<lookupConfig>

<config name=”XMLConfigurator” assembly=”lookup4net”

classPath=”lookup4net.Config.XMLConfigurator”>

<property name=”LookupFile”

value=”D:\Projects\lookup4net\config\Lookup.xml”/>

</config>

</lookupConfig>

Phổ biến hiện nay thông tin config thường là file xml nên khi có module lookup mới, developer sẽ đăng ký thông tin lookup module trong file xml và sử dụng XmlConfigurator để xây dựng lookup container. Sau đây là ví dụ về lookup config file:

<?xml version=”1.0″ encoding=”utf-8″ ?>

<lookups>

<lookup name=”MessageLookup” dllFileName=”MessageLookup” type=”MessageLookup.MessageManager” multiRegion=”true”>

<resource fileName=”D:\Projects\Web\ja-JP\Resource.xml” regionName=”ja-JP”/>

<resource fileName=”D:\Projects\Web\en-US\Resource.xml” regionName=”en-US”/>

<param name=”MessageParam” type=”lookup4net.Params.StringParam”/>

</lookup>

<lookup name=”EnumLookup” dllFileName=”ResourceManager” type=”com.nmd.utility.enumutilities.EnumLookup” multiRegion=”true”>

<resource fileName=”ja-JP/Enum.xml” regionName=”ja-JP”/>

<resource fileName=”en-US/Enum.xml” regionName=”en-US”/>

<param name=”EnumParam” type=”com.nmd.utility.messageutilities.params.EnumParam”/>

</lookup>

</lookups>

Client sử dụng lookup4net chỉ thông qua LookupManager:

StringParam param = new StringParam(“MessageParam”, “DatabaseAccessFailure”);

string strRsr = (string)LookupManager.GetLookup(“MessageLookup”, param).Lookup(param);

Mong các bạn góp ý

Nguyễn Minh Dũng

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s


%d bloggers like this: