Комментарии 32
Объектно-ориентированное программирование — оно про объекты, которые владеют собственными данными, а не предоставляют их для обработки другому коду.
Интересно, автор оригинала это сам придумал или ему кто-то подсказал. Инкапсуляция — это только часть ООП, а не все ООП. Хотелось бы узнать ответы автора оригинала на пару вопросов. Есть Клиент, есть Заказ и есть бизнес функция Рассчитать скидку.
Вопрос 1: куда поместить логику расчета скидки?
1. В класс клиента.
2. В класс заказа.
3. В отдельный сервис.
Вопрос 2. Кто в итоге будет «владеть» всем набором данных?
1. Класс клиента.
2. Класс заказа.
3. Сервис.
4. Никто.
В целом я бы переформулировал: если для выполнения некой логики достаточно знать состояние одной сущности, лучше поместить эту логику непосредственно в объект этой сущности.
Но предложенный в дальнейшем пример рефакторинга вполне годный.
Кроме того, не стоит быть догматичным. Любое приложение всегда будет немного процедурным, немного ООПшным, немного функциональным. Только пропорции немного меняются в зависимости от предпочтений разработчиков.
В приведённом примере логику можно сунуть туда, где ей удобно. В VO цены, сущности заказа или даже создать DiscountCalculator объект, передав ему всё необходимое. Это уже зависит от построенной модели. Вот только сервис — это уже что-то совсем процедурное. Сервис захочет залезть в сущности в поисках необходимой ему инфы, что не очень, но это уже догматизм, см. предыдущий абзац.
Вот только сервис — это уже что-то совсем процедурное
Нет, у сервиса так же может быть состояние, в отличие от процедуры. Только состояние это будет называться конфигурация сервиса. Например:
— у кого узнать план подписки клиента, вдруг, у него премиум подписка;
— где взять актуальные скидочные программы, например, за каждые 3 позиции товара А 20% скидки на одну позицию товара Б?
Один сервис явно все это не сможет объять. А уж если эту логику засунуть внутрь сущности или VO, он точно лопнет.
Т.е. когда всей полнотой информации не владеет ни одна из сущностей, участвующих в бизнес процессе, нужно вводить некий координатор — сервис. И это не будет процедурным/функциональным подходом — это будет нормальным распределением обязанностей.
nemavasi
Конкретно по вашему примеру: в жизни скидка бывает связана с заказом. Нет заказа — нет и скидки.
А доставка связана с заказом, нет заказа — нет доставки? Т.е. всю логику доставки вносим в заказ? А если у нас скидка определяется только планом клиента? А если скидка определяется историей заказов? А если скидка определяется способом доставки? Все равно все в заказ, так как нет заказа — нет скидки?
Еще раз: в данной ситуации полнотой информации, не «большей частью информации», а всей полнотой информации не владеет никто. Значит за выполнение логики должен отвечать объект более высокого уровня, который будет в состоянии получить требуемую информацию от каждой сущности вовлеченной в процесс.
Конечно приемлим. Здравый смысл? Не, не слышал.
Скидка на что? — на заказ.
Скидка для кого? — для клиента.
При этом никто не мешает методу расчета в составе заказа обращаться к другим объектам — например к Журналу заказов, к объекту Доставка и т.п.
Где мы разместим метод? Кто будет обращаться к данным пользователя, к настройкам доставки, к истории заказа? Класс Заказ? Курсы из справочника курсов валют тоже класс Заказ должен получать? Погоду на месяц? Все это должны делать внешние сервисы. Класс Заказ вообще не должен знать ничего об истории заказов, доставке (если это не отдельная позиция в строке заказа), курсах и т.п. Он может иметь ссылку на сущность Клиент, для удобства доступа к данным, но ему уж точно не надо знать о тарифных планах клиентов.
В реальной жизни скидку вам будет считать человек, часто именуемый менеджер по продажам, у которого будет доступ и к данным заказа, и к данным клиента, включая историю его заказов.
Причём собственно расчёт он может делегировать человеку, у которого основная обязанность считать. Сюрприз — таких людей называли калькуляторами.
А вот если методы существуют отдельно в разных утилитных классах или сервисах — тогда это уже не объекты в полном смысле, а структуры данных. И получаем анемичную модель, со всеми вытекающими. Уж тогда лучше переходить на функциональное программирование
А откуда метод расчёта скидки возьмёт историю заказов конкретного клиента или ещё какие его данные?
Экземпляр Заказ. К нему прикреплен экземпляр Цена, к которому прикреплен экземпляр Скидка, внутри которого метод расчета (возможно ленивый) непосредственного значения скидки. Где именно отвалилась S?
Не совсем понимаю, какого рода связь вы подразумеваете под словом «прикреплен», но ваша вышеизложенная идея «Скидка на что? — на заказ.» кажется мне, как минимум, странной и как максимум, попахивающей GodObject.
Собственно, заказ в такой терминологии будет:
1. Хранить/предоставлять доступ к списку позиций в заказе.
2. Осуществлять калькуляцию стоимости/цены заказа.
3. Высчитывать скидку.
4. Иметь еще какой-то функционал…
Адепту подхода «композиция круче наследования» во мне больно, особенно от осознания того, что S в SOLID — это Single-responsibility principle.
Собственно, размер скидки может зависеть как от суммарной суммы заказа, так и от наличия какого-то количества определенных позиций, от текущей даты/времени, положения звезд, истории клиента, наличие поштучных скидок на какие-либо позиции и т.д. и т.п.
Оно, в принципе, понятно, что метод/сервис рассчета скидок должен обладать информацией о составе заказа, это я даже не отрицаю. Но сама идея затянуть вообще все, что может понадобиться для рассчета скидки в объект заказа наряду с самой механикой рассчета меня откровенно пугает.
Заказ должен приходить на вход рассчета скидки, имхо.
И получаем анемичную модель, со всеми вытекающими.
Ну, собственно, не совсем анемичную. Идея валидации состава заказа на объектом «заказ» отторжения не вызывает. Отторжение вызывает собственно 2 вещи:
1. Прикрутить «динамично изменяемую модель расчета скидок» к собственно заказу (с последующей болью от рефакторинга).
2. Попытка «натянуть сову на глобус» в поисках «сущности реального мира, которой соответствует конкретно этот объект».
Дело в том, что логика рассчета скидок как таковая вполне себе достаточно крупная вещь для того, чтобы она была отдельным объектом. Если уж сильно-сильно хочется найти соотетствующую сущность, представьте себе 150-страничный документ «Регламент рассчета скидки по организации ООО Рога-и-копыта от 17.02.1986г».
Уж тогда лучше переходить на функциональное программирование
Именно для этого кейса (рассчет скидки и прочие преобразования изначального заказа), имхо, достаточно интересная идея.
Все должно быть в сервисах. Наличие метода экземпляра обладает сразу несколькими недостатками: 1. npe, если объекта нет. 2. зависимость от состояния объекта. 3. Если объект ещё и наследуется, то получается просто грусть печаль, в виде попробуй отследи экземпляр какого класса сейчас выполняет логику.
Это всё же купон, предоставляющий право на скидку при оплате заказа. Ну, обычно они такие.
Карты скидок обычно немного другие. Часто это просто идентификатор клиента технически и при очередном заказе считается сначала скидка в деньгах для заказа, а потом новый процент скидки для следующих заказов. Некоторые продавцы даже советуют "давайте мы вот это на один чек пробъём, у вас следующая ступень скидки будет, и остальное уже с новой скидкой".
А вообще, имхо, расчёт скидки должен жить в отдельном сервисе.
А «настоящие» (активные) классы должны отвечать только за бизнес-процессы (оформление заказа и т.д.). Именно они и должны заниматься распределением потоков управления.
По-хорошему должно быть два типа классов: I/O и управляющие.
Зачем тогда классы?
Вся информация — это факты реальной жизни. Они могут храниться в БД, просто чтобы не вводить каждый раз. А могут храниться не в БД, или в БД, где понятия строк нет или оно очень сильно отличается от этого понятия в SQL СУБД.
Зачем тогда классы?Чтобы проводить бизнес-логику. И чтобы осуществлять чтение/запись куда-бы то ни было. И это разные по своему назначению классы.
Текущее решение не очень оптимальное с точки зрения производительности, но логика его спрятана в методе IntervalCollection::diff и хорошо покрыта тестами. Если другой разработчик захочет оптимизировать его, он сможет это сделать без всякого страха. Любую ошибку в логике тесты поймают немедленно. Это второе преимущество unit-тестов.
В реальности чаще оказывается, что для оптимизации нужно менять сигнатуру метода diff и выбрасывать все юнит тесты.
Вы уверены, что пишете объектно-ориентированный код?