Hexagonal Architecture là gì và ứng dụng của nó

19 min read

Pattern: Ports và Adapters

Tổng quan

Cho phép một application được điều khiển đồng thời bởi users, programs, automated test hoặc batch scripts, và có thể developed and tested độc lập run-time devices và databases

Bất kỳ driver nào muốn sử dụng application qua một port, nó sẽ gửi một request được chuyển đổi qua adapter riêng thành một usable procedure call hoặc message, sau đó chuyền nó đến application port. Application hoàn toàn không biết gì về công nghệ của driver. Khi application có thứ gì đó cần gửi ra, nó gửi ra thông qua một port đến adapter, adapter này sẽ tạo ra các tín hiệu phù hợp cần thiết cho technology (human hoặc automated) nhận. Application có một interaction hợp lệ với các adapter ở tất cả các bên của nó, mà không thực sự biết bản chất của những thứ ở phía bên kia của các adapter.

  1. Driver: Là mọi thứ có thể điều khiển ứng dụng, bao gồm users, programs, automated test hoặt batch scripts
  2. Adapter: Là bộ phận đóng vai trò chuyển đổi giữa yêu cầu của driver và ứng dụng, cũng như tín hiệu từ ứng dụng trở lại driver.
  3. Port: Là interface nơi các yêu cầu và tín hiệu được truyền tải giữa driver và ứng dụng thông qua adapter.

Nguyên nhân

Một trong những vấn đề lớn của software applications qua nhiều năm là sự pha lẫn của business logic vào code. Vấn đề do việc này gây ra có ba khía cạnh:

  • Hệ thống không thể được kiểm thử gọn gàng với các bộ kiểm thử tự động vì một phần logic cần kiểm thử phụ thuộc vào các chi tiết thay đổi thường xuyên;
  • Vì lý do tương tự, việc chuyển từ hệ thống sử dụng bởi con người sang hệ thống chạy theo batch trở nên không khả thi;
  • Cũng vì lý do đó, việc cho phép chương trình được điều khiển bởi một chương trình khác trở nên khó khăn hoặc không thể thực hiện được khi điều đó trở nên phức tạp.

Cách mà nhiều tổ chức thường áp dụng là tạo một lớp mới trong kiến trúc với lời hứa rằng sẽ không có logic nghiệp vụ nào bị đưa vào lớp mới này. Tuy nhiên, vì không có cơ chế phát hiện vi phạm, sau vài năm, lớp mới dần bị lộn xộn với logic nghiệp vụ và vấn đề cũ lại xuất hiện.

Bây giờ hãy tưởng tượng rằng ”mọi” phần chức năng mà ứng dụng cung cấp đều có sẵn thông qua API (giao diện được lập trình ứng dụng) hoặc lệnh gọi hàm. Trong tình huống này, bộ phận kiểm tra hoặc QA có thể chạy các tập lệnh kiểm tra tự động đối với ứng dụng để phát hiện khi nào bất kỳ mã hóa mới nào phá vỡ chức năng hoạt động trước đó. Các chuyên gia có thể tạo các trường hợp kiểm thử tự động, trước khi hoàn thiện các chi tiết GUI, để thông báo cho các lập trình viên biết khi nào họ đã thực hiện đúng công việc của mình (và các trường hợp kiểm thử này trở thành các kiểm thử do bộ phận kiểm thử điều hành). Ứng dụng có thể được triển khai ở chế độ ”không đầu”, do đó chỉ có API và các chương trình khác có thể sử dụng chức năng của nó – điều này giúp đơn giản hóa thiết kế tổng thể của các bộ ứng dụng phức tạp và cũng cho phép các ứng dụng dịch vụ giữa doanh nghiệp với doanh nghiệp sử dụng lẫn nhau mà không cần sự can thiệp của con người trên web. Cuối cùng, các bài kiểm tra hồi quy hàm tự động phát hiện bất kỳ vi phạm nào đối với lời hứa loại bỏ logic nghiệp vụ ra khỏi lớp trình bày. Tổ chức có thể phát hiện và sau đó sửa lỗi rò rỉ logic.

Một vấn đề tương tự xảy ra ở phía bên kia của ứng dụng, nơi logic ứng dụng gắn liền với cơ sở dữ liệu hoặc các dịch vụ bên ngoài. Khi máy chủ cơ sở dữ liệu gặp sự cố hoặc phải nâng cấp, lập trình viên không thể làm việc do công việc của họ phụ thuộc vào cơ sở dữ liệu. Điều này làm tăng chi phí trì hoãn và tạo cảm giác căng thẳng giữa các nhân viên.

