Connascence: ovvero come misurare l’accoppiamento dei vostri servizi o moduli software.

Danilo Del Fio
6 min readMay 12, 2022
https://khalilstemmler.com/wiki/coupling-cohesion-connascence/

Introduzione

Creare servizi o moduli software che siano quanto più coesi e che abbiano un basso accoppiamento è diventato un mantra per la suddivisione di una soluzione software in diverse parti e fare in modo che queste parti siano più indipendenti possibile le une dalle altre e che necessitino di poche interazioni tra di esse.

A volte, però, tale definizione non basta a definirne “i confini” in modo netto, perché l’interdipendenza tra servizi e/o moduli software può dipendere da diversi fattori, siano essi funzionali o meno.

Iniziamo, per essempio, col dire che ci sono due tipi di “accoppiamento”.

Accoppiamento afferente

Misura il numero di connessioni in ingresso a un artefatto di codice (componente, classe, funzione e così via).

Accoppiamento efferente

Che misura le connessioni in uscita ad altri artefatti del codice. Per quasi tutte le piattaforme esistono strumenti che consentono agli architetti di analizzare le caratteristiche di accoppiamento del codice al fine di aiutare nella ristrutturazione, migrazione o comprensione di una base di codice.

Definizione di Connascence

Two components are connascent if a change in one would require the other to be modified in order to maintain the overall correctness of the system. — Meilir Page-Jones

La connascence, è una misura di quanto il software che scriviamo sia o meno “accoppiato”, ovvero che il cambiamento di una parte di codice determini il cambiamento di un’altra parte che in qualche modo interagisce con essa.

Tipi di Connascence

La Connascence si divide in Statica e Dinamica.

La connascence statica è direttamente legata al codice che scriviamo e rappresenta un accoppiamento a livello di codice sorgente, mentre quella dinamica si riferisce all’accoppiamento a runtime (che come vedremo è un livello di accoppiamento peggiore).

Connascence Statiche

Partiamo da quelle statiche che, sebbene possano creare problemi, sono più facilmente individuabili e gestibili.

Nome

La connascence del nome è quando più componenti devono accordarsi sul nome di un’entità.
Facendo un esempio con un linguaggio di programmazione ad oggetti, un primo meccanismo di “accoppiamento” è rappresentato indubbiamente dal nome dela classe e dai nomi dei metodi, per esempio, perché se decidessimo di modificarli, dovremmo modificare anche tutti i riferimenti ad essi.

Tipo

Le entità, in questo caso, devono essere di un certo tipo univocamente indentificato.
Alcuni linguaggi di programmazione “tipizzati”, come Typescript o Java, per esempio, tendono ad evitare che si vada incontro a questo tipo di problematica.

Significato (Convenzione)

Un valore deve avere un significato univoco, riconosciuto da chiunque acceda a tale valore.
Questo problema è evidenziato, per esempio, definendo valori hard-coded, che potremmo evitare ceando costanti che, quindi, porteranno lo stesso valore e lo stesso significato ovunque vengano usate.

Posizione

L’ordine dei “valori” deve essere riconosciuto dai vari componenti che ci accedono.
Questo aspetto è fondamentale, per esempio, nella specifica dei parametri di una funzione. Potremmo avere, infatti, più parametri dello stesso tipo nella firma di un metodo, ma con un significato completamente diverso.
Per esempio:

updateSeats(String name, String seatLocation);

Se la funzione venisse chiamata con:

updateSeats("14D", "Danilo Del Fio");

avremmo un problema legato alla posizione dei parametri.

Algoritmo

I vari componenti del nostro software devono concordare su un particolare algoritmo.
Un caso comune per questo tipo di connascence si verifica quando uno sviluppatore definisce un algoritmo di hashing di sicurezza che deve essere eseguito sia sul server che sul client e produrre risultati identici per autenticare l’utente.
Ovviamente questa soluzione crea un enorme problema di accoppiamento, perché ogni cambiamento all’algoritmo dal un lato deve riflettere un altrettanto cambiamento.

Connascence Dinamiche

Le connascence dinamiche sono più pericolose e più difficili da individuare e gestire, perché riguardano accoppiamenti a runtime delle nostre applicazioni.

Ordine di esecuzione

L’ordine con cui dobbiamo eseguire determinate istruzioni è importante e potrebbe creare problemi nell’esecuzione stessa.

email = new Email();
email.setRecipient("you@gmail.com");
email.setSender("me@gmail.com");
email.send();
email.setSubject("whoops");

Come vediamo dall’esempio, sbagliando l’ordine di esecuzione andiamo incontro ad un problema, perché non abbiamo definito un soggetto della nostra email, prima di chiamare la funzione di invio.

Timing

