Fitness function-driven development – Prima Parte

Fitness function-driven development – Prima Parte

Una delle principali attività a carico di un software architect è la creazione di una architettura che possa evolvere con le esigenze del Cliente. Il nostro software, una volta rilasciato, è destinato ad evolvere continuamente, ad adeguarsi alle esigenze dei nostri Clienti, alle continue richieste di nuove funzionalità inizialmente non previste, ma deve anche garantire una serie di requisiti non funzionali come la scalabilità, l’affidabilità, l’osservabilità e altre “-abilità” (gli inglesi scriverebbero “-ilities”). Come possiamo garantire l’operatività e la resilienza delle funzionalità quando i nostri applicativi vengono messi in produzione?

L’idea che l’architettura possa sostenere il cambiamento è descritta nel libro “Building Evolutionary Architectures” di Neal Ford, Rebecca Parsons e Pat Kua, giunto alla seconda edizione.

L’ “Architettura Evolutiva”, meglio Evolutionary Architecture si divide sostanzialmente in due aree, meccanica e strutturale.

Mechanics: Tratta le pratiche ingegneristiche e le verifiche che consentono ad un’architettura di evolvere. Si tratta di pratiche quali test, metriche e hosting.

Structure: l’altro aspetto di una Architettura Evolutiva tratta la struttura, gli aspetti topologici del nostro software. Verifica quali stili architetturali meglio si aadicono nel costruire un sistema in grado di evolvere.

L’Evoluzione del Software

Come possiamo misurare il deterioramento del nostro sistema nel tempo? Esiste un indice, o una semplice indicazione, che ci possa dimostrare in maniera deterministica che il nostro software sta degradando in termini di prestazioni, sicurazza, scalabilità?

bit rot o software rot, è un lento deterioramento della qualità del software nel corso del tempo, o una diminuzione della sua reattività che finirà per rendere il software difettoso, inutilizzabile e/o da aggiornare. Non si tratta di un fenomeno prettamente fisico, nel senso che il software non decade realmente, ma chi lo utilizza ne rileva una mancanza di reattività, o semplicemente una mancanza di aggiornamento rispetto all’ambiente mutevole in cui si trova. In pratica, appare vecchio!

Cosa sono le fitness functions?

Una volta appurato il problema, come possiamo gestire l’evoluzione del nostro sistema? Gli obiettivi ed i vincoli architetturali possono cambiare indipendentemente dalle aspettative funzionali. Per esempio, possiamo decidere di spostare il deploy da una struttura on-premise al Cloud, sapendo che il nostro software è stato pensato per questo. A livello funzionale nessuno si accorgerà del cambiamento, ma a livello strutturale le pipeline di rilascio verranno sicuramente aggiornate.

Se a livello di sviluppo software abbiamo delle tecniche consolidate che ci proteggono durante i refactor del codice, si veda TDD, ma anche BDD, a livello architetturale, come possiamo garantire la stessa aspettativa? Le fitness functions descrivono quanto un’architettura sia vicina al raggiungimento di un obiettivo architettturale, sono un meccanismo che fornisce una valutazione oggettiva dell’integrità di alcune caratteristiche architetturali.

Indipendentemente dall’architettura della nostra applicazione (monolite, microservizi, etc) lo sviluppo guidato dalle fitness function può introdurre un feedback continuo per la conformità architettonica e informare il processo di sviluppo mentre avviene, anzichè trovarsi a posteriori a ripare i danni. Ad esempio, in un’applicazione monolite, pur partendo con l’idea di mantenere le responsabilità di ogni modulo indipendenti dagli altri moduli dell’applicazione, può capitare, anche grazie all’IDE che ci suggerisce in automatico di aggiungere una referenza ad una funzione già presente in un altro modulo appunto, di rompere questo vincolo. Una fitness function ci garantisce che questo non possa capitare. Un ottimo tool, sia per Java che per .NET, è ArchUnit

Un esempio in C# che ci garantisce che il modulo Sales non abbia dipendenze dirette con il modulo Warehouse

using ArchUnitNET.Domain;
using ArchUnitNET.Fluent;
using ArchUnitNET.Loader;
using ArchUnitNET.xUnit;
using Brewup.Modules.Sales.Abstracts;

using static ArchUnitNET.Fluent.ArchRuleDefinition;

namespace Brewup.Modules.Sales.Fitness.Tests;

public class SalesFitnessTest
{
	private static readonly Architecture Architecture = new ArchLoader().LoadAssemblies(typeof(ISalesOrchestrator).Assembly)
		.Build();

	private readonly IObjectProvider<IType> _forbiddenLayer = Types().
		That().
		ResideInNamespace("Brewup.Modules.Warehouse").
		As("Forbidden Layer");
	private readonly IObjectProvider<Interface> _forbiddenInterfaces = Interfaces().
		That().
		HaveFullNameContaining("Warehouse").
		As("Forbidden Interfaces");

	[Fact]
	public void SalesTypesShouldBeInCorrectLayer()
	{
		IArchRule forbiddenInterfacesShouldBeInForbiddenLayer =
			Interfaces().That().Are(_forbiddenInterfaces).Should().Be(_forbiddenLayer);

		forbiddenInterfacesShouldBeInForbiddenLayer.Check(Architecture);
	}
}

Dove iniziare?

