Come indicato in alcune risposte e commenti, i DTO sono appropriati e utili in alcune situazioni, in particolare nel trasferimento di dati oltre i confini (ad es. Serializzazione su JSON per l'invio tramite un servizio Web). Per il resto di questa risposta, lo ignorerò più o meno e parlerò delle classi di dominio e di come possono essere progettate per minimizzare (se non eliminare) getter e setter, e ancora essere utile in un grande progetto. Inoltre non parlerò del perché rimuovere getter o setter, o quando farlo, perché queste sono domande proprie.
Ad esempio, immagina che il tuo progetto sia un gioco da tavolo come Chess o Battleship. Potresti avere vari modi di rappresentarlo in un livello di presentazione (app console, servizio web, GUI, ecc.), Ma hai anche un dominio principale. Una classe che potresti avere è quella di Coordinate
rappresentare una posizione sul tabellone. Il modo "cattivo" di scriverlo sarebbe:
public class Coordinate
{
public int X {get; set;}
public int Y {get; set;}
}
(Scriverò esempi di codice in C # piuttosto che Java, per brevità e perché ne ho più familiarità. Speriamo che non sia un problema. I concetti sono gli stessi e la traduzione dovrebbe essere semplice.)
Rimozione dei setter: immutabilità
Mentre getter e setter pubblici sono entrambi potenzialmente problematici, i setter sono il più "cattivo" dei due. Di solito sono anche più facili da eliminare. Il processo è semplice: imposta il valore all'interno del costruttore. Qualsiasi metodo che ha precedentemente modificato l'oggetto dovrebbe invece restituire un nuovo risultato. Così:
public class Coordinate
{
public int X {get; private set;}
public int Y {get; private set;}
public Coordinate(int x, int y)
{
X = x;
Y = y;
}
}
Nota che questo non protegge da altri metodi della classe che mutano X e Y. Per essere più strettamente immutabili, potresti usare readonly
( final
in Java). Ma in entrambi i casi, sia che tu renda le tue proprietà veramente immutabili o che prevenga semplicemente la mutazione pubblica diretta attraverso i setter, fa il trucco per rimuovere i tuoi setter pubblici. Nella stragrande maggioranza delle situazioni, funziona perfettamente.
Rimuovere Getters, Parte 1: Progettare per il comportamento
Quanto sopra va bene e va bene per i setter, ma in termini di getter, ci siamo effettivamente sparati al piede prima ancora di iniziare. Il nostro processo consisteva nel pensare a quale coordinata sono - i dati che rappresenta - e creare una classe attorno a ciò. Invece, avremmo dovuto iniziare con quale comportamento abbiamo bisogno da una coordinata. Questo processo, tra l'altro, è supportato da TDD, dove estraiamo classi come questa solo quando ne abbiamo bisogno, quindi iniziamo con il comportamento desiderato e lavoriamo da lì.
Diciamo quindi che il primo posto in cui ti sei trovato ad aver bisogno di un Coordinate
era per il rilevamento delle collisioni: volevi verificare se due pezzi occupano lo stesso spazio sulla scacchiera. Ecco il modo "malvagio" (costruttori omessi per brevità):
public class Piece
{
public Coordinate Position {get; private set;}
}
public class Coordinate
{
public int X {get; private set;}
public int Y {get; private set;}
}
//...And then, inside some class
public bool DoPiecesCollide(Piece one, Piece two)
{
return one.X == two.X && one.Y == two.Y;
}
Ed ecco il buon modo:
public class Piece
{
private Coordinate _position;
public bool CollidesWith(Piece other)
{
return _position.Equals(other._position);
}
}
public class Coordinate
{
private readonly int _x;
private readonly int _y;
public bool Equals(Coordinate other)
{
return _x == other._x && _y == other._y;
}
}
( IEquatable
implementazione abbreviata per semplicità). Progettando per il comportamento anziché modellare i dati, siamo riusciti a rimuovere i nostri getter.
Nota che questo è rilevante anche per il tuo esempio. È possibile che si stia utilizzando un ORM o che visualizzi le informazioni dei clienti su un sito Web o qualcosa del genere, nel qual caso un certo tipo di Customer
DTO avrebbe probabilmente senso. Ma solo perché il tuo sistema include i clienti e questi sono rappresentati nel modello di dati non significa automaticamente che dovresti avere una Customer
classe nel tuo dominio. Forse mentre progetti per il comportamento, ne emergerà uno, ma se vuoi evitare i vincitori, non crearne uno preventivamente.
Rimozione di Getters, parte 2: comportamento esterno
Così il sopra è un buon inizio, ma prima o poi si sarà probabilmente eseguito in una situazione in cui si ha un comportamento che è associato con una classe, che in qualche modo dipende dallo stato della classe, ma che non appartiene in classe. Questo tipo di comportamento è ciò che in genere vive nel livello di servizio dell'applicazione.
Prendendo il nostro Coordinate
esempio, alla fine vorrai rappresentare il tuo gioco per l'utente, e questo potrebbe significare disegnare sullo schermo. Ad esempio, potresti avere un progetto UI che utilizza Vector2
per rappresentare un punto sullo schermo. Ma sarebbe inappropriato che la Coordinate
classe si occupasse della conversione da una coordinata a un punto sullo schermo, il che porterebbe ogni tipo di problema di presentazione nel tuo dominio principale. Purtroppo questo tipo di situazione è inerente alla progettazione di OO.
La prima opzione , che è molto comunemente scelta, è solo esporre i dannati getter e dire all'inferno con esso. Questo ha il vantaggio della semplicità. Ma dal momento che stiamo parlando di evitare getter, diciamo per amor di discussione, rifiutiamo questo e vediamo quali altre opzioni ci sono.
Una seconda opzione è quella di aggiungere un qualche tipo di .ToDTO()
metodo alla tua classe. Questo o simili potrebbero essere necessari comunque, ad esempio quando vuoi salvare il gioco devi catturare praticamente tutto il tuo stato. Ma la differenza tra fare questo per i tuoi servizi e solo accedere direttamente al getter è più o meno estetica. Ha ancora lo stesso "male".
Una terza opzione - che ho visto sostenuto da Zoran Horvat in un paio di video di Pluralsight - è quella di utilizzare una versione modificata del modello di visitatore. Questo è un uso piuttosto insolito e una variazione del modello e penso che il chilometraggio delle persone varierà enormemente sul fatto che stia aggiungendo complessità per nessun guadagno reale o se sia un buon compromesso per la situazione. L'idea è essenzialmente di utilizzare il modello visitatore standard, ma i Visit
metodi devono assumere lo stato di cui hanno bisogno come parametri, anziché la classe che stanno visitando. Esempi sono disponibili qui .
Per il nostro problema, una soluzione che utilizza questo modello sarebbe:
public class Coordinate
{
private readonly int _x;
private readonly int _y;
public T Transform<T>(IPositionTransformer<T> transformer)
{
return transformer.Transform(_x,_y);
}
}
public interface IPositionTransformer<T>
{
T Transform(int x, int y);
}
//This one lives in the presentation layer
public class CoordinateToVectorTransformer : IPositionTransformer<Vector2>
{
private readonly float _tileWidth;
private readonly float _tileHeight;
private readonly Vector2 _topLeft;
Vector2 Transform(int x, int y)
{
return _topLeft + new Vector2(_tileWidth*x + _tileHeight*y);
}
}
Come probabilmente puoi dire, _x
e _y
non sono più realmente incapsulati. Potremmo estrarli creando un IPositionTransformer<Tuple<int,int>>
che li restituisce direttamente. A seconda del gusto, potresti sentire che ciò rende inutile l'intero esercizio.
Tuttavia, con i getter pubblici, è molto facile fare le cose nel modo sbagliato, semplicemente estraendo i dati direttamente e utilizzandoli in violazione di Tell, Don't Ask . Considerando che usare questo modello è in realtà più semplice farlo nel modo giusto: quando vuoi creare un comportamento, inizierai automaticamente creando un tipo associato ad esso. Le violazioni della TDA saranno ovviamente molto maleodoranti e probabilmente richiederanno di aggirare una soluzione più semplice e migliore. In pratica, questi punti rendono molto più facile farlo nel modo giusto, OO, rispetto al modo "malvagio" che incoraggiano i getter.
Infine , anche se inizialmente non è ovvio, potrebbero in effetti esserci modi per esporre abbastanza ciò di cui hai bisogno come comportamento per evitare di dover esporre lo stato. Ad esempio, utilizzando la nostra versione precedente del Coordinate
cui unico membro pubblico è Equals()
(in pratica sarebbe necessaria IEquatable
un'implementazione completa ), è possibile scrivere la seguente classe nel livello di presentazione:
public class CoordinateToVectorTransformer
{
private Dictionary<Coordinate,Vector2> _coordinatePositions;
public CoordinateToVectorTransformer(int boardWidth, int boardHeight)
{
for(int x=0; x<boardWidth; x++)
{
for(int y=0; y<boardWidth; y++)
{
_coordinatePositions[new Coordinate(x,y)] = GetPosition(x,y);
}
}
}
private static Vector2 GetPosition(int x, int y)
{
//Some implementation goes here...
}
public Vector2 Transform(Coordinate coordinate)
{
return _coordinatePositions[coordinate];
}
}
Si scopre, forse sorprendentemente, che tutto il comportamento di cui avevamo veramente bisogno da una coordinata per raggiungere il nostro obiettivo era il controllo dell'uguaglianza! Naturalmente, questa soluzione è adattata a questo problema e fa ipotesi sull'uso / prestazioni della memoria accettabili. È solo un esempio che si adatta a questo particolare dominio problematico, piuttosto che un progetto per una soluzione generale.
E ancora, le opinioni varieranno se in pratica si tratta di una complessità inutile. In alcuni casi, tale soluzione non potrebbe esistere o potrebbe essere proibitiva o complessa in modo proibitivo, nel qual caso è possibile tornare ai tre precedenti.