Как указывалось в довольно многих ответах и комментариях, DTO являются подходящими и полезными в некоторых ситуациях, особенно при передаче данных через границы (например, сериализация в JSON для отправки через веб-сервис). В оставшейся части этого ответа я более или менее проигнорирую это и поговорю о классах домена и о том, как они могут быть спроектированы так, чтобы минимизировать (если не исключать) методы получения и установки, и все же быть полезными в большом проекте. Я также не буду говорить о том, зачем удалять геттеры или сеттеры или когда это делать, потому что это их собственные вопросы.
В качестве примера представьте, что ваш проект представляет собой настольную игру, такую как Chess или Battleship. У вас могут быть различные способы представления этого на уровне представления (консольное приложение, веб-служба, графический интерфейс и т. Д.), Но у вас также есть основной домен. Один класс, который вы можете иметь Coordinate, представляет позицию на доске. «Злой» способ написать это будет:
public class Coordinate
{
public int X {get; set;}
public int Y {get; set;}
}
(Я собираюсь писать примеры кода на C #, а не на Java, для краткости и потому, что я более знаком с этим. Надеюсь, это не проблема. Концепции одинаковы, и перевод должен быть простым.)
Удаление сеттеров: неизменность
В то время как публичные геттеры и сеттеры являются потенциально проблематичными, сеттеры являются гораздо более «злым» из двух. Их также обычно легче устранить. Процесс прост - установить значение из конструктора. Любые методы, которые ранее мутировали объект, должны вместо этого возвращать новый результат. Так:
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;
}
}
Обратите внимание, что это не защищает от других методов в классе, изменяющих X и Y. Чтобы быть более неизменными, вы можете использовать readonly( finalв Java). Но так или иначе - независимо от того, делаете ли вы свои свойства по-настоящему неизменными или просто предотвращаете прямую публичную мутацию через сеттеры - это помогает избавиться от ваших публичных сеттеров. В подавляющем большинстве ситуаций это работает просто отлично.
Извлечение добытчиков, часть 1: проектирование поведения
Все вышеперечисленное хорошо для сеттеров, но с точки зрения добытчиков, мы даже застрелились в ногу еще до старта. Наш процесс состоял в том, чтобы подумать, что такое координата - данные, которые она представляет, - и создать класс вокруг этого. Вместо этого мы должны были начать с того, какое поведение нам нужно из координаты. Кстати, этому процессу помогает TDD, где мы извлекаем такие классы только тогда, когда они нам нужны, поэтому мы начинаем с желаемого поведения и работаем оттуда.
Итак, скажем, первое место, в котором вы нуждались, Coordinateбыло обнаружение столкновений: вы хотели проверить, занимают ли две фигуры одинаковое пространство на доске. Вот «злой» путь (конструкторы для краткости опущены):
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;
}
И вот хороший способ:
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реализация сокращенно для простоты). Разрабатывая поведение, а не моделируя данные, нам удалось удалить наших получателей.
Обратите внимание, что это также относится к вашему примеру. Вы можете использовать ORM или отображать информацию о клиенте на веб-сайте или что-то в этом случае, и в этом случае какой-то CustomerDTO, вероятно, будет иметь смысл. Но только то, что ваша система включает в себя клиентов и они представлены в модели данных, не означает автоматически, что у вас должен быть Customerкласс в вашем домене. Возможно, когда вы разрабатываете поведение, оно появится, но если вы хотите избежать получателей, не создавайте их превентивно.
Удаление добытчиков, часть 2: внешнее поведение
Таким образом, выше , это хорошее начало, но рано или поздно вы, вероятно , столкнуться с ситуацией , когда у вас есть поведение , которое связано с классом, который каким - то образом зависит от состояния класса, но не принадлежит по классу. Такое поведение обычно происходит на уровне обслуживания вашего приложения.
Взяв наш Coordinateпример, в конечном итоге вы захотите представить свою игру пользователю, а это может означать рисование на экране. Например, у вас может быть проект пользовательского интерфейса, который используется Vector2для представления точки на экране. Но было бы неуместно, чтобы Coordinateкласс взял на себя ответственность за преобразование координаты в точку на экране, что привело бы к всевозможным проблемам с представлением в вашей основной области. К сожалению, этот тип ситуации присущ ОО-дизайну.
Первый вариант , который очень часто выбирают, это просто разоблачить проклятых добытчиков и сказать им, черт побери. Это имеет преимущество простоты. Но так как мы говорим о том, чтобы избегать получателей, скажем, ради аргумента, мы отвергаем этот и посмотрим, какие есть другие варианты.
Второй вариант - добавить какой-нибудь .ToDTO()метод в ваш класс. Это - или подобное - вполне может понадобиться в любом случае, например, когда вы хотите сохранить игру, вам нужно захватить почти все ваше состояние. Но разница между выполнением этого для ваших услуг и простым доступом к получателю напрямую более или менее эстетична. В нем все еще столько же «зла».
Третий вариант, который, как я видел, защищал Зоран Хорват в паре видео Pluralsight, - это использование модифицированной версии шаблона посетителя. Это довольно необычное использование и вариация схемы, и я думаю, что пробег людей сильно зависит от того, добавляет ли он сложность без реальной выгоды, или это хороший компромисс для ситуации. По сути, идея состоит в том, чтобы использовать стандартный шаблон посетителя, но чтобы Visitметоды принимали необходимое состояние в качестве параметров вместо класса, который они посещают. Примеры можно найти здесь .
Для нашей проблемы, решение с использованием этого шаблона будет:
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);
}
}
Как вы можете сказать, _xи _yне являются действительно инкапсулированные больше. Мы могли бы извлечь их, создавая, IPositionTransformer<Tuple<int,int>>который просто возвращает их напрямую. В зависимости от вкуса, вы можете чувствовать, что это делает все упражнение бессмысленным.
Тем не менее, с публичными получателями очень легко сделать что-то не так, просто извлекая данные напрямую и используя их в нарушение « Скажите, не спрашивайте» . В то время как с помощью этого шаблона на самом деле проще сделать его правильно: когда вы хотите создать поведение, вы автоматически начнете с создания типа, связанного с ним. Нарушения TDA будут очень вонючими и, вероятно, потребуют решения более простого, лучшего решения. На практике эти пункты значительно облегчают правильное выполнение, ОО, способом, чем «злой», который поощряют добывающие.
Наконец , даже если это изначально неочевидно, на самом деле могут быть способы раскрыть достаточно того, что вам нужно, как поведение, чтобы избежать необходимости выставлять состояние. Например, используя нашу предыдущую версию Coordinate, единственным открытым членом которой является Equals()(на практике это потребует полной IEquatableреализации), вы можете написать следующий класс на уровне представления:
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];
}
}
Оказывается, что удивительно, оказывается, что все поведение, которое нам действительно требовалось от координаты для достижения нашей цели, было проверкой на равенство! Конечно, это решение адаптировано к этой проблеме и делает предположения о приемлемом использовании / производительности памяти. Это всего лишь пример, который подходит для этой конкретной предметной области, а не план для общего решения.
И снова, мнения будут различаться относительно того, является ли на практике это ненужной сложностью. В некоторых случаях такого решения не существует, или оно может быть непонятно странным или сложным, и в этом случае вы можете вернуться к вышеуказанным трем.