Così come, per la raccolta delle specifiche funzionali, il modo migliore è riunire tutti i soggetti coinvolti, stakeholder, utenti, dev, in una stanza, per la raccolta delle specifiche strutturali. Ovviamente, in questo caso, gli obiettivi sono gli attributi architetturali che considerano più importanti per il successo del prodotto che spesso, se non sempre, si allineano alle “-abilità” (“-ilities”) architettoniche. Al termine di questa raccolta si raggruppano i risultati in temi comuni, come resilienza, operatività e stabilità.

Una volta raggruppati gli obiettivi scopriremo che la famosa coperta di Linus è sempre troppo corta, perchè gli obiettivi di flessibilità possono essere in conflitto con quelli di stabilità e resilienza. Per favorire l’agilità dobbiamo ridurre le barriere al cambiamento, che è la pratica opposta al mantenimento della stabilità che ci richiede di alzare delle barriere per ridurre il cambiamento e mantenere il sistema stabile. E così si possono trovare altre innumerevoli situazioni. Si inizia quindi un esercizio di bilanciamento e ordinamento delle priorità con l’obiettivo di produrre le funzioni di fitness desiderate.

Una volta raccolte le funzioni di fitness è necessario redigerle in un framework di test. Spesso i framework sono molteplici, ognuno dedicato ad uno specifico obiettivo. Prima abbiamo visto un framework, ArchUnit, che ci può supportare nello sviluppo, ma per il monitoring ci dovremo affidare a qualche tool di orchestrazione, che sia in grado di valutare automaticamente se il sistema è in salute, o sotto pressione. Le stess pipeline di rilascio automatico devono poter valutare le metriche esposte dai tool per consentire o meno il rilascio della nuova versione. L’importante è mantenere la buona abitudine di revisionare periodicamente le funzioni di fitness e verificare se sono ancora adeguate, oppure se la scala delle priorità è cambiate e vanno aggiornate di conseguenza.

Categorie di Architectural Fitness Function

Le fitness function esistono in una varietà di categorie legate al loro ambito, alla cadenza, al risultato, all’invocazione e alla copertura

Scope: Atomic vs Holistic

Atomic: le fitness function vengono eseguite in un unico contesto e verificano un aspetto particolare dell’architettura. Un esempio è uno unit-test che verifica l’accoppiamento modulare (come visto sopra) o la complessità ciclomatica.

Holistic: le fitness function vengono eseguite in un contesto condiviso ed esercitano una combinazione di aspetti architettonici come la sicurezza e la scalabilità.

Cadence: Triggered vs Continual vs Temporal

Triggered: le fitness function vengono eseguite in base ad un evento particolare, come l’esecuzione di un test unitario o l’esecuzione di test esplorativi da parte di un responsabile alla QA

Continual tests: eseguono una verifica costante di un aspetto architettonico, come la velocità delle transazioni. MDD (Monitoring Driven Development) è una tecnica di test che sta crescendo in popolarità come strumento per valutare in produzione la salute, sia tecnica che commerciale, di un sistema anzichè affidarsi esclusivamente ai test

Temporal: le fitness function in questo caso vengono eseguite con cadenza regolare. Un esempio è rappresentato dal monitoraggio delle librerie utilizzate per la crittografia. Lo scopo della fitness function è avvisare il team che è tempo di verificare la validità della libreria, in maniera più o meno automatica.

Result: Static vs Dynamic

Static: si tratta di fitness function con un risultato binario, come il superamento o il fallimento di un test unitario

Dynamic: in questo caso le fitness function si basano su una definizione mutevole basata su un contesto aggiuntivo, come un test di verifica della scalabilità e della reattività del tempo di request/response per un certo numero di utenti

Invocation: Automated vs Manual

Automated: queste fitness function vengono eseguite in un contesto automatizzato, proprio come piace agli architetti del software

Manual: sono fitness function che richiedono la verifica di processi basati sulla persona

Conclusioni

Con questa introduzione alle fitness function abbiamo visto che anche l’Architettura, come l’infrastruttura ed il codice stesso, può essere monitorata e testata. Quali vantaggi ci portano queste fitness function?

In primo luogo ci consentono di misurare in modo oggettivo il debito tecnico e promuovere la qualità del codice. Abbiamo visto come gli IDE moderni troppo facilmente ci risolvono dipendenze a volte scomode, e come noi dev, a volte per progrizia, le accettiamo incodizionatamente. Test specifici ci aiutano a monitorare questo spiacevole accoppiamento. Altre funzioni sono di supporto ai Team che si occupano di sicurezza, ad esempio.

In generale possiamo affermare che lo sviluppo guidato dalle fitness function comunica gli standard architetturali come codice e consente ai Team di sviluppo di fornire funzionalità allineate ad essi. Allo stesso modo in cui gli utenti di un’applicazione chiedono modifiche alle funzionalità, così chi si occupa di architettura può richiedere modifiche ad essa, come trasformare un monolite in un’architettura a microservizi. L’inclusione di queste funzioni all’interno delle pipeline di compilazione e distribuzione consente ai Team di creare servizi operativi sicuri e conformi, rispettosi di tutte le proprietà “-abilità” richieste ad software moderno e scalabile.

Con questo articolo abbiamo introdotto il mondo delle fitness function. Nel prossimo capitolo entreremo più nel dettaglio ed analizzeremo in dettaglio i vari approcci. Come al solito, se l’articolo vi è piaciuto, state tuned.

A chi volesse approfondire l’argomento consiglio il libro “Building Evolutionary Architectures” (2nd Edition)

Condividi questo post...

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *