Jak stwierdzono w kilku odpowiedziach i komentarzach, DTO są odpowiednie i przydatne w niektórych sytuacjach, szczególnie w przesyłaniu danych ponad granicami (np. Przesyłanie seriali do JSON w celu wysłania za pośrednictwem usługi internetowej). W pozostałej części tej odpowiedzi z grubsza to zignoruję i porozmawiam o klasach domen oraz o tym, jak można je zaprojektować, aby zminimalizować (jeśli nie wyeliminować) funkcje pobierające i ustawiające, i nadal będą przydatne w dużym projekcie. Nie będę też mówić o tym, dlaczego usuwaj osoby pobierające lub ustawiające, ani kiedy to robić, ponieważ są to pytania same w sobie.
Jako przykład wyobraź sobie, że twój projekt to gra planszowa, taka jak Szachy lub Pancernik. Możliwe są różne sposoby przedstawienia tego w warstwie prezentacji (aplikacja konsoli, usługa internetowa, GUI itp.), Ale masz także domenę podstawową. Jedną z klas, które możesz mieć, jest Coordinate
reprezentowanie pozycji na planszy. „Złym” sposobem napisania byłoby:
public class Coordinate
{
public int X {get; set;}
public int Y {get; set;}
}
(Zamierzam pisać przykłady kodu w języku C # zamiast w Javie, dla zwięzłości i ponieważ jestem bardziej zaznajomiony z tym. Mam nadzieję, że to nie jest problem. Koncepcje są takie same, a tłumaczenie powinno być proste.)
Usuwanie seterów: niezmienność
Podczas gdy publiczni zdobywcy i seterzy są potencjalnie problematyczni, setery są znacznie bardziej „złymi” z tych dwóch. Zazwyczaj są one również łatwiejsze do wyeliminowania. Proces ten jest prosty - ustaw wartość w konstruktorze. Wszelkie metody, które wcześniej zmutowały obiekt, powinny zamiast tego zwrócić nowy wynik. Więc:
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;
}
}
Zauważ, że to nie chroni przed innymi metodami w klasie mutującymi X i Y. Aby być bardziej niezmiennym, możesz użyć readonly
( final
w Javie). Ale tak czy inaczej - niezależnie od tego, czy uczynisz swoje nieruchomości naprawdę niezmiennymi, czy po prostu zapobiegniesz bezpośredniej mutacji publicznej przez seterów - robi to sztuczkę polegającą na usunięciu twoich publicznych seterów. W zdecydowanej większości przypadków działa to dobrze.
Usuwanie getterów, część 1: Projektowanie pod kątem zachowania
Wszystko to jest dobre i dobre dla seterów, ale jeśli chodzi o gettery, faktycznie strzeliliśmy sobie w stopę, zanim jeszcze zaczęliśmy. Nasz proces polegał na wymyśleniu współrzędnych - reprezentowanych przez nie danych - i stworzeniu wokół nich klasy. Zamiast tego powinniśmy zacząć od tego, jakiego zachowania potrzebujemy od współrzędnych. Nawiasem mówiąc, proces ten jest wspomagany przez TDD, w którym klasy takie wyodrębniamy tylko wtedy, gdy są potrzebne, więc zaczynamy od pożądanego zachowania i stamtąd.
Powiedzmy, że pierwsze miejsce, w którym znalazłeś potrzebę, to Coordinate
wykrywanie kolizji: chciałeś sprawdzić, czy dwa elementy zajmują to samo miejsce na planszy. Oto „zły” sposób (konstruktorów pominięto dla zwięzłości):
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;
}
A oto dobry sposób:
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
implementacja w skrócie dla uproszczenia). Projektując zachowanie, a nie modelując dane, udało nam się usunąć nasze programy pobierające.
Uwaga: dotyczy to również twojego przykładu. Być może używasz ORM lub wyświetlasz informacje o kliencie na stronie internetowej lub czymś, w którym to przypadku pewna Customer
metoda DTO prawdopodobnie miałaby sens. Ale to, że w systemie są klienci i są reprezentowani w modelu danych, nie oznacza automatycznie, że powinieneś mieć Customer
klasę w swojej domenie. Być może podczas projektowania zachowań pojawi się jeden, ale jeśli chcesz uniknąć getterów, nie twórz go z wyprzedzeniem.
Usuwanie getterów, część 2: Zachowanie zewnętrzne
Więc powyższe to dobry początek, ale prędzej czy później będzie prawdopodobnie działać w sytuacji, gdy masz problem, który jest skojarzony z klasą, która w jakiś sposób zależy od stanu klasy, ale które nie należą na klasy. Tego rodzaju zachowanie zwykle występuje w warstwie usługowej aplikacji.
Biorąc nasz Coordinate
przykład, ostatecznie będziesz chciał przedstawić swoją grę użytkownikowi, a to może oznaczać rysowanie na ekranie. Możesz na przykład mieć projekt interfejsu użytkownika, który Vector2
reprezentuje punkt na ekranie. Ale byłoby niewłaściwe, gdyby Coordinate
klasa przejmowała konwersję ze współrzędnych na punkt na ekranie - to wprowadzałoby wszelkie obawy związane z prezentacją do twojej podstawowej domeny. Niestety tego rodzaju sytuacja jest nieodłącznie związana z projektowaniem OO.
Pierwszą opcją , która jest bardzo często wybierana, jest po prostu odsłonić tych cholernych łapaczy i powiedzieć z nią do diabła. Ma to tę zaletę prostoty. Ale ponieważ mówimy o unikaniu getterów, powiedzmy, że ze względu na argumenty odrzucamy ten i sprawdzamy, jakie są inne opcje.
Drugą opcją jest dodanie .ToDTO()
metody do swojej klasy. To lub podobne może i tak być potrzebne, na przykład, jeśli chcesz zapisać grę, musisz uchwycić prawie cały swój stan. Ale różnica między robieniem tego dla twoich usług a samym bezpośrednim dostępem do gettera jest mniej lub bardziej estetyczna. Nadal ma tyle samo „zła”.
Trzecią opcją - którą widziałem zalecaną przez Zorana Horvata w kilku filmach Pluralsight - jest użycie zmodyfikowanej wersji wzorca odwiedzającego. Jest to dość nietypowe zastosowanie i odmiana wzoru i myślę, że przebieg ludzi będzie się znacznie różnił w zależności od tego, czy zwiększy złożoność bez rzeczywistego zysku, czy też jest to miły kompromis w tej sytuacji. Chodzi przede wszystkim o użycie standardowego wzorca odwiedzającego, ale Visit
metody powinny przyjmować stan, którego potrzebują, jako parametry, zamiast klasy, którą odwiedzają. Przykłady można znaleźć tutaj .
W przypadku naszego problemu rozwiązaniem wykorzystującym ten wzorzec byłoby:
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);
}
}
Jak zapewne wiesz, _x
i _y
tak naprawdę nie są już zamknięte. Możemy je wyodrębnić, tworząc taki, IPositionTransformer<Tuple<int,int>>
który zwraca je bezpośrednio. W zależności od gustu może to powodować, że całe ćwiczenie jest bezcelowe.
Jednak w przypadku publicznych programów pobierających bardzo łatwo jest robić rzeczy w niewłaściwy sposób, po prostu wyciągając dane bezpośrednio i używając ich z naruszeniem instrukcji Tell, Don't Ask . Zważywszy, że za pomocą tego wzoru to faktycznie prościej zrobić to we właściwy sposób: gdy chcesz utworzyć zachowanie, będziesz automatycznie rozpocząć tworząc rodzaj z nim związane. Naruszenia TDA będą bardzo śmierdzące i prawdopodobnie będą wymagać obejścia prostszego, lepszego rozwiązania. W praktyce punkty te znacznie ułatwiają robienie tego we właściwy sposób, niż „zły” sposób, który zachęcają osoby pobierające.
Wreszcie , nawet jeśli początkowo nie jest to oczywiste, mogą istnieć sposoby na ujawnienie wystarczającej ilości tego, czego potrzebujesz jako zachowanie, aby uniknąć konieczności ujawnienia stanu. Na przykład, używając naszej poprzedniej wersji, Coordinate
której jedynym członkiem publicznym jest Equals()
(w praktyce wymagałoby to pełnej IEquatable
implementacji), możesz napisać następującą klasę w warstwie prezentacji:
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];
}
}
Okazuje się, być może zaskakujące, że wszystkie zachowania, których naprawdę potrzebowaliśmy od współrzędnych do osiągnięcia naszego celu, to sprawdzanie równości! Oczywiście to rozwiązanie jest dostosowane do tego problemu i przyjmuje założenia dotyczące akceptowalnego wykorzystania / wydajności pamięci. To tylko przykład, który pasuje do tej konkretnej domeny problemowej, a nie plan ogólnego rozwiązania.
I znowu, opinie będą się różnić, czy w praktyce jest to niepotrzebna złożoność. W niektórych przypadkach takie rozwiązanie może nie istnieć lub może być nadmiernie dziwne lub złożone, w którym to przypadku możesz powrócić do powyższych trzech.