L’evoluzione dell’ingegneria del software ha portato alla nascita di paradigmi architetturali sempre più sofisticati, tra cui spicca l’architettura esagonale, introdotta da Alistair Cockburn nel 2005.

Questo approccio innovativo, noto anche come pattern ‘Ports and Adapters’, rappresenta una risposta alle limitazioni delle tradizionali architetture a livelli e offre una soluzione elegante per costruire sistemi software robusti e facilmente mantenibili.

Le architetture tradizionali a tre livelli (presentazione, business logic e persistenza) hanno dominato il panorama dello sviluppo software per decenni ma questo approccio presenta significative limitazioni strutturali che ne compromettono la flessibilità e la testabilità. La dipendenza gerarchica dall’alto verso il basso crea un forte accoppiamento tra i componenti, rendendo il software rigido e difficile da modificare.

Quando la logica di business si infiltra nei livelli di presentazione o persistenza, emerge il problema della duplicazione del codice e della difficoltà di testing, compromettendo la qualità complessiva del sistema.

L’architettura esagonale ribalta questa prospettiva, proponendo un modello concentrico che pone al centro la logica di dominio, protetta dalle influenze esterne. Questa visione inside-out elimina la necessità di pensare in termini di ‘sopra’ e ‘sotto’, concentrandosi invece sulla separazione netta tra il cuore dell’applicazione e il mondo esterno. Il cuore dell’architettura esagonale è costituito da tre elementi fondamentali che lavorano in sinergia per garantire un disaccoppiamento efficace.

Il dominio o ‘domain’ rappresenta il nucleo centrale dell’applicazione, contenendo esclusivamente, ad esempio, le entità, le interfacce dei servizi e i validatori. Questo livello non contiene alcuna logica implementativa, ma solo le definizioni pure che stabiliscono le regole del gioco del business.

Un esempio pratico di definizione nel dominio potrebbe essere:

// domain/user/index.ts
export interface UserService {
  createUser(userData: CreateUserRequest): Promise<User>;
  findUserById(id: string): Promise<User | null>;
  validateUser(user: User): ValidationResult;
}

export interface CreateUserRequest {
  email: string;
  password: string;
  name: string;
}

export interface User {
  id: string;
  email: string;
  name: string;
  createdAt: Date;
}

Attorno al dominio si sviluppa il livello applicativo o ‘application’, che rappresenta il vero cervello del sistema.

Qui risiede la business logic, l’orchestrazione dei casi d’uso e la logica che determina come i diversi componenti devono interagire per raggiungere gli obiettivi prefissati. Questo livello dipende esclusivamente dalle interfacce definite nel dominio, mantenendo la propria indipendenza dalle implementazioni concrete.

Nel configuratore dell’applicazione, i servizi vengono istanziati e resi disponibili:

// application/configurator.ts
import { UserService } from '../domain/user';
import { DrizzleUserService } from '../adapters/drizzle/user';

export class ServiceConfigurator {
  private userService: UserService;

  constructor() {
    this.userService = new DrizzleUserService();
  }

  getUserService(): UserService {
    return this.userService;
  }
}

La corona esterna è formata dagli adattatori o ‘adapter’, i ponti che collegano l’applicazione al mondo esterno.

Questi componenti si occupano di tradurre le richieste dell’applicazione in chiamate concrete verso database, servizi di autenticazione, API esterne e altri sistemi. Gli adapter implementano le interfacce definite nel dominio, garantendo che il cambiamento di una tecnologia esterna non comprometta la stabilità del sistema. Un esempio di adapter concreto per la gestione degli utenti con database potrebbe essere:

// adapters/drizzle/user/index.ts
import { UserService, CreateUserRequest, User } from '../../../domain/user';
import { db } from '../connection';

export class DrizzleUserService implements UserService {
  async createUser(userData: CreateUserRequest): Promise<User> {
    const newUser = await db.insert(usersTable).values({
      email: userData.email,
      password: hashPassword(userData.password),
      name: userData.name,
      createdAt: new Date()
    }).returning();
    
    return newUser[0];
  }

  async findUserById(id: string): Promise<User | null> {
    const users = await db.select().from(usersTable).where(eq(usersTable.id, id));
    return users[0] || null;
  }

  validateUser(user: User): ValidationResult {
    // Implementazione della validazione
    return { isValid: true, errors: [] };
  }
}

Il meccanismo di funzionamento dell’architettura esagonale si basa sul concetto di porte e adattatori. Le porte fungono da punti di ingresso tecnologicamente neutri, definendo le interfacce che permettono la comunicazione tra il nucleo dell’applicazione e il mondo esterno. Gli adattatori implementano queste porte utilizzando tecnologie specifiche, fungendo da traduttori tra il linguaggio del dominio e quello delle tecnologie esterne.

Un esempio di utilizzo nell’API potrebbe essere:

// application/api/users/index.tsx
import { ServiceConfigurator } from '../../configurator';

const configurator = new ServiceConfigurator();
const userService = configurator.getUserService();

export async function POST(request: Request) {
  const userData = await request.json();
  
  try {
    const newUser = await userService.createUser(userData);
    return Response.json(newUser, { status: 201 });
  } catch (error) {
    return Response.json({ error: 'Failed to create user' }, { status: 500 });
  }
}

L’adozione dell’architettura esagonale porta benefici concreti e misurabili. La testabilità del software migliora drasticamente grazie al disaccoppiamento intrinseco tra i componenti. È possibile testare la logica di business in completo isolamento, utilizzando mock degli adapter per simulare le dipendenze esterne. La manutenibilità del codice aumenta significativamente grazie alla chiara separazione delle responsabilità, mentre la scalabilità del sistema beneficia della possibilità di sostituire o aggiornare singoli componenti senza impattare l’intera architettura.

L’implementazione dell’architettura esagonale richiede un investimento iniziale in termini di progettazione e sviluppo, ma rappresenta uno strumento potente per costruire sistemi software che privilegiano la flessibilità, la testabilità e la manutenibilità. La sua adozione richiede un cambio di mentalità che sposta l’attenzione dalla tecnologia al dominio, creando applicazioni che riflettono fedelmente le esigenze del business e si adattano naturalmente ai cambiamenti del mercato.

Per saperne di più: https://alistair.cockburn.us/hexagonal-architecture

Contattaci e scrivi brevemente ciò di cui hai bisogno.

Sarai ricontattato nel più breve tempo possibile.


Se preferisci, puoi prenotare direttamente un appuntamento cliccando qui.