Mặc dù không rõ ràng rằng hai vấn đề này có liên quan, nhưng chúng có một mối tương quan đối xứng thể hiện qua giải pháp được đề xuất.

Tính chất của giải pháp

Các vấn đề từ phía người dùng lẫn phía máy chủ thực chất đều xuất phát từ cùng một lỗi thiết kế và lập trình — sự rối rắm giữa logic nghiệp vụ và việc tương tác với các thực thể bên ngoài. Sự bất đối xứng cần khai thác không phải là giữa các bên “trái” và “phải” của ứng dụng mà là giữa “bên trong” và “bên ngoài” của ứng dụng. Quy tắc cần tuân theo là mã nguồn thuộc phần “bên trong” không nên rò rỉ ra “bên ngoài”.

Khi loại bỏ bất kỳ sự bất đối xứng nào theo chiều ngang hoặc chiều dọc, chúng ta thấy rằng ứng dụng giao tiếp qua các “ports” với các cơ quan bên ngoài. Từ “port” gợi đến các “ports” trong hệ điều hành, nơi bất kỳ thiết bị nào tuân theo giao thức của port có thể được kết nối vào; và các “ports” trên thiết bị điện tử, nơi một lần nữa, bất kỳ thiết bị nào phù hợp với giao thức cơ học và điện tử đều có thể được kết nối.

Giao thức cho một port được xác định bởi mục đích của cuộc trò chuyện giữa hai thiết bị. Giao thức này được thể hiện dưới dạng một application program interface (API).

Đối với mỗi thiết bị bên ngoài, có một “adapter” chuyển đổi định nghĩa API thành các tín hiệu cần thiết cho thiết bị đó và ngược lại. Giao diện người dùng đồ họa (GUI) là một ví dụ về một adapter, nó ánh xạ các cử động của người dùng đến API của port. Các adapter khác phù hợp với cùng một port bao gồm các bộ công cụ kiểm thử tự động như FIT hoặc Fitnesse, các driver batch, và bất kỳ mã nguồn nào cần thiết để giao tiếp giữa các ứng dụng trong doanh nghiệp hoặc mạng.

Ở một phía khác của ứng dụng, ứng dụng giao tiếp với một thực thể bên ngoài để lấy dữ liệu. Giao thức thường là giao thức cơ sở dữ liệu. Từ quan điểm của ứng dụng, nếu cơ sở dữ liệu được chuyển từ cơ sở dữ liệu SQL sang tệp tin phẳng hoặc bất kỳ loại cơ sở dữ liệu nào khác, cuộc trò chuyện qua API không nên thay đổi. Các adapter bổ sung cho cùng một port bao gồm adapter SQL, adapter tệp tin phẳng, và quan trọng nhất là một adapter cho cơ sở dữ liệu “mock”, một cơ sở dữ liệu nằm trong bộ nhớ và không phụ thuộc vào sự hiện diện của cơ sở dữ liệu thực.

Nhiều ứng dụng chỉ có hai port: giao tiếp phía người dùng và giao tiếp phía cơ sở dữ liệu. Điều này khiến chúng có vẻ bất đối xứng, làm cho việc xây dựng ứng dụng theo kiến trúc xếp chồng một chiều, ba, bốn hoặc năm lớp có vẻ tự nhiên.

Có hai vấn đề với các sơ đồ này. Thứ nhất và tồi tệ nhất, người ta thường không coi trọng các “đường kẻ” trong diagram class. Họ để cho logic ứng dụng rò rỉ qua các ranh giới class, gây ra các vấn đề đã nêu ở trên. Thứ hai, có thể có nhiều hơn hai port cho ứng dụng, khiến kiến trúc không phù hợp với sơ đồ lớp một chiều.

Kiến trúc hình lục giác, hay còn gọi là kiến trúc ports và adapters, giải quyết những vấn đề này bằng cách lưu ý đến sự đối xứng trong tình huống: có một ứng dụng ở bên trong giao tiếp qua một số port với các thực thể bên ngoài. Các mục bên ngoài ứng dụng có thể được xử lý một cách đối xứng.

Hình lục giác nhằm làm nổi bật

(a) Sự bất đối xứng bên trong-bên ngoài và bản chất tương tự của các port, để rời xa bức tranh class một chiều và tất cả những gì nó gợi ra.

(b) Sự hiện diện của một số lượng port khác nhau được xác định – hai, ba hoặc bốn (bốn là số nhiều nhất tôi đã gặp cho đến nay).

Hình lục giác không phải là lục giác vì số sáu quan trọng, mà để cho người vẽ có không gian để chèn các port và adapter theo nhu cầu của họ, không bị ràng buộc bởi một sơ đồ lớp một chiều. Thuật ngữ “hexagonal architecture” xuất phát từ hiệu ứng hình ảnh này.

