Wie in ganz wenige Antworten und Kommentaren angegeben, DTOs ist angemessen und nützlich in einigen Situationen, vor allem in Daten über Grenzen hinweg zu übertragen (zB auf JSON Serialisierung über einen Webdienst zu senden). Für den Rest dieser Antwort werde ich das mehr oder weniger ignorieren und über Domänenklassen sprechen und wie sie entworfen werden können, um Getter und Setter zu minimieren (wenn nicht zu eliminieren) und dennoch in einem großen Projekt nützlich zu sein. Ich werde auch nicht darüber sprechen, warum Getter oder Setter entfernt werden oder wann dies zu tun ist, da dies eigene Fragen sind.
Stellen Sie sich beispielsweise vor, Ihr Projekt ist ein Brettspiel wie Schach oder Schlachtschiff. Es gibt verschiedene Möglichkeiten, dies in einer Präsentationsebene darzustellen (Konsolenanwendung, Webdienst, GUI usw.), Sie haben jedoch auch eine Kerndomäne. Eine Klasse, die Sie haben könnten, ist die Coordinate
Vertretung einer Position auf dem Brett. Die "böse" Art zu schreiben wäre:
public class Coordinate
{
public int X {get; set;}
public int Y {get; set;}
}
(Ich werde der Kürze halber Codebeispiele in C # schreiben und nicht in Java, weil ich damit besser vertraut bin. Hoffentlich ist das kein Problem. Die Konzepte sind dieselben und die Übersetzung sollte einfach sein.)
Setter entfernen: Unveränderlichkeit
Während öffentliche Getter und Setter potenziell problematisch sind, sind Setter die weitaus "schlimmeren" der beiden. Sie sind in der Regel auch leichter zu beseitigen. Der Vorgang ist einfach: Setzen Sie den Wert im Konstruktor. Alle Methoden, die das Objekt zuvor mutiert haben, sollten stattdessen ein neues Ergebnis zurückgeben. Damit:
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;
}
}
Beachten Sie, dass dies nicht gegen andere Methoden in der Klasse schützt, die X und Y mutieren. Um strenger unveränderlich zu sein, können Sie readonly
( final
in Java) verwenden. Egal, ob Sie Ihre Eigenschaften wirklich unveränderlich machen oder nur die direkte öffentliche Veränderung durch Setter verhindern - es ist der Trick, Ihre öffentlichen Setter zu entfernen. In den allermeisten Situationen funktioniert dies einwandfrei.
Getter entfernen, Teil 1: Entwerfen für Verhalten
Das Obige ist alles gut und gut für Setter, aber in Bezug auf Getters haben wir uns tatsächlich selbst in den Fuß geschossen, bevor wir überhaupt angefangen haben. Unser Prozess bestand darin, zu überlegen, was eine Koordinate ist - die Daten, die sie darstellt - und eine Klasse darum herum zu erstellen. Stattdessen hätten wir mit dem Verhalten beginnen sollen, das wir von einer Koordinate benötigen. Übrigens wird dieser Prozess durch TDD unterstützt, bei dem wir solche Klassen erst extrahieren, wenn wir sie benötigen. Wir beginnen also mit dem gewünschten Verhalten und arbeiten von dort aus.
Nehmen wir also an, der erste Ort, an dem Sie einen benötigen, Coordinate
war die Kollisionserkennung: Sie wollten überprüfen, ob zwei Teile den gleichen Platz auf dem Brett einnehmen. Hier ist der "böse" Weg (Konstruktoren der Kürze halber weggelassen):
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;
}
Und hier ist der gute Weg:
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
Implementierung der Einfachheit halber abgekürzt). Wir haben es geschafft, unsere Getter zu entfernen, indem wir Verhaltensdaten anstatt Daten zu modellieren.
Beachten Sie, dass dies auch für Ihr Beispiel relevant ist. Möglicherweise verwenden Sie ein ORM oder zeigen Kundeninformationen auf einer Website oder Ähnlichem an. In diesem Fall ist eine Art Customer
DTO wahrscheinlich sinnvoll. Nur weil Ihr System Kunden enthält und diese im Datenmodell dargestellt werden, bedeutet dies nicht automatisch, dass Sie eine Customer
Klasse in Ihrer Domäne haben sollten. Vielleicht taucht beim Entwerfen eines Verhaltens eines auf, aber wenn Sie Getter vermeiden möchten, sollten Sie keines präventiv erstellen.
Getter entfernen, Teil 2: Äußeres Verhalten
So ist die oben ein guter Anfang, aber früher oder später werden Sie wahrscheinlich in einer Situation führen , wo Sie Verhalten haben , die mit einer Klasse zugeordnet ist, die in irgendeiner Weise auf die staatliche Klasse abhängt, die aber nicht gehört zu der Klasse. Diese Art von Verhalten ist typisch für die Service-Schicht Ihrer Anwendung.
Wenn Sie sich unser Coordinate
Beispiel ansehen, möchten Sie Ihr Spiel eventuell dem Benutzer vorstellen. Dies kann bedeuten, dass Sie auf dem Bildschirm zeichnen. Sie könnten beispielsweise ein UI-Projekt haben, mit Vector2
dem ein Punkt auf dem Bildschirm dargestellt wird. Es wäre jedoch unangemessen, wenn die Coordinate
Klasse die Konvertierung von einer Koordinate zu einem Punkt auf dem Bildschirm übernehmen würde - das würde alle möglichen Probleme mit der Präsentation in Ihre Kerndomäne bringen. Leider ist diese Art von Situation in OO-Design inhärent.
Die erste Option , die sehr häufig gewählt wird, besteht darin, die verdammten Getter bloßzustellen und damit zur Hölle zu sagen. Dies hat den Vorteil der Einfachheit. Aber da wir über das Vermeiden von Gettern sprechen, lasst uns aus Gründen der Argumentation sagen, wir lehnen dieses ab und sehen, welche anderen Optionen es gibt.
Eine zweite Möglichkeit besteht darin, .ToDTO()
Ihrer Klasse eine Methode hinzuzufügen . Dies - oder ähnliches - kann ohnehin erforderlich sein, wenn Sie beispielsweise das Spiel speichern möchten, das Sie benötigen, um praktisch Ihren gesamten Status zu erfassen. Der Unterschied zwischen der Ausführung für Ihre Dienste und dem direkten Zugriff auf den Getter ist jedoch mehr oder weniger ästhetisch. Es hat immer noch genauso viel "Böses" an sich.
Eine dritte Option - die ich in einigen Pluralsight-Videos von Zoran Horvat befürwortet habe - ist die Verwendung einer modifizierten Version des Besuchermusters. Dies ist eine ziemlich ungewöhnliche Verwendung und Variation des Musters, und ich denke, die Laufleistung der Leute wird massiv davon abhängen, ob es die Komplexität erhöht, ohne einen wirklichen Gewinn zu erzielen, oder ob es ein netter Kompromiss für die Situation ist. Die Idee ist im Wesentlichen, das Standard-Besuchermuster zu verwenden, aber die Visit
Methoden verwenden den Status, den sie als Parameter benötigen, anstelle der Klasse, die sie besuchen. Beispiele finden Sie hier .
Für unser Problem wäre eine Lösung unter Verwendung dieses Musters:
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);
}
}
Wie Sie wahrscheinlich sehen können _x
und _y
nicht mehr wirklich gekapselt sind. Wir könnten sie extrahieren, indem wir eine erstellen, IPositionTransformer<Tuple<int,int>>
die sie direkt zurückgibt. Je nach Geschmack haben Sie möglicherweise das Gefühl, dass dies die gesamte Übung sinnlos macht.
Mit öffentlichen Gettern ist es jedoch sehr einfach, Dinge falsch zu machen, indem Daten direkt ausgelesen und unter Verstoß gegen Tell, Don't Ask verwendet werden . Während es mit diesem Muster einfacher ist , es richtig zu machen: Wenn Sie Verhalten erstellen möchten, erstellen Sie automatisch einen damit verknüpften Typ. Verstöße gegen TDA werden sehr offensichtlich riechen und erfordern wahrscheinlich eine einfachere und bessere Lösung. In der Praxis machen es diese Punkte viel einfacher, es richtig zu machen, OO, als die "böse" Art und Weise, die Getter ermutigen.
Schließlich , auch wenn es zunächst nicht offensichtlich ist, gibt es in der Tat Möglichkeiten, um aussetzen genug von dem, was Sie brauchen , wie Verhalten , um zu vermeiden Zustand zu belichten. Wenn Sie beispielsweise unsere vorherige Version verwenden, Coordinate
deren einziges öffentliches Mitglied Equals()
(in der Praxis wäre eine vollständige IEquatable
Implementierung erforderlich ), können Sie die folgende Klasse in Ihre Präsentationsebene schreiben:
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];
}
}
Es stellt sich vielleicht überraschend heraus, dass alles, was wir wirklich von einer Koordinate brauchten, um unser Ziel zu erreichen, die Gleichheitsprüfung war! Diese Lösung ist natürlich auf dieses Problem zugeschnitten und geht von einer akzeptablen Speichernutzung / -leistung aus. Es ist nur ein Beispiel, das zu dieser speziellen Problemdomäne passt, und keine Blaupause für eine allgemeine Lösung.
Auch hier wird es unterschiedliche Meinungen darüber geben, ob dies in der Praxis eine unnötige Komplexität ist. In einigen Fällen existiert eine solche Lösung möglicherweise nicht, oder sie ist unheimlich oder komplex. In diesem Fall können Sie auf die obigen drei zurückgreifen.