Страницы

Поиск по вопросам

воскресенье, 24 ноября 2019 г.

Наглядный пример различия DTO, POCO (POJO) и Value Object


Навеяно статьёй о различиях DTO, POCO и Value Object на Хабрахабре:
DTO vs POCO vs Value Object, а также 
вопросом POCO vs DTO.

Нигде нет конкретных примеров. Приведите, пожалуйста, конкретный пример с небольшим описанием (или также примером), где и как его использовать и для чего.

UPD

Отличные ответы. Всем спасибо. 

Еще небольшой вопрос по использованию POCO. Когда и насколько рационально запихиват
логику в объекты? Вот к примеру, у меня есть сервисный слой, который возвращает POCO
какие именно методы я туда могу вставить? Допустим, мне нужно  валидировать Кастомера
ок, я сделал в POCO метод Validate, пока мне не нужно лезть для валидации в базу - вс
хорошо, но как только это понадобиться, идея уже не кажется такой хорошей. Или я не прав? Сейчас у меня приложение, где почти все действия выполняет бизнес слой, в моделях только простые методы типа GetFullName, и по сути я оперирую DTO-хами. Так вот, как уследить ту тонкую  грань "что в POCO, что в сервисе" или вообще "всю логику в сервисы, оперировать DTO"?
    


Ответы

Ответ 1