Thuật ngữ “ports and adapters” phản ánh các “mục đích” của các phần trong bản vẽ. Một port xác định một cuộc trò chuyện có mục đích. Thường thì có nhiều adapter cho một port, cho các công nghệ khác nhau có thể kết nối vào port đó. Những adapter này có thể bao gồm máy trả lời điện thoại, giọng nói con người, điện thoại quay số, giao diện người dùng đồ họa, bộ công cụ kiểm thử, driver batch, giao diện http, giao diện chương trình-đến-chương trình, cơ sở dữ liệu “mock” (trong bộ nhớ), cơ sở dữ liệu thực (có thể là các cơ sở dữ liệu khác nhau cho phát triển, kiểm thử, và sử dụng thực tế).

Trong các Application Notes, sự bất đối xứng trái-phải sẽ được nhắc lại một lần nữa. Tuy nhiên, mục đích chính của mẫu thiết kế này là tập trung vào sự bất đối xứng bên trong-bên ngoài, tạm thời coi tất cả các mục bên ngoài là giống nhau từ góc độ của ứng dụng.

Structure

Hình trên cho thấy application đang có hai active ports và several adapters cho hai port. Hai port là phía Driver ứng dụng và phía lấy dữ liệu. Sơ đồ này cho thấy rằng application có thể được điều khiển một cách đồng đều bởi một bộ kiểm thử hồi quy hệ thống tự động, bởi một người dùng, bởi một application http từ xa, hoặc bởi một application local khác. Ở phía dữ liệu, application có thể được cấu hình để chạy tách biệt với các cơ sở dữ liệu bên ngoài bằng cách sử dụng một cơ sở dữ liệu thay thế trong bộ nhớ, hoặc “mock”; hoặc nó có thể chạy trên cơ sở dữ liệu kiểm thử hoặc cơ sở dữ liệu thực thi. Thông số chức năng của ứng dụng, có thể là trong các trường hợp sử dụng, được thiết lập dựa trên giao diện của lục giác bên trong và không dựa trên bất kỳ công nghệ bên ngoài nào có thể được sử dụng.

Hình Trên cho thấy cùng một ứng dụng được ánh xạ vào một sơ đồ kiến trúc ba lớp. Để đơn giản hóa sơ đồ, chỉ có hai adapter được hiển thị cho mỗi port. Sơ đồ này nhằm mục đích cho thấy cách nhiều adapter phù hợp vào các lớp trên và dưới, và thứ tự mà các adapter khác nhau được sử dụng trong quá trình phát triển hệ thống. Các mũi tên được đánh số cho thấy thứ tự mà một nhóm có thể phát triển và sử dụng ứng dụng:

  1. Với một bộ công cụ kiểm thử FIT điều khiển ứng dụng và sử dụng cơ sở dữ liệu mock (trong bộ nhớ) thay thế cho cơ sở dữ liệu thực;
  2. Thêm một GUI vào ứng dụng, vẫn chạy trên cơ sở dữ liệu mock;
  3. Trong kiểm thử tích hợp, với các kịch bản kiểm thử tự động (ví dụ, từ Cruise Control) điều khiển ứng dụng chống lại cơ sở dữ liệu thực chứa dữ liệu kiểm thử;
  4. Trong sử dụng thực tế, với một người dùng ứng dụng để truy cập cơ sở dữ liệu sống.

Notes

Sự bất đối xứng trái-phải

Mẫu thiết kế ports and adapters được viết một cách có chủ ý với giả định rằng tất cả các port về cơ bản là tương tự nhau. Sự giả định này hữu ích ở cấp kiến trúc. Trong thực tế, ports và adapters xuất hiện dưới hai dạng, mà tôi sẽ gọi là “primary” và “secondary”, vì lý do sẽ rõ ràng ngay. Chúng cũng có thể được gọi là “driving” adapters và “driven” adapters.

Người đọc tinh ý sẽ nhận thấy rằng trong tất cả các ví dụ được đưa ra, các “fixtures” của FIT được sử dụng ở các port bên trái và các mocks ở bên phải. Trong kiến trúc ba lớp, FIT nằm ở lớp trên cùng và mock nằm ở lớp dưới cùng.

Điều này liên quan đến ý tưởng từ các trường hợp sử dụng về “primary actors” và “secondary actors”. Một “primary actor” là một actor điều khiển ứng dụng (kích hoạt ứng dụng từ trạng thái ngủ để thực hiện một trong những chức năng đã quảng cáo). Một “secondary actor” là một actor mà ứng dụng điều khiển, để nhận câu trả lời từ hoặc chỉ đơn giản là thông báo. Sự phân biệt giữa “primary” và “secondary” nằm ở việc ai kích hoạt hoặc chịu trách nhiệm về cuộc trò chuyện.

