Как указывалось в довольно многих ответах и комментариях, 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 или отображать информацию о клиенте на веб-сайте или что-то в этом случае, и в этом случае какой-то Customer
DTO, вероятно, будет иметь смысл. Но только то, что ваша система включает в себя клиентов и они представлены в модели данных, не означает автоматически, что у вас должен быть 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];
}
}
Оказывается, что удивительно, оказывается, что все поведение, которое нам действительно требовалось от координаты для достижения нашей цели, было проверкой на равенство! Конечно, это решение адаптировано к этой проблеме и делает предположения о приемлемом использовании / производительности памяти. Это всего лишь пример, который подходит для этой конкретной предметной области, а не план для общего решения.
И снова, мнения будут различаться относительно того, является ли на практике это ненужной сложностью. В некоторых случаях такого решения не существует, или оно может быть непонятно странным или сложным, и в этом случае вы можете вернуться к вышеуказанным трем.