Навеяно статьёй о различиях DTO, POCO и Value Object на Хабрахабре: DTO vs POCO vs Value Object, а также вопросом POCO vs DTO.
Нигде нет конкретных примеров. Приведите, пожалуйста, конкретный пример с небольшим описанием (или также примером), где и как его использовать и для чего.
UPD
Отличные ответы. Всем спасибо.
Еще небольшой вопрос по использованию POCO. Когда и насколько рационально запихивать логику в объекты? Вот к примеру, у меня есть сервисный слой, который возвращает POCO, какие именно методы я туда могу вставить? Допустим, мне нужно валидировать Кастомера, ок, я сделал в POCO метод Validate, пока мне не нужно лезть для валидации в базу - все хорошо, но как только это понадобиться, идея уже не кажется такой хорошей. Или я не прав? Сейчас у меня приложение, где почти все действия выполняет бизнес слой, в моделях только простые методы типа GetFullName, и по сути я оперирую DTO-хами. Так вот, как уследить ту тонкую грань "что в POCO, что в сервисе" или вообще "всю логику в сервисы, оперировать DTO"?
Представим некоторый интернет магазин. У этого магазина есть веб-интерфейс и сервер приложений, который обрабатывает логику. Некий пользователь хочет совершить какой-нибудь заказ. Для этого ему нужно выполнить ряд действий: добавить нужные товары в корзину и подтвердить заказ.
Для того, чтобы это сделать, на сервере приложений может существовать класс 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<Item> 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'ов по всему коду, что только упростит программу и сделает изменения более простыми.
Ну и опять же - все зависит от ситуации. Серебряной пули не бывает и в различных случаях стоит использовать различные подходы, которые будут более дешевы в каждой конкретной ситуации. Еще немного про ООП и остальное можете посмотреть в моей статье тут.
Приведу свою интерпретацию сказанного в статье. Правда я не согласен, что 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, потому что класс оброс базовым типом и атрибутами из сторонней библиотеки.
Это непересекающиеся понятия. Их в принципе нельзя размещать на одной плоскости (как сделано в статье на хабре).
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 и DTO - потому что вы спокойно отдаете свои POCO-BE наружу, практически без вредных последствий. Т.е. у вас не проявляется проблема, которую призван решать паттерн DTO, и поэтому вы не можете понять, зачем же отдельные классы-DTO нужны - потому что в вашем случае они действительно не нужны.
Разница проявляется как раз при уходе от Anemic - в сущности добавляется логика (ваш Validate, например). И отданная наружу из BL сущность может внезапно полезть в базу, например, в момент рендеринга вьюшки. Именно эта проблема решается паттернами DTO/LocalDTO - вы просто урезанный класс, без логики, который можно спокойно передать наружу.
Использование BE при этом автоматически ограничивается BL, и использование термина POCO (как бизнес-логики, закодированной в обычных объектах, а не в наследниках классов из фреймворка) - тоже.
Хочется попробовать ответить, хотя не знаю, достаточно ли глубоко я разбираюсь в тематике. Если что, то поправьте.
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 большой порог вступления.
Пример Агрегата можно здесь.
Dto->Aggregate учитывалась бы последовательность изменения свойств Dto). (Edit:) Некоторая логика может быть использована в доменных сервисах, вызываемых этим ассэмблером. (раньше говорил, что доменные сервисы могут вызываться агрегатом, но как-то не логично передавать в агрегат сервисы, особенно, если выдерживать IoC)Это теория, а на практике сложнее. К тому же это только мой опыт, которого у меня не столько много. Нет такого, что только так и не как иначе. Эти моменты я для себя уяснил, а принял их от более опытного коллеги и из курса лекций pluralsight "DDD". Принимаю критику.
По поводу Value Object - недавно видел пример: https://folkprog.net/value-object-y-u-symfony-formakh/ Но там в контексте Symfony form. Насколько понял, удобство заключается в хранении вроде как одного значения, но которое состоит из нескольких простых (скалярных?) значений. Чем-то напоминает вектор. Соответсвенно, и логика сравнения такая-же, по значениям (в отличии от entity, где по идентификатору)
Как развивать веб-проекты в 2026 году: технологии, контент E-E-A-T и факторы доверия
Современные инструменты для криптотрейдинга: как технологии помогают принимать решения
Апостиль в Лос-Анджелесе без лишних нервов и бумажной волокиты
Основные этапы разработки сайта для стоматологической клиники