Представим некоторый интернет магазин. У этого магазина есть веб-интерфейс и серве приложений, который обрабатывает логику. Некий пользователь хочет совершить какой-нибудь заказ. Для этого ему нужно выполнить ряд действий: добавить нужные товары в корзину и подтвердить заказ. Для того, чтобы это сделать, на сервере приложений может существовать класс Order: public class Order { private ItemDiscountService _itemDiscountService; private UserService _userService; public Order(ItemDiscountService itemDiscountService, UserService userService) { _itemDiscountService = itemDiscountService; _userService = userService } public int Id { get; set; } public List Items { get; set; } public decimal Subtotal { get;set; } public decimal Discount { get; set; } public void AddItem(Item item) { Items.Add(item); CalculateSubtotalAndDiscount(); } public void CalculateSubtotalAndDiscount() { decimal subtotal = 0; decimal discount = 0; foreach (var item in Items) { var currentCost = item.Cost * _itemDiscountService.GetDiscountFactor(item) * _userService.GetCurrentUserDiscountFactor(); subtotal += currentCost; discount += item.Cost - currentCost; } Subtotal = subtotal; Discount = discount; } } Этот класс содержит в себе данные и логику их изменения. Он не унаследован от какого-либ специфического класса из сторонней библиотеки или от какого-либо стороннего класса и является достаточно простым - Plain Old CLR/Java Object. Когда пользователь добавляет что-то в корзину, эта информация передаётся на серве приложений, что вызывает метод AddItem в классе Order, который пересчитывает стоимость товаров и скидку, меняя тем самым состояние заказа. Нужно отобразить пользователю это изменение, и для этого нужно передать обновлённое состояние обратно на клиент. Но мы не можем просто передать экземпляр нашего Order или его копию, так как он зависи от других классов (ItemDiscountService, UserService), которые в свою очередь могут зависеть от других классов, которым может быть нужно соединение с базой данных и т. п. Конечно, их можно продублировать на клиенте, но тогда на клиенте будет доступна вс наша логика, строка подключения к БД и т. п., чего мы показывать совершенно не хотим. Поэтому, чтобы просто передать обновленное состояние, мы можем сделать для этого специальный класс: public class OrderDto { public int Id { get; set; } public decimal Subtotal { get; set; } public decimal Discount { get; set; } public decimal Total { get; set; } } Мы сможем поместить в него те данные, которые хотим передать на клиент, создав те самым Data Transfer Object. В нем могут содержаться совершенно любые нужные нам атрибуты. В том числе и те, которых нет в классе Order, например, атрибут Total. У каждого заказа есть свой идентификатор - Id, который мы используем для того, чтоб отличать один заказ от другого. В то время как в памяти сервера приложений может существоват заказ с Id=1, содержащий в себе 3 предмета, в БД может хранится такой же заказ, с тем же идентификатором, но содержащий в себе 5 предметов. Такое может возникнуть, если мы прочитали состояние заказа из БД и поменяли его в памяти, не сохранив изменения в БД. Получается, что несмотря на то, что некоторые значения у заказа в БД и заказа в памят сервера приложений будут отличаться, это все равно будет один и тот же объект, так как их идентификаторы совпадают. В свою очередь значение стоимости - 100, номер идентификатора - 1, текущая дата имя текущего пользователя - "Петрович" будут равны аналогичным значениям только тогда, когда эти значения будут полностью совпадать, и никак иначе. Т. е. 100 может быть равно только 100, "Петрович" может быть равен только "Петрович и т. д. И неважно, где будут созданы эти объекты. Если их значения будут полностью совпадать - они будут равны. Такие объекты называются Value Object. Помимо уже существующих Value Object типа decimal или string можно создавать и свои В нашем примере мы могли бы создать тип OrderPrice и поместить туда поля Subtotal, Total и Discount. public struct OrderPrice { public decimal Subtotal; public decimal Discount; public decimal Total; } В c# есть подходящая для этого возможность создавать значимые типы которые сравниваются по значению и при присваивании целиком копируются. UPDATE Что касается обновленного вопроса (хоть это действительно отдельный большой вопрос, как заметил Discord): Когда мы разрабатываем приложение мы работаем с какой-либо предметной областью. Эт предметная область может быть выражена в виде некоторой модели и действий, которые меняю состояние этой модели. Все это может быть представлено в виде набора классов. Такие классы содержат в себе как данные (в виде полей классов) так и действия, которые этими данными манипулируют (в виде методов). В принципе нет никаких ограничений на размещение данных или методов по классам. Можно вообще все засунуть в один класс и это будет прекрасно работать. Основная проблема заключается в том, что такой код будет сложнее, а значит дорож поддерживать. Так как все будет переплетено между собой - любые изменения могут привносить кучу ошибок и т.п. Поэтому, для достижения более "дешевого" кода мы начинаем его как-то структурировать, разбивать на модули и т.п. Мы можем разложить данные в одни классы, а методы в другие и это тоже будет работат и будет даже более модульно. Но все равно может нести ряд минусов. Глядя на кучу данны может быть не очевидным то, что вообще с ними может происходить или кому они могут быт нужны. Тоже самое и с кучей методов. Поэтому, чтобы было еще удобнее можно разложить данные по классам как-то сгруппировав их понятным образом. Тоже самое и с методами. Данные заказа, пользователя, товара и т.п. могут стать отдельными классами так же как и классы с соответствующими методами. Это будет еще модульнее и понятнее. Но у любого подхода есть свои плюсы и минусы. Например, в нашем интернет магазине есть различные товары, логика расчета цены которых может быть достаточно сложной. Представим, что есть некий базовый класс Item, и множество производных классов: public class Item { public int Id {get;set;} public string Name {get;set;} public decimal BaseCost {get;set;} public decimal Cost {get;set;} } public class Boots : Item { ... } public class Shirt : Item { ... } public class Pants : Item { ... } Так как логика у нас находится в отдельных классах, представим что есть класс ItemCostService, который умеет рассчитывать стоимость товара. Тогда, из-за наличия большого числа различных условий он может выглядеть как-то так: public class ItemCostService { public decimal CalculateCost(Item item) { if(item is Boots) { item.Cost = ... } else if (item is Shirt) { item.Cost = ... } else if .... } } И таких мест в программе, где в зависимости от конкретного типа товара должно быть различное поведение может быть много. Конечно, это все будет работать. Но, как только у нас появляется новый тип товара, или поменяется логика обработки существующего типа товара нам придется изменить код в большом количестве мест везде, где присутствуют такие условия Это сложнее, чем поменять все в одном месте, дольше и чревато тем, что можно что-то забыть сделать. В данном вопросе мы говорим о языках, основной парадигмой которых является ООП. это значит, что существует готовая инфраструктура которая поддерживает основные принцип ООП. Чтобы следовать этой парадигме и получать выгоду от готовой инфраструктуры мы можем поменять наши класс, добавив логику вычисления стоимости в них, меняя ее по необходимости в производных классах: public class Item { ... public virtual void CalculateCost() { ... } } public class Boots : Item { public override void CalculateCost() { ... } } Каждый производный тип сам сможет определить логику своего поведения. Вся она буде в одном месте, рядом с данными. А какой из конкретных методов вызвать определит уж инфраструктура избавив нас от этой головной боли. В данном примере такой подход будет более удобен, т.к. у нас пропадет необходимость создавать куче if'ов по всему коду, что только упростит программу и сделает изменения более простыми. Ну и опять же - все зависит от ситуации. Серебряной пули не бывает и в различны случаях стоит использовать различные подходы, которые будут более дешевы в каждой конкретной ситуации. Еще немного про ООП и остальное можете посмотреть в моей статье тут.