Adapter kiểm thử tự nhiên để thay thế cho một “primary” actor là FIT, vì framework này được thiết kế để đọc một kịch bản và điều khiển ứng dụng. Adapter kiểm thử tự nhiên để thay thế cho một “secondary” actor như cơ sở dữ liệu là mock, vì nó được thiết kế để trả lời các truy vấn hoặc ghi nhận các sự kiện từ ứng dụng.

Những quan sát này dẫn chúng ta đến việc theo dõi sơ đồ ngữ cảnh trường hợp sử dụng của hệ thống và vẽ các “primary ports” và “primary adapters” ở phía bên trái (hoặc trên) của hình lục giác, và các “secondary ports” và “secondary adapters” ở phía bên phải (hoặc dưới) của hình lục giác.

Mối quan hệ giữa các primary và secondary ports/adapters và các thực thi tương ứng của chúng trong FIT và mocks là điều quan trọng cần lưu ý, nhưng nên được sử dụng như một hệ quả của việc sử dụng kiến trúc ports and adapters, chứ không phải để rút ngắn nó. Lợi ích cuối cùng của việc triển khai ports and adapters là khả năng chạy ứng dụng trong chế độ hoàn toàn cách ly.

Các trường hợp sử dụng và ranh giới ứng dụng

Việc sử dụng mẫu kiến trúc hình lục giác (hexagonal architecture) để củng cố cách viết các trường hợp sử dụng (use cases) là rất hữu ích.

Một sai lầm phổ biến là viết các “use case” với “core engine” nằm ngoài mỗi port. Những “use case” này đã bị chỉ trích vì lý do chính đáng vì chúng thường dài dòng, khó đọc, nhàm chán, dễ bị hỏng và tốn kém trong việc bảo trì.

Hiểu rõ kiến trúc ports and adapters, chúng ta có thể thấy rằng các trường hợp sử dụng nên được viết ở ranh giới ứng dụng (hình lục giác bên trong), để chỉ định các chức năng và sự kiện mà ứng dụng hỗ trợ, bất kể công nghệ bên ngoài là gì. Những trường hợp sử dụng này ngắn gọn hơn, dễ đọc hơn, ít tốn kém hơn trong việc bảo trì, và ổn định hơn theo thời gian.

Có bao nhiêu Ports?

Cái gì chính xác là một port và cái gì không phải là một port phần lớn phụ thuộc vào sở thích cá nhân. Ở một cực, mỗi trường hợp sử dụng (use case) có thể được gán cho một port riêng, dẫn đến hàng trăm port cho nhiều ứng dụng. Ở cực đối diện, có thể tưởng tượng việc gộp tất cả các port chính (primary ports) và tất cả các port phụ (secondary ports) lại thành chỉ hai port, một bên trái và một bên phải.

Hệ thống thời tiết được mô tả trong các Ví dụ Đã Biết có bốn port tự nhiên: nguồn cấp dữ liệu thời tiết, quản trị viên, các người đăng ký được thông báo, cơ sở dữ liệu người đăng ký. Một bộ điều khiển máy pha cà phê có bốn port tự nhiên: người dùng, cơ sở dữ liệu chứa các công thức và giá, các thiết bị phân phối, và hộp đựng tiền. Một hệ thống quản lý thuốc trong bệnh viện có thể có ba port: một cho y tá, một cho cơ sở dữ liệu đơn thuốc, và một cho các thiết bị phân phối thuốc điều khiển bằng máy tính.

Có vẻ như không có thiệt hại đặc biệt nào trong việc chọn số lượng port “sai”, vì vậy điều này vẫn phụ thuộc vào trực giác. Lựa chọn của tôi thường nghiêng về số lượng ít, hai, ba hoặc bốn port, như đã mô tả ở trên và trong các Ví dụ Đã Biết

Ví dụ

Hình trên cho thấy một ứng dụng với bốn port và nhiều adapter tại mỗi port. Hình ảnh này được rút ra từ một ứng dụng theo dõi các cảnh báo từ dịch vụ khí tượng quốc gia về động đất, lốc xoáy, hỏa hoạn và lũ lụt, và thông báo cho người dùng qua điện thoại hoặc máy trả lời điện thoại. Khi chúng tôi thảo luận về hệ thống này, các giao diện của hệ thống được xác định và thảo luận theo “công nghệ, liên kết với mục đích”. Có một giao diện cho dữ liệu kích hoạt đến qua nguồn cấp dữ liệu, một giao diện để gửi dữ liệu thông báo đến máy trả lời điện thoại, một giao diện quản trị được thực hiện qua GUI, và một giao diện cơ sở dữ liệu để lấy dữ liệu người đăng ký.

