Я получил множество отзывов на мою недавнюю серию постов по Poka-yoke проектированию (я был бы расстроены, если было бы иначе). Множество из этих отзывов касаются различных технологий сериализации или трансляции, используемых обычно на границах приложения: сериализация, XML (де)гидратация (прим. переводчика: тоже самое, что и сериализация), UI-валидация и т.д. Заметьте, что такая трансляция происходит не только по периметру приложения, но также и на уровне сохраняемости (persistence). ORM-ы также являются трасляционными механизмами.
Общим для многих комментариев является утверждение о том, что большая часть технологий сериализации требует наличия конструктора по умолчанию. Например, класс XmlSerializer требует наличия конструктора по умолчанию и публичных, доступных для записи свойств. Большая часть объектно-реляционных преобразователей, которые я изучал, похоже, имеют те же требования. Контролы Windows Forms и WPF (UI – также граница приложения) почти обязаны иметь конструктор по умолчанию. Не нарушает ли это инкапсуляцию? И да и нет.
Объекты на границе
Инкапсуляция, определённо, была бы нарушена, если бы вы экспонировали ваши объекты (доменные) прямо на границе приложения. Рассмотрим простой XML-документ:
<name>
<firstName>Mark</firstName>
<lastName>Seemann</lastName>
</name>
В независимости от наличия формального контракта (XSD), мы могли бы договориться о том, что элементы firstName и lastName являются обязательными. Несмотря на такой контракт, я спокойной могу создать документ, который его нарушит:
<name>
<firstName>Mark</firstName>
</name>
Мы не можем продвинуть (прим. переводчика: принудить выполнить) этот контракт по той причине, что мы не можем вовлечь в это этап компиляции. Мы можем валидировать ввод (и вывод), но это совсем другой вопрос. Именно по той причине, что мы не можем принудить выполнить контракт, очень просто создать некорректно сформированный ввод. Тот же самый аргумент можно отнести и к формам ввода в UI и любым видам сериализованных байтовых последовательностей. Поэтому мы обязаны рассматривать любой ввод, как ожидаемый.
Это вовсе не новое наблюдение. В книге «Шаблоны корпоративных приложений», Мартин Фаулер описал такие объекты как объекты передачи данных (DTO – Data Transfer Object). Однако, несмотря на название, мы должны понимать, что DTO-объекты – это не совсем объекты. И в этом тоже нет ничего нового. В 2004 году, Дон Бокс сформулировал четыре положения сервисной ориентации. (Да, я знаю, что они уже не в моде и, что люди хотят отправить их на покой, но некоторые из них по-прежнему имеют огромный смысл). В особенности, третье положение является соответствующим данному посту:
Сервисы разделяют схему и контракт, не класс.
Да, и это означает, что они не являются объектами. DTO – это представление куска данных, отображённого на объектно-ориентированной язык. Это всё равно не делает их объектами в смысле инкапсуляции. Такое было бы невозможно представить. Поскольку любой ввод ожидаем, мы не можем продвинуть (поддержать) какие-либо инварианты.
Обычно, как указал на это в комментариях к одному из моих предыдущих постов Крэйг Стантз, даже если входные данные некорректны, мы хотим захватить то, что получили на вводе с целью отображения правильного сообщения об ошибке (этот аргумент также применим к границам между машинами). Это означает, что любой DTO будет иметь очень слабые инварианты (если будет иметь какие-либо вообще).
DTO-объекты не нарушают инкапсуляцию, поскольку они вообще не являются объектами.
Не давайте себя обмануть своим инструментам. .NET-фреймворк очень, очень хочет, чтобы вы считали свои DTO-объекты настоящими объектами.
Это следует из кодогенерации.
Строгая типизация, обеспечиваемая такими авто-генерируемыми классами, даёт ложное чувство безопасности. Вы можете думать, что получаете быструю обратную связь от компилятора, но есть множество возможных ситуаций, в которых вы получите ошибку времени выполнения (самые примечательные – те, которые вы получаете, забыв обновить автоматически сгенерированный код, после обновления схемы).
Ещё худшим результатом представления входных и выходных данных как объектов является то, что множество разработчиков, обманываясь, работают с ними как с реальными моделями объектов. Неизменный результат – анемичная доменная модель.
Всё больше и больше, эта цепочка аргументов приводит меня к заключению, что ментальная модель DTO, которую мы используем последние 10 лет – это тупик.
Что должно происходить на границе приложения
Предположим, что мы пишем объектно-ориентированный код, а те данные на границах – что угодно, кроме объектно-ориентированных объектов. Как нам с ними работать?
Один из вариантов – остаться с тем, что имеем. В этом случае, для заполнения дыры мы должны разработать слой трансляции, который смог бы транслировать DTO-объекты в правильно инкапсулированные доменные объекты. Это тут путь, которому я следую в примерах в моей книге. Однако, это решение, я всё больше подозреваю, не является лучшим. Оно порождает проблемы с поддержкой. (Кстати, такая проблема возникает, когда пишешь книгу: к тому времени как ты завершил, ты знаешь намного больше того, что знал, когда начинал её… Я не то, чтобы осуждаю книгу – это просто не идеально…)
Другая возможность – перестать относиться к данным, как к объектам и начать рассматривать их как структурированные данные, чем они на самом деле и являются. Было бы очень здорово, если бы наш язык программирования имел отдельную концепцию структурированных данных… Интересно, что в то время как C# не умеет ничего такого, F# имеет кучу возможностей для моделирования структур данных, не имеющих поведения. Возможно, это более честный подход к работе с данными… Надо будет поэкспериментировать с этим…
Третий вариант – посмотреть в сторону динамических типов. Дино Эспозито, в своей статье «На острие: Expando-объекты в C# 4.0», обрисовывает динамический подход к потреблению структурированных данных, который сокращает автоматически сгенерированный код и предоставляет легковесный API к структурированным данным. Это также выглядит многообещающим подходом… Это не обеспечивает обратной связи на этапе компиляции, но это в конце концов – лишь ложное чувство безопасности. Мы обязаны прибегнуть к юнит тестам для получения быстрой обратной связи, мы же уже все практикуем TDD, так ведь?
В заключение, вся моя серия об инкапсуляции относится к объектно-ориентированному программированию. Несмотря на наличие множества технологий для представления данных как «объектов», они являются ложными объектами. Даже если мы используем объектно-ориентированный на границе, этот код не имеет ничего общего с объектной ориентацией. Таким образом, правила Poka-yoke проектирования здесь не применимы.
Теперь вернитесь в начало и перечитайте пост, заменяя «DTO» на «Entity» (прим. переводчика: сущность) (или чем-то, чем ваша ORM называет представление строки реляционной таблицы) и вы должны увидеть очертания того, почему объектно-реляционные преобразователи так сомнительны.
Автор: EngineerSpock