Ответ 2



Приведу свою интерпретацию сказанного в статье. Правда я не согласен, что DTO и VO не пересекаются. POCO — это класс, который не прибит гвоздями к архитектуре какой-либо библиотеки Программист сам волен выбирать иерархию классов (или отсутствие оной). Например, библиотека для работы с БД не будет заставлять наследовать "пользователя" от "сущности" или "активной записи". В идеале чистоты классов не нужны даже атрибуты. Подобный подход развязывает руки программистам и позволяет строить удобную им архитектуру использовать уже имеющиеся классы для работы со сторониими библиотеками и т. п. Впрочем, не обходится и без проблем, например, использование POCO может требовать магии во время выполнения: генерации унаследованных классов в памяти и т. п. Примером POCO является любой класс, который не унаследован от специфического дл некоторой библиотеки базового класса, не загромождён конвенциями и атрибутами, но который тем не менее может этой библиотекой полноценно использоваться. DTO — это класс с данными, но без логики. Он используется для передачи данных между слоями приложения и между приложениями, для сериализации и аналогичных целей. Примером DTO является любой класс, который содержит только поля и свойства. Он не должен содержать методов для получения и изменения данных. VO — это класс, который идентифицирутся по значению. Если не прибегать к перегрузк операторов сравнения и прочих методов, то в C# класс не будет VO (для классов по умолчани равенство — это ссылка на один и тот же объект, reference equality), а структура — будет (для структур по умолчанию равенство — это равенство всех полей). При этом класс не ограничивается в наличии логики. Такие классы рекомендуется делать неизменяемыми, но иногда жизнь заставляет отступать от этого правила. Примером VO является любой класс, который реализует равенство через равенство содержащихся в нём данных. Рассмотрим пример: struct Point { public int X; public int Y; } Этот тип является POCO, так как он не унаследован от непользовательских типов. О является DTO, потому что содержит только данные и может использоваться для передачи данных между процессами. И он является VO, потому что две точки с равными координатами будут равны. class Point { public int X; public int Y; } Если заменить struct на class, то пропадёт статус VO, так как две точки с равным координатами будут неравны. Чтобы снова полноправно называться VO, нужно будет реализовать Equals, операторы и интерфейсы сравнения. class Point { public int X; public int Y; void Move (int deltaX, int deltaY) { ... } void IsWithin (Rect rect) { ... } } После добавления методов пропадёт статус DTO, так как класс уже содежит логику для изменений и вычислений. class Point : DbEntity { [Property] public int X; [Property] public int Y; void Move (int deltaX, int deltaY) { ... } void IsWithin (Rect rect) { ... } } А вот в таком виде это уже даже не POCO, потому что класс оброс базовым типом и атрибутами из сторонней библиотеки.

Ответ 3