La tempistica dell’esecuzione di più componenti è importante.
Il caso comune di questo tipo di connascence è una race condition causata da due thread in esecuzione contemporaneamente, che influiscono sull’esito dell’operazione congiunta.

Valori

Si verifica quando più valori sono correlati tra loro e devono cambiare insieme.
Considera il caso in cui uno sviluppatore ha definito un rettangolo come quattro punti, che rappresentano i vertici dello stesso.
Per mantenere l’integrità della struttura dei dati, lo sviluppatore non può modificare casualmente uno dei punti senza considerare l’impatto sugli altri punti.
Il caso più comune e problematico riguarda le transazioni, soprattutto nei sistemi distribuiti. Il problema, infatti, nasce quando definiamo un’architettura con database separati, ma la modifica di un valore in uno di essi deve riflettere quella in un altro database. In questi casi dobbiamo trovare il modo di creare architetture che supportino la cosiddetta “Eventual Consistency” (ovvero basarci sul fatto che i dati saranno consistenti in un momento del futuro).

Identità

Si verifica quando più componenti devono fare riferimento alla stessa entità.
L’esempio comune di questo tipo di connascence coinvolge due componenti indipendenti che devono condividere e aggiornare una struttura di dati comune, come una coda distribuita.
Quest’ultimo tipo di connascence è il più complesso da gestire ed è uno dei motivi per cui si dovrebbe creare un database per ogni servizio in una architettura a microservizi. Un database comune sarebbe un enorme fonte di accoppiamento tra i servizi stessi.

Connascence è uno strumento di analisi per architetti e sviluppatori e alcune proprietà di connascence aiutano gli sviluppatori a utilizzarlo al meglio.

https://medium.com/interviewnoodle/the-notes-for-fundamentals-of-software-architecture-2-9186b65400ae

Proprietà di Connascence

Forza

Definiamo come forza della connascence la facilità con cui uno sviluppatore può rifattorizzare quel tipo di accoppiamento; alcuni tipi di connascence sono più facili da gestire rispetto ad altri, come mostrato nell’immagine sopra.
Architetti e sviluppatori possono migliorare le caratteristiche di accoppiamento del codice effettuando il refactoring verso tipi migliori di connascence (indicato dalla freccia).

Località

Questa proprietà misura quanto siano vicine le due entità l’una all’altra.
Il codice più vicino, ovvero nello stesso modulo, classe o funzione, per esempio, dovrebbe avere forme di connascence maggiori rispetto a un codice più separato (in altri moduli o altre codebase).
In altre parole, le forme di connascence che indicano uno scarso accoppiamento quando sono distanti sono molto più innocue quando sono più vicine.
Ad esempio, se due classi nello stesso componente hanno una connascence di significato, è meno dannoso per la base di codice che se due componenti hanno la stessa forma di connascence.
Gli sviluppatori devono considerare insieme la forza e la località. Forme più forti di connascence che si trovano all’interno dello stesso modulo rappresentano un odore di codice minore rispetto alla stessa connascence dispersa.

Grado

Il grado di connascence è correlato alla dimensione del suo impatto: ha un impatto su poche o molte classi?
Gradi minori di connascence hanno, ovviamente, meno impatto sul nostro codice.
Questo porta con se il corollario che, avere un’elevata connascence dinamica non è terribile se hai solo pochi moduli.
Il problema è che in genere i repository tendono a crescere, rendendo quello quello che prima era un piccolo problema, molto più grande, con notevoli conseguenze di accoppiamento nel codice, che porta ad un rework continuo man mano che la dimensione della nostra codebase aumenta.

Jim Weirich, che riportò il concetto di connascence alla ribalta, ci fornisce due consigli:

  • Regola di grado (cerchiamo di ridurre il grado di connascence): convertire forme forti di connascence in forme più deboli di connascence.
  • Regola di località: all’aumentare della distanza tra gli elementi del software, utilizzare forme di conascenza più deboli (nelle architetture a microservizi, infatti, si parla di comunicazione lightweight tra i vari servizi).

Conclusione

In questo breve articolo ho cercato di dare una chiave di lettura per la creazione e gestione di soluzioni software, delegando a diversi moduli o servizi il più possibile indipendenti, l’implementazione delle nostre logiche di business.
E’ necessario, soprattutto al crescere delle dimensioni del progetto, fare in modo che ci sia meno accoppiamento possibile tra le varie parti.

Raccogliendo le idee intorno ai concetti della connascence, infatti, potremmo essere in grado di identificare, ed evitare, problemi di interdipendenza, per evitare continui rework, perdite di tempo nel refactory del codice e arrivare al punto che il cambiamento di qualche linea di codice abbia regressioni in altri componenti.

If you want to read the same argument in English, please refer to:

--

--

Danilo Del Fio

Transparency, respect and self-discipline, mixed with great passion!