Nhóm gặp khó khăn vì họ cần thêm một giao diện http từ dịch vụ khí tượng, một giao diện email cho người đăng ký của họ, và họ phải tìm cách đóng gói và mở gói bộ ứng dụng đang phát triển của mình cho các tùy chọn mua hàng khác nhau của khách hàng. Họ lo lắng rằng họ đang đối mặt với cơn ác mộng bảo trì và kiểm thử khi phải triển khai, kiểm thử và duy trì các phiên bản riêng biệt cho tất cả các kết hợp và hoán vị.

Sự chuyển đổi trong thiết kế của họ là kiến trúc các giao diện hệ thống theo “mục đích” thay vì theo công nghệ, và để các công nghệ có thể thay thế được (ở tất cả các bên) bằng các adapter. Họ ngay lập tức có khả năng bao gồm nguồn cấp dữ liệu http và thông báo email (các adapter mới được hiển thị trong sơ đồ với các đường kẻ đứt). Bằng cách làm cho mỗi ứng dụng có thể thực thi ở chế độ headless thông qua các API, họ có thể thêm một adapter ứng dụng-đến-thêm và mở gói bộ ứng dụng, kết nối các ứng dụng con khi cần. Cuối cùng, bằng cách làm cho mỗi ứng dụng có thể thực thi hoàn toàn trong cách ly, với các adapter kiểm thử và mock, họ có khả năng kiểm thử hồi quy các ứng dụng của họ bằng các kịch bản kiểm thử tự động độc lập.

Tổng kết

Tóm lại, pattern ports và adapters (hay còn gọi là kiến trúc hình lục giác) mang lại một cách tiếp cận mạnh mẽ và linh hoạt để xây dựng ứng dụng. Bằng cách phân tách logic ứng dụng khỏi các yếu tố bên ngoài thông qua các port và adapter, mẫu thiết kế này giúp cải thiện khả năng bảo trì, mở rộng và kiểm thử của ứng dụng.

Sự phân biệt giữa các port chính (primary) và phụ (secondary) cũng như việc lựa chọn số lượng port phù hợp là điều quan trọng để đảm bảo rằng thiết kế của bạn vừa hiệu quả vừa dễ quản lý. Việc áp dụng kiến trúc này giúp bạn tập trung vào chức năng cốt lõi của ứng dụng, mà không bị ràng buộc bởi các công nghệ bên ngoài, từ đó nâng cao sự ổn định và linh hoạt của hệ thống.

Tài liệu tham khảo và bài đọc liên quan

FIT, A Framework for Integrating Testing: Cunningham, W., online at http://fit.c2.com, and Mugridge, R. and Cunningham, W., ‘’Fit for Developing Software’’, Prentice-Hall PTR, 2005.

The ‘’Adapter’’ pattern: in Gamma, E., Helm, R., Johnson, R., Vlissides, J., ‘’Design Patterns’’, Addison-Wesley, 1995, pp. 139-150.

The ‘’Pedestal’’ pattern: in Rubel, B., “Patterns for Generating a Layered Architecture”, in Coplien, J., Schmidt, D., ‘’PatternLanguages of Program Design’’, Addison-Wesley, 1995, pp. 119-150.

The ‘’Checks’’ pattern: by Cunningham, W., online at http://c2.com/ppr/checks.html

The ‘’Dependency Inversion Principle’‘: Martin, R., in ‘’Agile Software Development Principles Patterns and Practices’’, Prentice Hall, 2003, Chapter 11: “The Dependency-Inversion Principle”, and online at http://www.objectmentor.com/resources/articles/dip.pdf

The ‘’Dependency Injection’’ pattern: Fowler, M., online at http://www.martinfowler.com/articles/injection.html

The ‘’Mock Object’’ pattern: Freeman, S. online at http://MockObjects.com

The ‘’Loopback’’ pattern: Cockburn, A., online at http://c2.com/cgi/wiki?LoopBack

‘’Use cases:’’ Cockburn, A., ‘’Writing Effective Use Cases’’, Addison-Wesley, 2001, and Cockburn, A., “Structuring Use Cases with Goals”, online at http://alistair.cockburn.us/crystal/articles/sucwg/structuringucswithgoals.htm

Avatar photo

Leave a Reply

Your email address will not be published. Required fields are marked *