Fra tutti le nozioni che mi sono stati impartite nel corso del tempo c’è un “principio” molto importante chiamato “ortogonalità“.
Usare questo termine richiama immediatamente il suo significato geometrico, rappresentato ad esempio da due linee perpendicolari fra loro.
Tuttavia, come spesso accade, molti termini matematici vengono recuperati ed usati in altri ambiti, con altre finalità e significati.
Anche ortogonalità non fa eccezione ed assume in informatica (o computer science per gli anglofili/angolofoni) una sua sfumatura particolare.
Nuovo significato?
Per capire questo “nuovo significato” e da dove derivi, si può ricorrere ancora all’esempio delle due rette perpendicolari fra loro: con un po’ di licenza poetica è intuitivamente possibile supporre che rimuovendone una, l’altra non ne risenta.
Da cui il significato informatico: dato un sistema e “parti” di esso, queste sono ortogonali fra loro se le modifiche su di una non comportano cambiamenti, diretti o indiretti, sull’altra.
Wikipedia al solito fornisce una definizione migliore:
“Orthogonality is a system design property facilitating feasibility and compactness of complex designs.
Orthogonality guarantees that modifying the technical effect produced by a component of a system neither creates nor propagates side effects to other components of the system.”
E’ una proprietà di un certo sistema, qualcosa che normalmente viene accuratamente pensato e preparato per esserlo: se un sistema esibisce questa proprietà è logico supporre che si possano apportare modifiche su di esso in modo agevole, perchè si ha la certezza che ogni modifica riguarderà solo ed esclusivamente la parte coinvolta da essa.
Per definizione non vi possono essere effetti collaterali, quelle modifiche subdole e non intenzionali che si propagano in modo non previsto da un componente all’altro di un sistema a causa di legami nascosti fra i componenti stessi e di cui non si è a conoscenza oppure non si è debitamente tenuto conto.
Ortogonalità e separazione dei problemi
Abbiamo definito ortogonalità come proprietà di un sistema relativamente alle sue parti. Una definizione con un “alto livello di astrazione” e che si può tradurre in modelli concreti, in cui le funzionalità fra loro intimamente legate sono raggruppate fra loro ed isolate dal resto.
Suona come qualcosa di estremamente complesso ma in realtà è qualcosa di assolutamente normale.
Ad esempio, se dobbiamo modellare un treno, la parte che rappresenta e gestisce il suo movimento è logicamente distinta da (cioè non c’entra nulla con) quella che ne modella gli orari e i percorsi da compiere giornalmente (es: “timetable”).
Sono preoccupazioni (concern) diverse e distinte, e come tali devono essere debitamente tenute separate: in gergo si parla di separare le problematiche (“Separation of Concerns“, SoC).
Ortogonalità e programmazione
Quanto detto finora appare banale: non ci vuole la terza laurea in informatica per capire che mantenere fisicamente divise cose che lo sono a livello logico rende ogni cosa più agevole.
Sfortunatamente applicare questo concetto non lo è.
Ad esempio, data una classe C++ che modella il sopra-citato treno, per ragioni di tempo si può finire ad includere tutto, anche le parti “movimento” e “gestione orari”. Tutto nella stessa classe-calderone, che alla lunga diventa un “God object” (un antipattern da evitare come la peste).
Alzi la mano chi non ha mai visto o addirittura è stato costretto a farlo, anche solo per una questione di fretta.
All’inizio, quando la classe è piccola, risulta ancora immediato distinguere ad occhio le due parti ma se la classe cresce?
Magari alla fine due funzioni membro, una appartenente a “movimento” e l’altra a “gestione orari”, finiscono improvvisamente a dipendere da una stessa funzione-membro interna (o da qualche attributo). Questa dipendenza a livello implementativo non è stata ovviamente prevista perchè in fase di progettazione non esistevano vincoli fra le due parti.
A questo punto una modifica alla funzione “comune” per sistemare un problema che riguarda la parte “movimento” potrebbe ripercuotersi in modo imprevisto e dannoso sulla parte “gestione orari” benchè le due parti siano slegate.
Un “design” più pulito prevederebbe lo sviluppo delle due parti come due classi distinte, la “funzione comune” inserita in una classe di “utilità generica” ed il treno che le include come oggetti.
In questo modo si manterrebbe tanto l’ortogonalità del “design” quanto la separazione dei concern a livello implementativo, cioè a livello di classe.
Il policy-based design
A tal proposito vi segnalo un bellissimo post intitolato “A Case for Orthogonality in Design” che a sua volta cita Andrei Alexandrescu, guru del C++.
Questi nel suo famoso libro “Modern C++ Design” introduce ed applica il cosiddetto “policy-based design“: piccole classi autocontenute che implementano ognuno un “comportamento” distinto e che possono quindi essere sostituite fra loro. Un comportamento è una sorta di “variante” per una parte del sistema. Le varie parti risultano quindi materialmente distinte e ognuna di essa viene rappresentata e concretamente implementata da uno specifico “comportamento”.
Suona complicato? A parole sì, ma in realtà il tutto è relativamente semplice, perfino usando un approccio basato sui template.
/* [I]nterfaces (not strictly required: just to make behaviors "constrained") */
class IMotion // interface
{
// defines "requirementes" as pure virtual functions, ...
};
class ITimeTable // interface
{
// defines "requirementes" as pure virtual functions, ...
};
/* [C]oncrete implementations */
class StdMotion: public IMotion
{
// ...
};
class MaglevMotion: public IMotion
{
// ...
};
class WorkHoursTimeTable: public ITimeTable
{
// ...
};
<template typename TMotion, typename TTimeTable>
class Train
{
private:
TMotion motion_;
TTimeTable timetable_;
// ...
};
/* [I]nstances */
Train<StdMotion, WorkHoursTimetable> aTrain;
Train<MaglevMotion, WorkHoursTimetable> aMaglevTrain;
La separazione dei concern, che mantiene anche l’ortogonalità logica del sistema modellato, è evidente: il treno è composto da parti distinte e per ognuna di esse si può scegliere un “comportamento” specifico. La classe train è a tutti gli effetti una classe policy-based, composta da piccole classi-comportamento che vengono “inserite” tramite template (aka: programmazione generica).
Una definizione pomposa per qualcosa di relativamente semplice da implementare.
Questo approccio di fatto è ubiquo, nel senso che si può trovarlo ovunque.
Gli stessi container della STL ne fanno ricorso. Ad esempio un’implementazione normale della classe std::vector, compatibile cioè con lo standard, è definita così:
namespace std {
template <class T, class Allocator = allocator<T> > class vector;
// ...
Si nota subito come l’allocazione della memoria, una parte importantissima del sistema-vettore nonchè “concern“, è modificabile se necessario.
Cioè, invece dell’allocatore standard (argomento di default) si può usarne un altro (es: l’eastl_allocator che fa parte della “Electronic Arts STL“, EASTL, molto usata in ambito videogiochi), applicare cioè un’altra classe-behaviour.
Conclusioni
In questo breve post abbiamo rapidamente visto cosa sia l’ortogonalità in informatica, a cosa serva e come “implementarla” nel codice. Spero che abbiate gradito.
Chiudo segnalandovi l’eccellente post “Small, Orthogonal, and Loosely Coupled“ che riassume ed amplia un po’ il discorso introducendo anche il concetto di loose coupling (cioè “dipendenza lasca o addirittura nulla”) fra classi.
Che ne pensate?
Buongiorno JP,
bel post e sopratutto buoni riferimenti che mi andrò subito a leggere. Quando lessi per la prima volta il concetto del policy-based in Modern C++ Design me ne sono subito innamorato anche se sono veramente poche le volte che l’ho utilizzato (lo reputo un grandissimo strumento ma più adatto ad un codice-libreria)
Nel prossimo post che ieri ho iniziato a scrivere ne faccio uso anche se in modo molto soft.
Una nota mi sento di farti; nelle due interfacce commenti così:
// defines “requirementes” as pure virtual functions, …
perchè? Qua l’unico vantaggio di avere un pure virtual è per “simulare” un’interfaccia “alla Java” e quindi obbligare a farne un’implementazione concreta. Personalmente evito poichè pagheresti per nulla il costo del virtual* senza ricavarne alcun vantaggio. Infatti, essendo tutto a binding statico, non abbiamo polimorfismo (e non ne avrebbe neanche senso).
Anche se il compilatore, conoscenso il tipo a compile time, sicuramente non passa dalla vtable….
Ciao!
@Martino: non c’era una particolare necessità nel simulare e imporre le interfacce, quanto nel far capire che ci si aspetta che i vari “behavior” rispettino una forma di “protocollo comune“, di “vincoli”. Ho aggiunto un commento nel codice epr esplicitarlo.
Pagare il “virtual“? Come giustamente hai fatto notare, non è detto e in questo caso è quasi garantito che non si paghi nulla a meno di un compilatore particolarmente inefficiente.
I compilatori intelligenti sono in grado di analizzare le chiamate e perfino sopprimere gli accessi inutili alle Virtual Table. Esempio: MSVC con Profile-guided Optimization attivata:
Tale ottimizzazione è anche nota come “devirtualization“. Cfr. ad esempio “Optimizing software in C++: An optimization guide for Windows, Linux and Mac platforms” di Agner, cap. 8.1:
In ogni casi, ribadisco, era solo un esempio. ^^’
Ciao & grazie di essere passato. Attendo il tuo post.
[...] This post was mentioned on Twitter by Andrea Marin, Gian Paolo Ghilardi. Gian Paolo Ghilardi said: Ortogonalità, Separation of Concerns & Co….: http://wp.me/p9z1q-2da [...]
Ciao JP !
Quello che ieri molto faticosamente ho cercato di scrivere, e che Safari mi ha spudoratamente cancellato, era una piccola nota di colore sul fatto che il libro “The Pragmatic Programmer” spiega il concetto di ortogonalità utilizzando come esempio il sistema di comandi di un elicottero.
Abbastanza bizzarro, perché di solito l’esempio usato come paragone di un concetto dovrebbe essere (più o meno) alla portata di tutti. In questo caso invece sembra più complesso l’esempio rispetto al concetto. La cosa buffa è il fatto che ho imparato qualcosa di nuovo sugli elicotteri a partire da una spiegazione sull’ortogonalità.
Tutto qui.
(Safari, beccati questa)
@Stefano: mi spiace per la tua disavventura con Safari… ^^’
In effetti gli esempi dovrebbero aumentare la comprensione e non comprometterla ma spesso tutto non va secondo i piani… ^^’
Ciao e grazie di essere passato!