Learn

Fitness function-driven development

15 Maggio 2023 - 7 minuti di lettura

Una delle principali attività a carico di un software architect è la creazione di una architettura che possa evolvere con le esigenze del cliente. Un software, una volta rilasciato, è destinato a evolvere continuamente e ad adeguarsi sia alle esigenze dei clienti che alle continue richieste di nuove funzionalità inizialmente non previste. Lo stesso software deve anche garantire una serie di requisiti non funzionali come la scalabilità, l’affidabilità, l’osservabilità e altre “-abilità” (in inglese, “-ilities”).

Come possiamo garantire l’operatività e la resilienza delle funzionalità quando gli applicativi vengono messi in produzione?

In questo articolo vi introdurrò il concetto di Fitness function e l’approccio Fitness function-driven development, integrando la spiegazione con del codice di esempio.

Premessa – L’Architettura Evolutiva ed evoluzione del software

Architettura Evolutiva

L’idea che l’architettura possa sostenere il cambiamento è descritta nel libro “Building Evolutionary Architectures“, giunto alla seconda edizione.

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

  • Mechanics: l’insieme delle pratiche ingegneristiche e delle verifiche che consentono a un’architettura di evolvere. In quest’area rientrano pratiche note quali test, l’adozione di metriche e hosting.
  • Structure: l’altro aspetto di una Architettura Evolutiva tratta la struttura, gli aspetti topologici di un progetto software. Verifica quali stili architetturali meglio si addicono nel costruire un sistema in grado di evolvere.

L’evoluzione del software

Come possiamo misurare il deterioramento di un sistema nel tempo? Esiste un indice, o una semplice indicazione, che ci possa dimostrare in maniera deterministica che il software sviluppato stia degradando in termini di prestazioni, di sicurezza e di scalabilità?

Con le espressioni “bit rot” o “software rot” ci si riferisce a 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, “il software appare vecchio“!

È qui che entra in gioco il concetto di fitness function.

Fitness function

Una volta appurato il problema, come si può gestire l’evoluzione di un sistema?
Gli obiettivi e i vincoli architetturali possono cambiare indipendentemente dalle aspettative funzionali. Per esempio, si può decidere di spostare il deploy da una struttura on-premise al cloud, consapevoli che il software è stato pensato per questo cambiamento. 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 vengono utilizzate delle tecniche consolidate – TDD o anche BDD – che “proteggono” durante le fasi di refactor del codice, a livello architetturale come si può garantire la stessa aspettativa? Le fitness function descrivono quanto un’architettura sia vicina al raggiungimento di un obiettivo architettonico, sono un meccanismo che fornisce una valutazione oggettiva dell’integrità di alcune caratteristiche architettoniche.

Indipendentemente dall’architettura dell’applicazione (monolite, a microservizi ecc.) 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 riparare i danni.

Ad esempio, in un’applicazione monolite, pur partendo con l’idea di mantenere le responsabilità di ogni modulo indipendenti dagli altri può capitare, anche grazie all’IDE di sviluppo che suggerisce in automatico di aggiungere una referenza a una funzione già presente in un altro modulo appunto, di rompere questo vincolo. Una fitness function garantisce che questo non possa capitare. Un ottimo strumento, valido sia per Java che per .NET, è ArchUnit, utilizzato nel codice seguente.

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 _forbiddenLayer = Types().
          That().
          ResideInNamespace("Brewup.Modules.Warehouse").
          As("Forbidden Layer");

      private readonly IObjectProvider _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 avviene per la fase di raccolta delle specifiche funzionali, il modo migliore per iniziare a lavorare con le fitness function è riunire tutti i soggetti coinvolti (stakeholder, utenti, sviluppatori) in una stanza per la raccolta delle specifiche strutturali. Ovviamente, in questo caso, gli obiettivi sono gli attributi architetturali che si 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 quali resilienza, operatività e stabilità.

Una volta raggruppati gli obiettivi si scoprirà 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à si devono ridurre, se non abbattere, le barriere al cambiamento, che è la pratica opposta al mantenimento della stabilità che richiede di alzare delle barriere per ridurre il cambiamento e mantenere così il sistema stabile. Si inizia quindi un esercizio di bilanciamento e di ordinamento delle priorità con l’obiettivo di produrre le funzioni di fitness desiderate.

Raccolte le funzioni di fitness è necessario redigerle in un framework di test e di questi ne esistono molteplici, ognuno dedicato a uno specifico obiettivo. ArchUnit, che ho citato, è utile nello sviluppo, ma per le attività di monitoring ci si dovrà affidare a qualche strumento di orchestrazione che sia in grado di valutare automaticamente se il sistema è in salute, o sotto pressione. Le stesse pipeline di rilascio automatico devono poter valutare le metriche esposte dagli strumenti 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à è cambiata e vanno di conseguenza aggiornate.

Categorie di fitness function architetturali

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

Ambito: Atomic vs Holistic

Le fitness function atomiche 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 in precedenza) o la complessità ciclomatica.
Le fitness function olistiche lavorano in un contesto condiviso ed esercitano una combinazione di aspetti architettonici come la sicurezza e la scalabilità.

Cadenza: Triggered vs Continual vs Temporal

Le fitness function “triggered” vengono eseguite in base a un evento particolare, come l’esecuzione di un test unitario o l’esecuzione di test esplorativi da parte di un responsabile alla Quality Assurance.
Le fitness function “continual” eseguono una verifica costante di un aspetto architettonico, come la velocità delle transazioni. La tecnica MDD (Monitoring Driven Development) sta crescendo in popolarità come strumento per la valutazione, in produzione, della salute sia tecnica che commerciale di un sistema anziché affidarsi esclusivamente ai test.
Infine, le fitness function “temporal” sono quelle che 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.

Risultato: Static vs Dynamic

Quando si parla di fitness function statiche ci si riferisce a funzioni con un risultato binario, come il superamento o il fallimento di un test unitario.
Le fitness function dinamiche si basano invece 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

Come suggerisce il nome, le fitness function automatiche vengono eseguite in un contesto automatizzato, proprio come piace ai software architect.
Per quanto riguarda le fitness function manuali, sono quelle che richiedono la verifica di processi basati sulla persona.

Conclusioni

Con questa introduzione alle fitness function vi ho mostrato che anche l’architettura di un software, come la sua infrastruttura e il codice, può essere monitorata e testata.

Quali vantaggi portano le fitness function?

In primo luogo consentono di misurare in modo oggettivo il debito tecnico e promuovere la qualità del codice. Nell’esempio che ho riportato, abbiamo visto come gli IDE moderni troppo facilmente risolvono dipendenze a volte scomode e come noi sviluppatori, a volte per pigrizia, le accettiamo incondizionatamente. Test specifici aiutano a monitorare questo spiacevole accoppiamento. Altre funzioni sono di supporto ai team che si occupano di sicurezza, per esempio.

In generale si può affermare che lo sviluppo guidato dalle fitness function comunica gli standard architetturali come codice e consente ai team di sviluppo di fornire funzionalità allineate a essi. Allo stesso modo in cui gli utenti di un’applicazione chiedono modifiche alle funzionalità, così chi si occupa di architettura può richiedere modifiche a 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 a un software moderno e scalabile.

Per chi volesse approfondire l’argomento, consiglio il libro “Building Evolutionary Architectures (2nd Edition)“.

Articolo scritto da