Giao tiếp trong kiến trúc Module
Kiến trúc mô-đun (module) cho phép chia nhỏ bài toán (hay yêu cầu) của phần mềm thành các phần hầu như không trùng lắp và do đó hỗ trợ làm việc song song trên các module và đặc biệt là dễ bảo trì hơn.
Về khái niệm,
kiến trúc mô-đun (module) cho phép chia nhỏ bài toán (hay yêu cầu) của
phần mềm thành các phần hầu như không trùng lắp và do đó hỗ trợ làm việc
song song trên các module và đặc biệt là dễ bảo trì hơn. Kiến trúc
module cũng là chìa khóa để đạt tới các khả năng tái sử dụng các thành
phần của hệ thống và khả năng mở rộng tốt hơn.
Một số ngôn ngữ hỗ trợ lập trình theo cơ chế module
cho phép biên dịch các module một cách độc lập và có thể gắn kết vào hệ
thống lúc thực thi như Flex, Ruby... Một số ngôn ngữ khác thì hỗ trợ cơ
chế như thư viện liên kết động (DLL) để biên dịch các module thành các
thư viện độc lập và có thể gắn kết động vào hệ thống.
|
Những ví dụ dễ thấy của kiến trúc module là các môi
trường như Eclipse, Visual Studio. Chúng được tạo ra như những nền tảng
cơ bản trong đó việc hỗ trợ các ngôn ngữ như Java, C#... được thiết kế
dạng plugin có thể dễ dàng gắn vào nền tảng. Chúng ta cũng có thể thấy
công cụ Visual Studio đi kèm bộ SQL Professional để hỗ trợ các project
của SQL server dưới tên khác là SQL
Server Business Intelligence Development Studio. Việc
tổ chức như thế thậm chí còn cho phép các nhà phát triển thứ 3 dễ dàng
phát triển các tính năng mở rộng, như bổ sung hỗ trợ PHP vào Visual
Studio qua plugin VS.Php. (Hình 2)
Kiến trúc module có thể mở rộng để áp dụng ở mức hệ
thống và các module có thể là các ứng dụng hay dịch vụ chạy song song và
tương tác với nhau thông qua một kiểu giao tiếp nào đó như Messaging,
RPC, Socket...
| |
Hình 2: Các ứng dụng web như các CMS Joomla, Drupal cũng được xây dựng trên nền tảng module cho cộng đồng cùng phát triển và dễ mở rộng. |
Về mặt ý tưởng, kiến trúc module có thể xem là nền
tảng cơ bản của rất nhiều kiến trúc tiên tiến khác như MVC, Multi-tier,
SOA... Hơn nữa, kiến trúc module có thể tái áp dụng vào các module của
chính nó hay các thành phần con của các kiến trúc trên như các dịch vụ
bên trong các hệ thống SOA.
Interface – công cụ giao tiếp của các module
Các module được gắn kết với nhau trong chương trình
thông qua các "interface". Một interface của module mô tả những thành
phần được cung cấp và cần được cung cấp. Các thành phần này này được các
module khác "thấy" và sử dụng.
Lưu ý khái niệm interface ở đây khác biệt với khái
niệm "interface" của các ngôn ngữ như Java, C#. Các interface của các
module thường được thiết kế theo tiêu chí sao cho các quan hệ phụ thuộc
giữa các module là nhỏ nhất.
David Parnas – người đi tiên phong về lĩnh vực kiến
trúc phần mềm – đã đưa ra khái niệm Information-hidding được dùng như
nền tảng cơ bản cho những nghiên cứu sau này.
Information-hiding (Parnas)
|
Trong kiến trúc module, theo nguyên tắc
Information-hiding, những phần có khả năng thay đổi bên trong của module
được ẩn đi nên những phần còn lại dùng để giao tiếp giữa các module sẽ
không bị ảnh hưởng khi thay đổi thiết kế. Kết quả là những module có thể
thay đổi một cách độc lập mà không ảnh hưởng lẫn nhau. Ví dụ, khi chúng
ta muốn xây dựng một ứng dụng hỗ trợ nhiều cách lưu trữ dữ liệu khác
nhau, thậm chí có thể mở rộng bổ sung trong tương lai thì cách tốt nhất
là ẩn đi những xử lý lưu trữ cụ thể và chỉ dùng một giao diện chung đơn
giản cho chúng.
Như vậy theo Parmas, interface của module nên được
thiết kế chỉ bao gồm những phần hầu như không thay đổi, những thành phần
này được gọi là thành phần "công khai". Còn những chi tiết ẩn bên trong
bởi interface thường được gọi là các thành phần "bí mật" hoặc "riêng
tư”. (Hình 3)
Trong .Net framework, những thành phần bí mật được sử
dụng nội bộ bên trong các lớp thư viện của framework được khai báo bằng
từ khóa "Internal". Các thành phần "Internal" của .Net không thể truy
cập trực tiếp từ bên ngoài assembly của chúng cho dù chúng ta biết rõ mô
tả của chúng. Tuy nhiên, trong một số trường hợp chúng ta muốn sử dụng
một số tính năng cấp thấp hoặc sửa lỗi của framework nên buộc phải truy
cập vào các thành phần này, một kĩ thuật thường dùng là Reflection.
Trong các mẫu lập trình (pattern), có một mẫu được tổ chức theo ý tưởng trên và được dùng rất phổ biến, đó là Facade pattern.
Facade pattern
Facade pattern thường được dùng trong lập trình hướng
đối tượng. Một facade là một object đại diện cho các lớp thư viện bên
trong, cung cấp một giao diện đơn giản ra bên ngoài. Facade có thể:
• Làm cho một thư viện phần mềm dễ hiểu và dễ sử dụng
hơn vì facade cung cấp phương tiện đơn giản hơn để truy cập những tác
vụ thông thường (thay vì thao tác qua nhiều lớp thư viện phức tạp).
• Giảm sự phụ thuộc giữa code bên ngoài và code thực
thi bên trong thư viện, vì thế cho phép phát triển hệ thống một cách mềm
dẻo hơn.
| |
Hình 4 : Một ví dụ về Facade pattern |
• Được dùng như một cách để gói gọn một tập hợp API
có sẵn được tổ chức không tốt bằng một tập API đơn giản hơn nhiều để
phục vụ cho một vài tác vụ nhất định.
(Xem hình 4)
(Xem hình 4)
Facade pattern thường kết hợp với Singleton pattern
để tổ chức lớp facade theo dạng singleton. Ngoài ra, Facade pattern đặc
biệt phù hợp trong mô hình 3 cấp (3-tier) qua cách định nghĩa các giao
tiếp đơn giản để tương tác giữa các cấp, nhờ đó ẩn đi các xử lý phức tạp
bên trong. Nó còn được áp dụng rất phổ biến trong .NET Framework theo
mô hình thiết kế component-oriented.
Một trong những facade thường được sử dụng trong .NET
2.0 là "System.Web.Mail.SmtpMail". Lớp này cung cấp cho người dùng một
giao tiếp đơn giản để gửi mail thông qua giao thức SMTP mà không phải
quan tâm đến các thao tác xử lý phức tạp bên trong.
Tuy nhiên, việc sử dụng các lớp facade trong một thư
viện cần được xem xét cẩn thận. Vì các facade cung cấp giao tiếp ở mức
cao cho những chi tiết bên trong nên nếu không được thiết kế tốt, chúng
sẽ hạn chế những tính năng của thư viện khi người dùng không thể truy
cập đến những chi tiết ở mức thấp bị ẩn đi.
Inversion of control
Inversion of Control, hay IoC, là một nguyên tắc
thuộc lĩnh vực thiết kế kiến trúc phần mềm trong đó mô tả luồng điều
khiển của hệ thống bị đảo ngược so với kiến trúc cổ điển.
Theo mô hình lập trình cổ điển, lập trình viên viết
các xử lý và thủ tục gọi đến các thủ tục trong thư viện có sẵn, các thủ
tục này có thể gọi đến một thủ tục khác ở một thư viện khác nhưng nó
không bao giờ gọi trở lại những xử lý do người lập trình đó viết (chúng
ta tạm gọi là thủ tục của người dùng – user procedure). Nhưng đối với
IoC, điều này lại xảy ra. Kết quả này có thể đạt được bằng cách thư viện
hay một framework định nghĩa cơ chế chung cho một loại vấn đề, còn xử
lý cụ thể sẽ do thủ tục của người dùng giải quyết.
Một ví dụ dễ thấy của IoC là sự thay đổi của mô hình
lập trình giao diện. Theo mô hình console cổ điển thì giao diện ứng dụng
được điều khiển bởi code của ứng dụng. Ví dụ, để giải một bài toán
c=a+b ta cần lần lượt yêu cầu người dùng nhập a, b rồi hiện kết quả c.
Tuy nhiên, với các framework lập trình hỗ trợ giao diện đồ họa mới như
.NET, vòng lặp xử lý chính được framework cung cấp và chương trình cần
viết chỉ việc cung cấp các thủ tục xử lý sự kiện cho các control tương
ứng với các giá trị trên. Luồng xử lý chính của ứng dụng như thế đã tách
khỏi chương trình và đi vào framework.
IoC được sử dụng như một tính năng cơ bản trong các
framework hỗ trợ lập trình mức cao (như Spring, Cairngorm, PureMVC...).
Thay vì phải viết một chuỗi các xử lý và thủ tục được gọi một cách tuần
tự, người sử dụng các framework này thường viết các xử lý để đáp ứng lại
các event hoặc yêu cầu dữ liệu đặc trưng. Đây cũng là một hướng khác
trong việc thiết kế phương tiện giao tiếp giữa các module so với kiểu cổ
điển vì trong phương thức giao tiếp có sự tham gia điều phối của
framework bên dưới. Dưới đây chúng ta sẽ khái quát ba trong số các khái
niệm kế thừa từ IoC: data-binding, dependency injection và event-driven
architecture.
Data-binding
Trở lại ví dụ chương trình giải bài toán c=a+b ở
trên, nếu chúng ta sử dụng một công cụ bảng tính như Excel và gán công
thức cho cell C là cell A + cell B thì khi giá trị cell A và B được cập
nhật, giá trị cell C sẽ tự động được tính lại. Tính năng tự động đồng bộ
dữ liệu khi có thay đổi như trên không phải là mới và xuất hiện nhiều
trong các ngôn ngữ lập trình hiện nay với tên gọi là data-binding.
|
Theo kiểu lập trình thô sơ hơn mà chúng ta thường làm
thì để đạt được khả năng trên, trong một ngôn ngữ lập trình như .NET,
chúng ta sẽ bắt các sự kiện thay đổi của các control (cell A, cell B) và
gọi lại thủ tục cập nhật cell C. Còn khi sử dụng kĩ thuật data-binding,
một đối tượng quan sát (observer) được tạo ra để quan sát các thay đổi
trên thuộc tính của đối tượng nguồn và tự động cập nhật thay đổi lên
thuộc tính được đăng ký tương ứng của đối tượng đích. Để cài đặt cơ chế
data-binding, Observer pattern thường được dùng để tạo đối tượng
observer và thuộc tính của đối tượng đích thường được dùng ở dạng set
property. Cơ chế kích hoạt của data-binding là cách sử dụng sự kiện
(event). Khi có sự thay đổi trên thuộc tính đối tượng nguồn, một event
sẽ được kích hoạt và gửi tới observer. Observer sau đó duyệt trong danh
sách đăng ký của mình và kích hoạt lại biểu thức gán thuộc tính các đối
tượng đích tương ứng. (Hình 5)
Tính năng data-binding được hỗ trợ sẵn trong hầu hết
các các framework lập trình hiện tại (như .NET, Flex...) và giúp lập
trình viên giảm bớt rất nhiều thao tác xử lý so với cách xử lý thủ công.
Hiện tại, data-binding ở các framework khác nhau cũng được hỗ trợ một
số tính năng bổ sung khác nhau như khả năng binding 2 chiều, binding
ngược từ đối tượng đích đến đối tượng nguồn...
Trở lại vấn đề ứng dụng trong kiến trúc module thì
data-binding có ứng dụng như thế nào? Data-binding có khả năng rất to
lớn trong việc thay đổi cách tương tác của các thành phần giao diện với
các thành phần đảm nhận xử lý logic và dữ liệu. Trong các mô hình như
3-tier hay MVC, khi sử dụng data-binding, các module presentation hay
View có thể gửi và nhận các thay đổi từ các module khác một cách tự động
nhờ xử lý của framework bên dưới và do đó giảm bớt sự phụ thuộc giữa
các module. Một framework được xem là chuẩn và được sử dụng rộng rãi
trong các dự án lớn hiện nay là Cairngorm (xây dựng trên nền tảng Flex
của Adobe theo mô hình MVC) cũng sử dụng data-binding như là phương tiện
duy nhất để thao tác tới các thành phần View.
Dependency injection
Dependency injection
Được xem như một khái niệm điển hình kế thừa từ IoC,
dependency injection thường được ứng dụng như một pattern mạnh mẽ trong
việc hạn chế sự phụ thuộc giữa các object hay các module.
Trong một ứng dụng, giả sử B là một lớp dịch vụ được
tổ chức ở mức trừu tượng – abstract, ví dụ như hỗ trợ lưu trữ ảnh, và có
nhiều lớp cụ thể kế thừa từ nó như lưu trữ vào database, lưu trữ file
jpg, png, ... Thông thường, nếu một thành phần A (client) cần truy cập
đến một dịch vụ cụ thể của B thì A thường hoặc phải nắm giữ tham chiếu
trực tiếp đến đối tượng cụ thể hoặc thông qua một đối tượng trung gian
nào đó để lấy tham chiếu đến nó. Bằng cách sử dụng cách truy cập thông
qua đối tượng trung gian, chúng ta có thể tăng tính tái sử dụng A trong
nhiều trường hợp bằng cách kết hợp nó với nhiều dạng lưu trữ khác nhau.
Tuy nhiên theo cách này thì A vẫn bị giới hạn là phải biết đến đối tượng
trung gian cũng như cách truy cập một dịch vụ cụ thể của B từ nó. Nhưng
đối với dependency injection, A đơn giản chỉ cần cung cấp một thuộc
tính dùng để chứa tham chiếu và một khi một đối tượng A cụ thể được tạo
ra, một dịch vụ cụ thể của B cũng sẽ được tự động tạo ra và gắn vào tham
chiếu này thông qua một cơ chế bên ngoài.
Dependency injection được ứng dụng rộng rãi trong các
framework trên nền tảng Java và .NET như Spring, PicoContainer,
Spring.NET, Castle MicroKernel/Windsor... hay trong Flex như Mate,
Swiz... Các framework này hầu hết đều hỗ trợ cấu hình các liên kết thông
qua file config nên càng tăng tính tiện dụng.
Event-driven architecture
Event-driven architecture (EDA) là mẫu kiến trúc phần
mềm (architecture software pattern) trong đó, về cơ bản, hệ thống được
xây dựng xung quanh các thao tác như tạo, khám phá, tiêu thụ và đáp trả
lại các sự kiện. Hiểu đơn giản hơn, EDA là một dạng kiến trúc phần mềm
được xây dựng trên luồng các event, sử dụng event như là phương tiện
giao tiếp giữa các thành phần hệ thống.
Một event trong EDA được hiểu là một "thay đổi trạng
thái đáng chú ý” của một thành phần nào đó. Event có thể được phát sinh
do người dùng, do các thiết bị phần cứng hoặc do chính phần mềm phát
sinh trong một điều kiện nào đó. EDA được xem như một trong những kĩ
thuật thiết kế hiệu quả nhất trong việc hạn chế đến mức nhỏ nhất quan hệ
phụ thuộc giữa các thành phần hệ thống hay các module.
| |
Hình 6: Một ví dụ đơn giản về EDA |
Một ví dụ đơn giản của EDA: một module quản lý việc
đăng nhập của user cần chứng thực thông tin của user vừa nhập xong nên
tự phát sinh và gửi đi một event gọi là LoginEvent chứa thông tin user.
Event này sau đó được một module có khả năng thao tác với dữ liệu như
WebServer, Database... bắt lấy, thực hiện việc kiểm chứng và sau đó trả
lời kết quả thông qua LoginResultEvent để module đăng nhập bắt lấy. Theo
cách xử lý này thì module đăng nhập không cần biết module nào và sẽ làm
thế nào để thực hiện việc kiểm tra, nó chỉ cần biết gửi yêu cầu và nhận
kết quả sau khi kiểm tra và tất cả những gì nó quan tâm chỉ là các
event được định nghĩa ở mức hệ thống. Hơn nữa, event kết quả trong
trường hợp trên có thể được quan tâm bởi nhiều module khác như module
đảm nhận ghi log và do đó làm cho hệ thống càng mềm dẻo hơn. (Hình 6)
EDA là mẫu kiến trúc được ứng dụng rất rộng rãi trong
các hệ thống hiện đại và đặc biệt được dùng như phương tiện giao tiếp
giữa các dịch vụ trong các hệ thống Service-oriented architecture (SOA)
kiểu mới. EDA còn được sử dụng rộng rãi trong các framework phổ biến như
Cairngorm, PureMVC...
Lời kết
Hi vọng bài viết cung cấp cho các bạn những thông tin
hữu ích về kiến trúc module. Còn rất nhiều vấn đề chưa được đề cập như
phương pháp chia nhỏ hệ thống thành các module, các nguyên tắc thiết
kế... Những vấn đề này hi vọng chúng ta sẽ có dịp bàn tới trong các bài
viết khác.
Tài liệu tham khảo
1. Bertrand Meyer, Object-Oriented software construction – Second edition.
2. David Parnas, On the Criteria to Be Used in Decomposing Systems Into Modules. 3. Len Bass, Paul Clements, Rick Kazman, Software Architecture in Practice, Second Edition. 4. Martin Flower, Inversion of Control Containers and the Dependency Injection Pattern. 5. http://docs.joomla.org/Framework. 6. http://en.wikipedia.org/wiki/. 7. http://msdn.microsoft.com. |
Nhận xét
Đăng nhận xét