Это непересекающиеся понятия. Их в принципе нельзя размещать на одной плоскости (как сделано в статье на хабре). POCO/POJO - это подход к написанию классов-сущностей бизнес-логики. Как сущности POCO содержат внутри себя и данные, и логику. Часть "Plain Old" всего-лишь показывае что для создания классов сущностей не используется наследование от тяжелого суперкласса из фреймворка (вроде наследования от EntityObject в старом Entity Framework). Т.е. суть подхода - "испольются не суровые мега-пупер-модные мегаклассы из фреймворка, а по старинке, тупо старые добрые обычные объекты". POCO используется только внутри BL. DTO - это паттерн, который предполагает использование отдельных классов для передач данных (без состояния, без логики, просто объектов с набором свойств). DTO не может быть сущностью бизнес-логики - и, соответственно, не может быть POCO. DTO используется на границе между слоями/сервисами. Быть при этом POCO он никак н может - он не является полноценной сущностью. К тому же для DTO никогда не было проблемы повсеместного использования "новомодных суперклассов", от которых можно было бы вернуться к Plain Old Objects. Value Objects - это способ представления логически целостных объектов, для которы нет готовых стандартных типов. Например, даты, время, деньги. Value Objects - это не самостоятельные сущности. Это "кирпичики" для построения классов-сущностей. Value Object использутеся где придется. Это вспомогательный тип, вроде DateTime И он не может быть POCO, по определению - т.к. не представляет из себя сущность, д и опять же, повального использования "новых новомодных суперклассов" для ValueObjects никогда не наблюдалось, так что становится "старыми добрыми" им просто не пришлось. По дополнению к вопросу: сейчас у вас то, что называется Anemic Domain Model. П ссылке Фаулер достаточно подробно расписывает чем это плохо: This is one of those anti-patterns that's been around for quite a long time, yet seems to be having a particular spurt at the moment. The fundamental horror of this anti-pattern is that it's so contrary to the basic idea of object-oriented design; which is to combine data and process together. При всех ужасах, расписанных Фаулером, это вполне нормальный подход для небольши несложных приложений - потому что логики в них мало, и ее можно выразить в виде transaction script - того, что вы сейчас называете сервисами. Т.е. каждая операция у вас просто расписана в виде сценария - загрузить то, то, поменять то-то, сохранить. У Anemic есть свои плюсы - т.к. в сущностях нет логики - их можно спокойно передават за пределы Service Layer. Собственно, поэтому вы и не видите разницы между POCO и DT - потому что вы спокойно отдаете свои POCO-BE наружу, практически без вредных последствий. Т.е. у вас не проявляется проблема, которую призван решать паттерн DTO, и поэтому вы не можете понять, зачем же отдельные классы-DTO нужны - потому что в вашем случае они действительно не нужны. Разница проявляется как раз при уходе от Anemic - в сущности добавляется логика (ва Validate, например). И отданная наружу из BL сущность может внезапно полезть в базу, например, в момент рендеринга вьюшки. Именно эта проблема решается паттернами DTO/LocalDTO - вы просто урезанный класс, без логики, который можно спокойно передать наружу. Использование BE при этом автоматически ограничивается BL, и использование термин POCO (как бизнес-логики, закодированной в обычных объектах, а не в наследниках классов из фреймворка) - тоже.

Ответ 4



