Страницы

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

воскресенье, 30 сентября 2018 г.

Наглядный пример различия 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"?


Ответ

Представим некоторый интернет магазин. У этого магазина есть веб-интерфейс и сервер приложений, который обрабатывает логику. Некий пользователь хочет совершить какой-нибудь заказ. Для этого ему нужно выполнить ряд действий: добавить нужные товары в корзину и подтвердить заказ.
Для того, чтобы это сделать, на сервере приложений может существовать класс 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'ов по всему коду, что только упростит программу и сделает изменения более простыми.
Ну и опять же - все зависит от ситуации. Серебряной пули не бывает и в различных случаях стоит использовать различные подходы, которые будут более дешевы в каждой конкретной ситуации. Еще немного про ООП и остальное можете посмотреть в моей статье тут

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

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