2022-11-19

Hexagonal Architecture

What is Hexagonal Architecture

The Hexagonal Architecture, also known as the Ports and Adapters pattern, is a software architectural pattern that promotes the separation of concerns by viewing an application as a hexagon. Each side of the hexagon represents a port that interacts with the external world via adapters.

Hexagonal Architecture seeks to centralize the application's core business logic and isolate it from peripheral concerns, maintaining a clear boundary that external factors cannot cross. This isolation allows changes on one side of the application (e.g., database or user interface) to occur independently of the other, ensuring the application's stability and easing the process of testing and maintenance.

Principles of Hexagonal Architecture

In this chapter, I'll delve into the principles that underpin the hexagonal architecture. We'll explore the key concepts of application, ports, adapters, and the hexagonal layers.

Hexagonal architecture
Hexagonal Architecture Explained

Center: The Application

At the heart of the hexagonal architecture lies the application, also known as the domain. This encapsulates the business rules and the core logic of the software. The application doesn't have any dependencies on external concerns, such as databases, user interfaces, or any third-party services. This feature is crucial because it enables us to make changes in one part of the system without affecting the core application.

Ports: Inward and Outward

The application interacts with the outside world through ports, which can be categorized into primary (or driving) ports and secondary (or driven) ports.

Primary ports are interfaces that expose the operations provided by the application to the outside world. They represent use-cases the application supports, defining what the application can do. Typically, these are the APIs that the application developers will design and implement.

Secondary ports, on the other hand, are interfaces that the application uses to interact with external services or components. These are things the application needs from the outside world to function, like a persistence mechanism, notifications, or any external APIs.

Adapters: The Intermediaries

While ports define what kind of interactions are possible, adapters handle those interactions. Adapters are the link between the application and the outside world. They convert data from the format that the outside world uses to one the application can understand, and vice versa. There can be different types of adapters for different kinds of inputs and outputs, like a web adapter to handle HTTP requests or a database adapter to interact with a database.

Hexagonal Layers

Each side of the hexagon in the architecture represents a port. The adapters that interact with these ports sit on the edges of the hexagon. This arrangement brings about the physical representation of the architecture as a hexagon, giving this pattern its name.

The hexagonal architecture differs from traditional layered architectures in that there is no 'top' or 'bottom' layer. Instead, all inputs and outputs are treated as equal and placed around the edge of the hexagon, ensuring the central application remains isolated and unaffected by external changes.

Practical Implementation of Hexagonal Architecture

A Python-based application employing hexagonal architecture can be organized into a directory structure as follows:

/bookstore
    /application_core
        __init__.py
        application_core.py
    /ports_and_adapters
        __init__.py
        ports_and_adapters.py
    /interfaces
        __init__.py
        interfaces.py
    /tests
        __init__.py
        tests.py
    main.py

Designing the Application Core

The application core encapsulates the business rules and logic of the system. Consider a simplified bookstore application, where we will have a Book entity and an Inventory service.

/application_core/application_core.py

class Book:
    def __init__(self, id, title, author, price):
        self.id = id
        self.title = title
        self.author = author
        self.price = price

class Inventory:
    def __init__(self, repository):
        self.repository = repository

    def add_book(self, book):
        return self.repository.save(book)

Creating Ports and Adapters

Here we create the ports and adapters for the system. We define a BookRepository interface (secondary port) and a concrete implementation of the interface InMemoryBookRepository (adapter).

/ports_and_adapters/ports_and_adapters.py
from abc import ABC, abstractmethod
from application_core.application_core import Book

class BookRepository(ABC):

    @abstractmethod
    def save(self, book: Book):
        pass

class InMemoryBookRepository(BookRepository):

    def __init__(self):
        self.books = {}

    def save(self, book: Book):
        self.books[book.id] = book
        return book

Developing Interfaces

Interfaces represent the primary ports in the hexagonal architecture. These are essentially the APIs that the application exposes to the outside world. For our bookstore application, we might have a simple command-line interface.

/interfaces/interfaces.py
from application_core.application_core import Book, Inventory
from ports_and_adapters.ports_and_adapters import InMemoryBookRepository

def command_line_interface():
    repository = InMemoryBookRepository()
    inventory = Inventory(repository)

    book_id = input("Enter book id: ")
    title = input("Enter book title: ")
    author = input("Enter book author: ")
    price = float(input("Enter book price: "))

    book = Book(book_id, title, author, price)

    inventory.add_book(book)
    print(f"Added book {title} to inventory.")

Implementing Tests

Tests are easier to write and maintain in a hexagonal architecture because dependencies can be replaced with test doubles. Below is a simple test case for adding a book to the inventory.

/tests/tests.py
import unittest
from application_core.application_core import Book, Inventory
from ports_and_adapters.ports_and_adapters import InMemoryBookRepository

class TestInventory(unittest.TestCase):

    def test_add_book(self):
        repository = InMemoryBookRepository()
        inventory = Inventory(repository)
        book = Book('1', 'Test Book', 'Test Author', 9.99)
        inventory.add_book(book)
        self.assertEqual(repository.books['1'], book)

if __name__ == '__main__':
    unittest.main()

In the root directory, you might have a main.py file which could look something like this:

main.py
from interfaces.interfaces import command_line_interface

if __name__ == '__main__':
    command_line_interface()

References

https://www.arhohuttunen.com/hexagonal-architecture/
https://betterprogramming.pub/a-quick-and-practical-example-of-hexagonal-architecture-in-java-8d57c419250d

Ryusei Kakujo

researchgatelinkedingithub

Focusing on data science for mobility

Bench Press 100kg!