Como se indicó en bastantes respuestas y comentarios, los DTO son apropiados y útiles en algunas situaciones, especialmente en la transferencia de datos a través de límites (por ejemplo, serialización a JSON para enviar a través de un servicio web). Para el resto de esta respuesta, lo ignoraré más o menos y hablaré sobre las clases de dominio y cómo se pueden diseñar para minimizar (si no eliminar) los captadores y establecedores, y seguir siendo útiles en un proyecto grande. Tampoco hablaré sobre por qué eliminar getters o setters, o cuándo hacerlo, porque esas son preguntas propias.
Como ejemplo, imagina que tu proyecto es un juego de mesa como el ajedrez o el acorazado. Puede tener varias formas de representar esto en una capa de presentación (aplicación de consola, servicio web, GUI, etc.), pero también tiene un dominio central. Una clase que puede tener es Coordinate
, representando una posición en el tablero. La forma "malvada" de escribirlo sería:
public class Coordinate
{
public int X {get; set;}
public int Y {get; set;}
}
(Voy a escribir ejemplos de código en C # en lugar de Java, por brevedad y porque estoy más familiarizado con él. Espero que eso no sea un problema. Los conceptos son los mismos y la traducción debería ser simple).
Eliminar setters: inmutabilidad
Si bien los captadores públicos y los setters son potencialmente problemáticos, los setters son los más "malvados" de los dos. También suelen ser los más fáciles de eliminar. El proceso es simple: establece el valor desde dentro del constructor. Cualquier método que previamente haya mutado el objeto debería devolver un nuevo resultado. Entonces:
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;
}
}
Tenga en cuenta que esto no protege contra otros métodos en la clase que muta X e Y. Para ser más estrictamente inmutable, puede usar readonly
( final
en Java). Pero de cualquier manera, ya sea que haga que sus propiedades sean realmente inmutables o simplemente evite la mutación pública directa a través de los setters, es el truco para eliminar sus setters públicos. En la gran mayoría de las situaciones, esto funciona bien.
Eliminando Getters, Parte 1: Diseñando para el comportamiento
Lo anterior está muy bien para los setters, pero en términos de getters, en realidad nos disparamos en el pie incluso antes de comenzar. Nuestro proceso fue pensar qué es una coordenada, los datos que representa, y crear una clase en torno a eso. En cambio, deberíamos haber comenzado con qué comportamiento necesitamos de una coordenada. Este proceso, por cierto, es ayudado por TDD, donde solo extraemos clases como esta una vez que las necesitamos, por lo que comenzamos con el comportamiento deseado y trabajamos desde allí.
Entonces, digamos que el primer lugar donde necesitabas un Coordinate
detector de colisión: querías verificar si dos piezas ocupan el mismo espacio en el tablero. Aquí está la forma "malvada" (los constructores se omiten por brevedad):
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;
}
Y aquí está el buen camino:
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
implementación abreviada para simplificar). Al diseñar para el comportamiento en lugar de modelar datos, hemos logrado eliminar nuestros captadores.
Tenga en cuenta que esto también es relevante para su ejemplo. Puede estar utilizando un ORM, o mostrar información del cliente en un sitio web o algo así, en cuyo caso Customer
probablemente tendría sentido algún tipo de DTO. Pero el hecho de que su sistema incluya clientes y estén representados en el modelo de datos no significa automáticamente que deba tener una Customer
clase en su dominio. Tal vez, a medida que diseñe para el comportamiento, surgirá uno, pero si desea evitar captadores, no cree uno preventivamente.
Eliminando Getters, Parte 2: Comportamiento Externo
Entonces, lo anterior es un buen comienzo, pero tarde o temprano probablemente se encontrará con una situación en la que tiene un comportamiento asociado con una clase, que de alguna manera depende del estado de la clase, pero que no pertenece a la clase. Este tipo de comportamiento es lo que generalmente vive en la capa de servicio de su aplicación.
Tomando nuestro Coordinate
ejemplo, eventualmente querrás representar tu juego para el usuario, y eso podría significar dibujar en la pantalla. Puede, por ejemplo, tener un proyecto de IU que Vector2
representa un punto en la pantalla. Pero sería inapropiado que la Coordinate
clase se hiciera cargo de la conversión de una coordenada a un punto en la pantalla, lo que llevaría todo tipo de problemas de presentación a su dominio principal. Lamentablemente, este tipo de situación es inherente al diseño OO.
La primera opción , que se elige con mucha frecuencia, es simplemente exponer a los malditos captadores y decirle al diablo con ella. Esto tiene la ventaja de la simplicidad. Pero ya que estamos hablando de evitar a los captadores, digamos por el argumento de rechazar este y ver qué otras opciones hay.
Una segunda opción es agregar algún tipo de .ToDTO()
método en su clase. Esto, o similar, puede ser necesario de todos modos, por ejemplo, cuando desea guardar el juego, necesita capturar casi todo su estado. Pero la diferencia entre hacer esto por sus servicios y simplemente acceder al getter directamente es más o menos estético. Todavía tiene tanta "maldad".
Una tercera opción , que he visto defendida por Zoran Horvat en un par de videos de Pluralsight, es usar una versión modificada del patrón de visitante. Este es un uso y variación bastante inusual del patrón y creo que el kilometraje de las personas variará enormemente en cuanto a si está agregando complejidad sin ganancia real o si es un buen compromiso para la situación. La idea es esencialmente usar el patrón de visitante estándar, pero que los Visit
métodos tomen el estado que necesitan como parámetros, en lugar de la clase que visitan. Los ejemplos se pueden encontrar aquí .
Para nuestro problema, una solución usando este patrón sería:
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);
}
}
Como probablemente pueda ver, _x
ya _y
no están realmente encapsulados. Podríamos extraerlos creando uno IPositionTransformer<Tuple<int,int>>
que simplemente los devuelva directamente. Dependiendo del gusto, puede sentir que esto hace que todo el ejercicio no tenga sentido.
Sin embargo, con los captadores públicos, es muy fácil hacer las cosas de manera incorrecta, simplemente extrayendo los datos directamente y usándolos en violación de Tell, Don't Ask . Mientras que usar este patrón en realidad es más simple hacerlo de la manera correcta: cuando desee crear un comportamiento, comenzará automáticamente creando un tipo asociado con él. Las violaciones de TDA serán muy evidentemente malolientes y probablemente requieran trabajar en una solución más simple y mejor. En la práctica, estos puntos hacen que sea mucho más fácil hacerlo de la manera correcta, OO, que la forma "malvada" que estimulan los captadores.
Finalmente , incluso si inicialmente no es obvio, de hecho puede haber formas de exponer suficiente de lo que necesita como comportamiento para evitar la necesidad de exponer el estado. Por ejemplo, utilizando nuestra versión anterior de Coordinate
cuyo único miembro público es Equals()
(en la práctica necesitaría una IEquatable
implementación completa ), podría escribir la siguiente clase en su capa de presentación:
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];
}
}
Resulta, quizás sorprendentemente, que todo el comportamiento que realmente necesitábamos de una coordenada para lograr nuestro objetivo era la verificación de la igualdad. Por supuesto, esta solución se adapta a este problema y hace suposiciones sobre el uso / rendimiento aceptable de la memoria. Es solo un ejemplo que se ajusta a este dominio del problema en particular, en lugar de un plan para una solución general.
Y nuevamente, las opiniones variarán sobre si en la práctica esto es una complejidad innecesaria. En algunos casos, no existe una solución como esta, o puede ser prohibitivamente extraño o complejo, en cuyo caso puede volver a las tres anteriores.