Comme indiqué dans de nombreuses réponses et commentaires, les DTO sont appropriés et utiles dans certaines situations, notamment pour le transfert de données à travers des frontières (par exemple, la sérialisation en JSON pour l'envoi via un service Web). Pour le reste de cette réponse, j'ignorerai plus ou moins cela et parlerai des classes de domaine et de la manière dont elles peuvent être conçues pour minimiser (voire éliminer) les accesseurs et les installateurs, tout en restant utiles dans un projet de grande envergure. Je ne parlerai pas non plus de la raison pour laquelle il faut supprimer les accesseurs ou les setters, ni à quel moment , car ce sont des questions qui leur sont propres.
A titre d'exemple, imaginez que votre projet est un jeu de plateau comme Chess ou Battleship. Vous pouvez avoir différentes façons de représenter cela dans une couche de présentation (application console, service Web, interface graphique, etc.), mais vous disposez également d'un domaine principal. Une classe que vous pourriez avoir est Coordinate
, représentant une position sur le conseil. La "mauvaise" façon de l'écrire serait:
public class Coordinate
{
public int X {get; set;}
public int Y {get; set;}
}
(Je vais écrire des exemples de code en C # plutôt qu'en Java, par souci de concision et parce que je le connais mieux. Espérons que cela ne pose pas de problème. Les concepts sont les mêmes et la traduction doit être simple.)
Retrait des setters: immuabilité
Alors que les sites publics et les setters sont potentiellement problématiques, les setters sont le plus "méchant" des deux. Ils sont aussi généralement les plus faciles à éliminer. Le processus est simple: définissez la valeur depuis le constructeur. Toutes les méthodes qui ont déjà muté l'objet doivent plutôt renvoyer un nouveau résultat. Alors:
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;
}
}
Notez que cela ne protège pas contre les autres méthodes de la classe qui mutent X et Y. Pour être plus strictement immuable, vous pourriez utiliser readonly
( final
en Java). Mais dans les deux cas - que vous rendiez vos propriétés véritablement immuables ou que vous empêchiez simplement la mutation publique directe par le biais de setters -, vous avez la possibilité de supprimer vos setters publics. Dans la grande majorité des situations, cela fonctionne très bien.
Suppression des accesseurs, partie 1: concevoir pour le comportement
Ce qui précède est très bien pour les passeurs, mais pour ce qui est des Getters, nous nous sommes tiré dans le pied avant même de commencer. Notre processus consistait à réfléchir à ce qu'est une coordonnée - les données qu'elle représente - et à créer une classe autour de celle-ci. Au lieu de cela, nous aurions dû commencer par quel comportement nous avons besoin d'une coordonnée. Soit dit en passant, ce processus est facilité par TDD, qui extrait uniquement les classes de ce type dès que nous en avons besoin. Nous commençons par le comportement souhaité et travaillons à partir de là.
Supposons donc que le premier endroit où vous ayez eu besoin d'un Coordinate
était la détection de collision: vous vouliez vérifier si deux pièces occupaient le même espace sur le tableau. Voici le "mal" moyen (constructeurs omis pour plus de brièveté):
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;
}
Et voici le bon moyen:
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
mise en oeuvre abrégée pour plus de simplicité). En concevant pour le comportement plutôt que pour la modélisation des données, nous avons réussi à supprimer nos accesseurs.
Notez que ceci est également pertinent pour votre exemple. Vous utilisez peut-être un ORM, ou affichez des informations sur les clients sur un site Web ou quelque chose du genre, auquel cas une sorte de Customer
DTO aurait probablement un sens. Mais ce n'est pas parce que votre système inclut des clients et qu'ils sont représentés dans le modèle de données que vous devez avoir une Customer
classe dans votre domaine. Peut-être que lorsque vous concevez un comportement, un comportement apparaîtra, mais si vous voulez éviter les accesseurs, ne créez pas un comportement préventif.
Suppression de getters, partie 2: comportement externe
Ce qui précède est donc un bon début, mais tôt ou tard, vous rencontrerez probablement un comportement associé à une classe, qui dépend en quelque sorte de l'état de la classe, mais qui n'appartient pas à la classe. Ce type de comportement correspond généralement à la couche de service de votre application.
En prenant notre Coordinate
exemple, vous voudrez éventuellement représenter votre jeu auprès de l'utilisateur, ce qui peut vouloir dire que vous dessinez à l'écran. Vous pouvez, par exemple, avoir un projet d'interface utilisateur qui Vector2
représente un point à l'écran. Mais il serait inapproprié que la Coordinate
classe prenne en charge la conversion d'une coordonnée en un point à l'écran, ce qui apporterait toutes sortes de problèmes de présentation à votre domaine principal. Malheureusement, ce type de situation est inhérent à la conception OO.
La première option , qui est très souvent choisie, consiste simplement à exposer les maudits getters et à dire au diable. Cela a l'avantage de la simplicité. Mais puisque nous parlons d’éviter les accesseurs, disons, par argument, nous rejetons celui-ci et voyons quelles autres options existent.
Une deuxième option consiste à ajouter une sorte de .ToDTO()
méthode à votre classe. Quoi qu'il en soit, cela peut être nécessaire, ou similaire, par exemple, lorsque vous souhaitez enregistrer le jeu, vous devez capturer la quasi-totalité de votre état. Mais la différence entre le faire pour vos services et simplement accéder directement au getter est plus ou moins esthétique. Il a toujours autant de "mal".
Une troisième option - que j'ai vue préconisée par Zoran Horvat dans quelques vidéos de Pluralsight - consiste à utiliser une version modifiée du modèle de visiteur. Il s’agit d’une utilisation et d’une variation assez inhabituelles du modèle et je pense que la distance parcourue par les gens variera énormément selon qu’il s’agit d’ajouter de la complexité sans créer de réel avantage ou si c’est un bon compromis pour la situation. L'idée est essentiellement d'utiliser le modèle de visiteur standard, mais les Visit
méthodes prennent comme paramètres l'état dont elles ont besoin, au lieu de la classe qu'elles visitent. Des exemples peuvent être trouvés ici .
Pour notre problème, une solution utilisant ce modèle serait:
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);
}
}
Comme vous pouvez probablement le constater, _x
et _y
ne sont plus vraiment encapsulés. Nous pourrions les extraire en créant un IPositionTransformer<Tuple<int,int>>
qui les retourne directement. Selon vos goûts, vous aurez peut-être l'impression que cela rend tout l'exercice inutile.
Cependant, avec les getters publics, il est très facile de mal faire les choses, il suffit d'extraire les données directement et de les utiliser en violation de Tell, Don't Ask . Tandis que vous utilisez ce modèle, il est en fait plus simple de le faire correctement: lorsque vous souhaitez créer un comportement, vous commencez automatiquement par créer un type qui lui est associé. Les violations de la TDA seront manifestement malodorantes et nécessiteront probablement une solution plus simple et meilleure. En pratique, ces points rendent beaucoup plus facile de faire les choses correctement, OO, plutôt que la manière "perverse" que les getters encouragent.
Enfin , même si cela n’est pas évident au départ, il peut en fait exister des moyens d’exposer suffisamment de comportements dont vous avez besoin pour éviter de devoir exposer l’état. Par exemple, en utilisant notre version précédente Coordinate
dont le seul membre public est Equals()
(en pratique, une IEquatable
implémentation complète serait nécessaire ), vous pourriez écrire la classe suivante dans votre couche de présentation:
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];
}
}
Il se trouve, peut-être étonnamment, que tout le comportement dont nous avions vraiment besoin d'une coordonnée pour atteindre notre objectif était la vérification de l'égalité! Bien entendu, cette solution est adaptée à ce problème et fait des hypothèses sur l'utilisation / les performances de la mémoire. C'est juste un exemple qui correspond à ce domaine de problème particulier, plutôt qu'un plan pour une solution générale.
Et encore une fois, les opinions varieront selon qu’il s’agisse ou non d’une complexité inutile. Dans certains cas, une telle solution n'existe peut-être pas, ou elle peut s'avérer prohibitive ou complexe, auquel cas vous pouvez revenir aux trois solutions ci-dessus.