Почти год назад я уже писал о том, для чего нужны DTO(и это не только веб-севисов касается, это вплоть до контрактов между бизнес-логикой и UI, если нужно) и какие проблемы решаются с их помощью. Тогда я говорил об этом в связи с тем, что использовать объекты бизнес-модели в качестве этих самых DTO вредно и вообще плохо.
Однако, в последнее время столкнулся с другой крайностью, которую я бы обозвал DTO Driven Development. Ситуация получается тоже странная. Я объясню.
Представьте себе достаточно крупное приложение: различные там слои, подсистемы, WCF-сервисы, UI и т.д. Теперь в общем виде представьте себе физическую структуру этого приложения: всякие там веб-сайты, WCF-сервисы, куча DLL с разными логиками, все как обычно. А среди всей этой толпы DLL есть одна с названием MyCompany.DTO.dll. В этой библиотеке сложены DTO для всего приложения. Ну, там, UserDTO, CompanyDTO, ProductDTO (суффиксы только для примера). И все “слои” и подсистемы этого приложения пользуются этими DTO для того, чтобы обмениваться данными. Делается это для того, чтобы, дескать, не увеличивать сложность приложения, не “плодить” “лишних” классов, и еще куча-кучей благих побуждений.
Вроде бы все прекрасно, а на самом-то деле просто ужас какой-то. На самом-то деле получается не передача необходимых данных от одной подсистемы (или, там, слоя) в другую, а изобретение “велосипеда” в виде “какими бы имеющимися DTO воспользоваться, чтобы передать то, что я хочу”.
Пример: сервис, возвращает информацию о том, куда пользователю должен быть отправлен товар (почтой). Клиентская часть возьмет этот адрес и распечатает на конвертике.
Если еще не понятно, то мы получаем навязанный нам “третьей” стороной контракт.
По идее мне нужно вернуть имя/фамилию пользователя и его почтовый адрес, а приходится возвращать пару UserDTO+AddressDTO, при этом и тот и другой объект будут содержать либо кучу совершенно ненужной в рамках данного контекста информации (какие-нибудь поля CompanyID, ManagerID, Age, LoginName для пользователя и какие-нибудь PhoneNumber, FaxNumber для адреса), либо же “ненужные” поля будут незаполнены, что вообще бред полнейший, так как получатель имеет на руках непонятно почему “недозаполненный” объект.
Если же нужно в этих данных изменить и отправить назад – то вообще полный кошмар: как заполнять эти объекты, где брать недостающую информацию, что я обязан заполнить, а что – нет?
По сути нам говорят “вы будете укладывать свои данные вот в эту структуру. Ах не лезет?! Нет уж, упихивайте!”
На стороне же клиента, который обращается к заданной подсистеме, вообще “непонятки”: вместо одного простого объекта, просто содержащего необходимую информацию и укладывающегося в его бизнес-модель, ему приходит два каких-то “левых”. Лично я бы не обрадовался на месте программиста, работающего с таким API.
Кроме того – потенциальный “косяк” на будущее. Если нам вдруг срочно потребуется где-то изменить имеющийся DTO, то это немедленно затронет ВСЕ подсистемы, где он был использован. И ВСЕХ клиентов этих подсистем. Даже если мы всегда точно знаем все места использования этого объекта, всех этих клиентов, то, согласитесь, править (да и хотя бы просто заново тестировать) все это дело как-то не очень-то и хочется.
Это все как-то уже и не очень сочетается с посылом “не увеличивать сложность”, не правда ли? Ведь один из главных столпов ООП – инкапсуляция. А что рекомендуется инкапсулировать? Правильно, инкапсулировать нужно изменения. Ну, чтобы можно было что-то где-то в одном месте поменять, а в других местах это и не аукнулось бы никак. Вот тогда и сложности никакой не будет.
Итак, какая ситуация была бы правильной:
- Каждая подсистема, имеющая внешний API, имеет собственный, ни от кого не зависящий набор DTO, который по смыслу подходит этому API.
- Изменения, вносимые в API подсистемы, затрагивают только этот API и, возможно, его прямых клиентов и НЕ затрагивают никаких других частей приложения. На самом деле, то, что вы передаете из API-функции своей подсистемы – дело только этой самой API-функции и тех, кто именно ее использует. Это и называется “контрактом”.
- Ваши подсистемы будут действительно независимы (к чему мы и стремимся).
Как этого добиться:
- Никогда не используйте объекты бизнес-модели в качестве контрактов данных (DTO) и наоборот. DTO – это только DTO. Они передают данные – и только, такая у них работа. Это опять сюда, плюс инкапсуляция потенциальных изменений. Плюс, опять же, в OOP укладывается: каждая сущность делает что-то одно.
- Пусть каждая подсистема имеет собственный набор DTO, относящийся только к этой подсистеме.
- Наплюйте на количество классов. Поверьте, никто вас в количестве классов не ограничивает, можно создать столько, сколько нужно. Честно. Легче поддеживать два или три маленьких специализированных класса, чем поддерживать и всюду притягивать за уши один большой и унивесальный. Даже и чисто по времени легче.
- Попробуйте оценить свои DTO именно как средство транспортировки данных между вашим подсистемой и ее непосредственным клиентом. То есть, ваш DTO не должен служить средством связи дяди Васи с дядей Петей, если вы сами не один из них :) Иначе однажды к ��ам придет Петя и скажет “хочу, чтобы Вася мне еще и сплясал, а твой убогий транспорт этого не позволяет! Меняй!” И будете менять, и менять везде, ибо переводить Васю с Петей на другой, собственный, транспорт может быть уже слишком накладно.
- DTO должен пересекать только одну
желтую линию границу: границу той подсистемы, в которой он создан. Других границ (между дядей Васей и дядей Петей) он пересекать не должен.
Все гораздо проще, чем вам бы того хотелось :)