Хочется попробовать ответить, хотя не знаю, достаточно ли глубоко я разбираюсь тематике. Если что, то поправьте. Poco. У меня ассоциации, что Poco - напрямую мэппинг к таблице БД. Хотя, судя п определению, это не обязательно так. По определению, просто простой класс с простыми полями свойствами и методами типа string, int... Может иметь логику. public class Person { public string Email { get; set; } public string Name { get; set; } public DateTime BirthDate { get; set; } public override string ToString() { return String.Format("Name: {0}, E-mail: {1}, Birth Date: {2}", Name, Email, BirthDate); } //EDIT! some other logic public void SetEmailToLowerCase() { Email = Email.ToLower(); } } ValueObject. Единица из парадигмы DDD, которой не нужен id, т.к. она немутируемая т.е. не изменяется по ходу. Судя по всему, может иметь логику, которая не мешает ей быть немутируемой. public class PersonValueObject { public string Email { get; private set; } public string Name { get; private set; } public DateTime BirthDate { get; private set; } public PersonValueObject(string email, string name, DateTime birthDate) { Email = email; Name = name; BirthDate = birthDate; } public override string ToString() { return String.Format("Name: {0}, E-mail: {1}, Birth Date: {2}", Name, Email, BirthDate); } //EDIT! public override bool Equals(object obj) { var other = obj as PersonValueObject; if (other == null) return false; else return ( Email == other.Email && Name == other.Name && BirthDate == other.BirthDate ); } } DTO. Для передачи данных между разными системами или разными слоями одной системы Не должен иметь логику. Хотя, в каких-то случаях почему бы не иметь там логику. Только не такую, чтобы менялось состояние объекта, а такую, чтобы на основе состояния вычислить какое-то значение. Но для наглядности, напишу без логики. public class PersonDto { public string Email { get; set; } public string Name { get; set; } public DateTime BirthDate { get; set; } } На вопрос, в чем разница между Poco и Dto я бы ответил, что Dto только для передачи данных, а Poco - простая модель данных. UPD Еще небольшой вопрос по использованию POCO. Когда и насколько рационально запихивать логику в обьекты? Не знаю как в Вашем случае лучше. Но знаю, что все встает на свои места, если использоват DDD. Попытаюсь сказать про понятие Агрегата. Может быть это в данном случае и не будет полезно, потому что у DDD большой порог вступления. Пример Агрегата можно здесь. Агрегат, на сколько я понимаю, это логичная связка сущьностей, объединенная в одно целое, у которой есть ID. Агрегат нельзя сломать, в нем всегда корректные данные. На агрегат хорошо ложится валидация и другая логика. Однако, слышал, что валидация относится к логике persistance, то есть должна быть реализована репозиториями. Свойства у агрегата приватные. Изменение данных проводится через методы Агрегата Сразу же производится обработка данных, изменение состояния агрегата или вызов exception. На сколько я понимаю, Агрегат при смене состояния посылает сообщения (mediator pattern). На эти сообщения подписываются другие агрегаты, чтобы правильно реагировать. Агрегат достается из репозитория. Так же посылается в репозиторий для сохранени данных. Если учесть, что один агрегат может объединять в себе данные из нескольких таблиц БД, то реализация репозиториев получается достаточно навороченной. (не пробовал, правда, делать это с NHibernate) Слой презентации оперирует только с DTO. Операция записи данных происходит примерно так: в рамках одной транзакции достаютс агрегаты из репозиториев, вызываются методы агрегатов, чтобы поменять их состояния и агрегаты сохраняются в репозиторях. превращение Dto -> aggrgate и aggregate -> Dto происходит с помощью DtoAssemble (причем чтобы при превращении Dto->Aggregate учитывалась бы последовательность изменени свойств Dto). (Edit:) Некоторая логика может быть использована в доменных сервисах, вызываемых этим ассэмблером. (раньше говорил, что доменные сервисы могут вызываться агрегатом, но как-то не логично передавать в агрегат сервисы, особенно, если выдерживать IoC) Это теория, а на практике сложнее. К тому же это только мой опыт, которого у мен не столько много. Нет такого, что только так и не как иначе. Эти моменты я для себя уяснил, а принял их от более опытного коллеги и из курса лекций pluralsight "DDD". Принимаю критику.

Ответ 5



По поводу Value Object - недавно видел пример: https://folkprog.net/value-object-y-u-symfony-formakh/ Но там в контексте Symfony form. Насколько понял, удобство заключается в хранени вроде как одного значения, но которое состоит из нескольких простых (скалярных?) значений. Чем-то напоминает вектор. Соответсвенно, и логика сравнения такая-же, по значениям (в отличии от entity, где по идентификатору)

Комментариев нет:

Отправить комментарий