![](https://habrastorage.org/webt/mc/xv/xx/mcxvxx5zbueqfamfftgldnyupx4.png)
Последний пункт особенно актуален. На одном из своих выступлений Грег Янг попросил поднять руки тех, кто практиукует DDD. А потом попросил опустить тех, кто создает классы с набором публичных геттеров и сеттеров, располагает логику в «сервисах» и «хелперах» и называет это DDD. По залу прошел смешок:)
Как же правильно структурировать бизнес-логику в DDD-стиле? Где хранить «поведение»: в сервисах, сущностях, extension-методах или везде по чуть-чуть? В статье я расскажу о том, как проектирую предметную область и какими правилами пользуюсь.
Все люди лгут
![](https://habrastorage.org/webt/6c/fh/ty/6cfhty-ssjpgtzcz2xl-eaadua4.png)
Сначала аналитика, потом проектирование и лишь затем — разработка
![](https://habrastorage.org/files/2b1/eea/d27/2b1eead27a6a45b4927002fb06797ec3.jpg)
Абстрактные разговоры — просто потеря времени. Люди убеждены, что детали не значительны и «незачем вообще обсуждать их обсуждать, ведь все уже ясно». Просьба заполнить таблицу контрольных примеров наглядно показывает, что вариантов на самом деле не 3 а 26 (это не преувеличение, а результат аналитики на одном из наших проектов).
Таблицы и диаграммы — основной инструмент коммуникации между бизнесом, аналитикой и разработкой. Параллельно составлению BPMN — диаграмм и таблиц контрольных примеров начинаем записывать термины в тезарус проекта. Словарь поможет позже для проектирования сущностей.
Выделяем контексты
![](https://habrastorage.org/webt/ph/b0/1a/phb01a61skk0baii2mr9udyxtvy.png)
В реальной жизни я такого не видел. Поэтому желательно сразу грубо «нарезать» предметную модель на несколько частей. Чем меньше они связаны, тем лучше. Обычно все-таки получается нащупать некоторый набор общих терминов. Я называю это ядром предметной области. Любой контекст может зависеть от ядра. При этом крайне желательно избегать зависимостей между контекстами. Потенциально такой подход приводит к «распуханию» ядра, однако взаимная зависимость контекстов порождает сильную связность, что хуже «толстого» ядра.
Архитектура
![](https://habrastorage.org/web/bb3/430/769/bb3430769e6c4e8fbca757a8bc071f2e.png)
Порты и адаптеры, луковая архитектура, clean architecture — все эти подходы базируются на идее использовать домен в качестве ядра приложения. Эванс вскользь затрагивает этот вопрос, когда говорит о «домене» и «инфраструктуре». Бизнес-логика не оперирует понятиями «транзакция», «база данных», «контроллер», «lazy load» и т.д. n-layer — архитектура не позволяет разнести эти понятия. Запрос будет приходить в контроллер, передаваться в «бизнес-логику», а «бизнес-логика» будет взаимодействовать с
DAL
. А DAL
это сплошные «транзакции», «таблицы», «блокировки» и т.д. Clean Architecture позволяет позволяет инвертиртировать зависимости и отделить мухи от котлет. Конечно совсем абстрагироваться от деталей реализации не получится. RDBMS, ORM, сетевое взаимодействие все-равно наложат свои ограничения. Но в случае использования Clean Architecture
это можно контролировать. В n-layer
придерживаться «единого языка» гораздо сложнее из-за того что на самом нижнем слое лежит структура хранения.Clean Architecture
хорошо работает в паре с Bounded Context.
Разные контексты могут представлять собой разные подсистемы. Простые контексты лучше реализовывать с помощью простого CRUD
. Для контекстов с асимметричной нагрузкой хорошо подойдет CQRS. Для подсистем, требующих Audit Log'а есть смысл использовать Event Sourcing. Для нагруженных на чтение и запись подсистемах с ограничениями по пропускной способности и задержкам есть смысл рассмотреть event driven — подход. На первый взгляд это может показаться неудобным. Например я работал с CRUD
-подсистемой и мне пришла задача из CQRS
-подсистемы. Придется некоторе время смотреть на все эти Command
и Query
как на новые ворота. Альтернатива — проектировать систему в едином стиле — недальновидна. Архитектура — это набор инструментов, а каждый инструмент подходит для решения конкретной задачи.Структура проекта
.NET-проекты я структурирую следущим образом:
/App
/ProjectName.Web.Public
/ProjectName.Web.Admin
/ProjectName.Web.SomeOtherStuff
/Domain
/ProjectName.Domain.Core
/ProjectName.Domain.BoundedContext1
/ProjectName.Domain.BoundedContext1.Services
/ProjectName.Domain.BoundedContext2
/ProjectName.Domain.BoundedContext2.Command
/ProjectName.Domain.BoundedContext2.Query
/ProjectName.Domain.BoundedContext3
/Data
/ProjectName.Data
/Libs
/Problem1Resolver
/Problem2Resolver
Проекты из папки
Libs
не зависят от домена. Они решают только свою локальную задачу, например формирование отчетов, парсинг csv, механизмы кеширования и т.д. Структура домена соответствует BoundedContext'ам
. Проекты из папки Domain
не зависят от Data
. В Data
находятся DbContext
, миграции, конфигурации, относящиеся к DAL. Data
зависит от сущностей Domain
для построения миграций. Проекты из папки App
используют IOC-контейнер для внедерния зависимостей. Таким образом получается добиться максимальной изоляции кода домена от инфраструктуры.Моделируем сущности
Под сущностью будем понимать объект предметной области, обладающий уникальным идентификатором. Для примера возьмем класс, описывающий российскую компанию, в контексте получения аккредитации в неком ведомстве.
[DisplayName("Юридическое лицо (компания)")]
public class Company
: LongIdBase
, IHasState<CompanyState>
{
public static class Specs
{
public static Spec<Supplier> ByInnAndKpp(string inn, string kpp)
=> new Spec<Supplier>(x => x.Inn == inn && x.Kpp == kpp);
public static Spec<Supplier> ByInn(string inn)
=> new Spec<Supplier>(x => x.Inn == inn);
}
// Для EF
protected Company ()
{
}
public Company (string inn, string kpp)
{
DangerouslyChangeInnAndKpp(inn, kpp);
}
public void DangerouslyChangeInnAndKpp(string inn, string kpp)
{
Inn = inn.NullIfEmpty() ?? throw new ArgumentNullException(nameof(inn));
Kpp = kpp.NullIfEmpty() ?? throw new ArgumentNullException(nameof(kpp));
this.ValidateProperties();
}
[Display(Name = "ИНН")]
[Required]
[DisplayFormat(ConvertEmptyStringToNull = true)]
[Inn]
public string Inn { get; protected set; }
[Display(Name = "КПП")]
[DisplayFormat(ConvertEmptyStringToNull = true)]
[Kpp]
public string Kpp { get; protected set; }
[Display(Name = "Статус организации")]
public CompanyState State { get; protected set; }
[DisplayFormat(ConvertEmptyStringToNull = true)]
public string Comment { get; protected set; }
[Display(Name = "Дата изменения статуса")]
public DateTime? StateChangeDate { get; protected set; }
public void Accept()
{
StateChangeDate = DateTime.UtcNow;
State = AccreditationState.Accredited;
}
public void Decline(string comment)
{
StateChangeDate = DateTime.UtcNow;
State = AccreditationState.Declined;
Comment = comment.NullIfEmpty()
?? throw new ArgumentNullException(nameof(comment));
}
Чтобы правильно выбрать агрегаты и отношения зачастую одной итерации недостаточно. Сначала я накидываю основную структуру классов, определяю отношения один к одному, один ко многим и многие ко многим и описываю структуру данных. Затем трассирую структуру по бизнес процессам, сверяясь с BMPN и контрольными примерами. Если какой-то кейс не укладывается в структуру, значит при проектировании допущена ошибка и структуру необходимо изменить. Результирующую структуру можно оформить в виде диаграммы и дополнительно согласовать с экспертами в предметной области.
Эксперты могут указать на ошибки и неточности проектирования. Иногда в процессе выясняется, что для некоторых сущностей нет подходящего термина. Тогда я предлагаю варианты и через некоторое время находится подходящий. Новый термин вносится в тезарус. Очень важно обсуждать и договариваться о терминологии совместно. Это исключает большой пласт проблем непонимания в будущем.
Выбор уникального идентификатора
К счастью, Эванс дает четкие рекомендации на этот счет: сначала ищем идентификатор в предметной области: ИНН, КПП, паспортные данные и т.д. Если нашли — используем его. Не нашли — полагаемся на
GUID
или генерируемые базой данных Id
. Иногда целесообразно использовать в качестве Id идентификатор, отличный от доменного, даже если последний существует. Например, если сущность должна быть версинируемой и система должна хранить все предыдущие версии или если идентификатор из предметной модели — сложный композит и не дружит с persistance.Настоящие конструкторы
Для материализации объектов ORM чаще всего используют reflection. EF сможет дотянуться до protected-конструктора, а программисты – нет. Им придется создать корректное юр. лицо, идентифицируемое по ИНН и КПП. Конструктор снабжен гардами. Создать не корректный объект просто не получится. Extension-метод
ValidateProperties
вызывает валидацию по DataAnnotation
— атрибутам, а NullIfEmpty
не дает передать пустые строки.public static class Extensions
{
public static void ValidateProperties(this object obj)
{
var context = new ValidationContext(obj);
Validator.ValidateObject(obj, context, true);
}
public static string NullIfEmpty(this string str)
=> string.IsNullOrEmpty(str) ? null : str;
}
Для валидации ИНН специально написан атрибут следующего вида:
public class InnAttribute : RegularExpressionAttribute
{
public InnAttribute() : base(@"^(\d{10}|\d{12})$")
{
ErrorMessage = "ИНН должен быть последовательностью из 10/12 цифр.";
}
public InnAttribute(CivilLawSubject civilLawSubject)
: base(civilLawSubject == CivilLawSubject.Individual
? @"^\d{12}$"
: @"^\d{10}$")
{
ErrorMessage = civilLawSubject == CivilLawSubject.Individual
? "ИНН физического лица должен быть последовательностью из 12 цифр."
: "ИНН юридического лица должен быть последовательностью из 10 цифр.";
}
}
Конструктора без параметров объявлен защищенным, чтобы его использовала только для ORM. Для материализации используется reflection, поэтому модификатор доступа — не помеха. В «настоящий» конструктор переданы оба необходимых поля: ИНН и КПП. Остальные поля юр.лица в контексте системы не обязательные и заполняются представителем компании позже.
Инкапсуляция и валидация
Свойства ИНН и КПП объявлены с protected — сеттером. EF опять сможет до них дотянуться, а программисту придется воспользоваться функцией
DangerouslyChangeInnAndKpp
. Название функции явно намекает, что смена ИНН и КПП – ситуация не штатная. В функцию передается два параметра, что означает, что если менять ИНН и КПП, то только вместе. ИНН+КПП можно было бы даже сделать композитным ключом. Но для совместимости я оставил long Id
. Наконец, при вызове этой функции сработают валидаторы и если ИНН и КПП не корректны, будет выброшен ValidationException
.Можно еще больше усилить систему типов. Однако в описанном по ссылке подходе есть существенный недостаток: отсутствие поддержки со стороны стандартной инфраструктуры ASP.NET. Поддержку можно дописать, но такой инфраструктурный код чего-то стоит и его нужно сопровождать.
Свойства для чтения, специализированные методы для изменения
По бизнес-процессу организацию можно «принять» или «отклонить», причем в случае отклонения необходимо оставить комментарий. Если бы все свойства были публичными, то узнать об этом можно было только из документации. В данном случае правила смены статусов видны из сигнатур методов. В статье я привел только фрагмент класса юр.лица. На самом деле там гораздо больше полей и понимание что с чем связано очень помогает, особенно при подключении новых членов команды. Если свойство можно бесконтрольно менять в отрыве от других без явных бизнес-операций setter можно тоже сделать публичным. Однако такое свойство должно насторожить: если нет явных операций, связанных с данными, возможно эти данные не нужны?
Альтернативный вариант — использовать паттерн «состояние» и вынести поведение в отдельные классы.
Спецификации
Некоторое время было не ясно, что лучше писать extension’ы модифицирующие
Queryable
или возиться с деревьями выражений. В конечном итоге, реализация LinqSpecs оказалась самой удобной.Extension-методы
Ad hoc полиморфизм для интерфейсов (чтобы не приходилось реализовывать методы в каждом наследнике) рано или поздно появится в C#. Пока приходится довольствоваться extension-методами.
public interface IHasId
{
object Id { get; }
}
public interface IHasId<out TKey> : IHasId
where TKey: IEquatable<TKey>
{
new TKey Id { get; }
}
public static bool IsNew<TKey>(this IHasId<TKey> obj)
where TKey : IEquatable<TKey>
{
return obj.Id == null || obj.Id.Equals(default(TKey));
}
Extension-методы подходят для использования в LINQ для большей выразительности. Однако, методы
ByInnAndKpp
и ByInn
нельзя использовать внутри других выражений. Их не сможет разобрать провайдер. Более подробно про использование extension-методов а-ля DSL
рассказал Дино Эспозито на одном из DotNext.public static class CompanyDataExtensions
{
public static CompanyData ByInnAndKpp(
this IQueryable<CompanyData> query, string inn, string kpp)
=> query
.Where(x => x.Company, Supplier.Specs.ByInnAndKpp(inn, kpp))
.FirstOrDefault();
public static CompanyData ByInn(
this IQueryable<CompanyData> query, string inn)
=> query
.Where(x => x.Company, Supplier.Specs.ByInn(inn));
}
Обратите внимание на необычный Where с двумя параметрами.
EF Core
стал поддерживать InvokeExpression
. В прикладном коде используется так:var priceInfos = DbContext
.CompanyData
.ByInn("инн")
.ToList();
Альтернативный вариант — использовать
SelectMany
.var priceInfos = DbContext
.Company
// имеется в виду другой extension-метод с подходящей сигнатурой
.ByInnAndKpp("инн", "кпп")
.SelectMany(x => x.Company)
.ToList();
Вопрос эквивалентности вариантов сSelect
иSelectMany
с точки зренияIQueryProvider
я еще до конца не изучил. Буду благодарен любой информации на эту тему в комментариях.
Связанные коллекции
public virtual ICollection<Document> Documents { get; protected set; }
Желательно использовать только в блоке
Select
для преобразования в SQL-запрос, потому что код вида company.Documents.Where(…).ToList()
не построит запрос к БД, а сначала поднимет в оперативную память все связанные сущности, а потому применит Where
к выборке в памяти. Таким образом, наличие коллекций в модели может крайней негативно отразиться на производительности приложения. При этом рефакторинг будет произвести сложно, потому что придется передавать необходимые IQueryable
из вне. Чтобы контролировать качество запросов нужно поглядывать в miniProfiler.Сервисы (Service)
В анемичной модели вообще вся логика хранится в сервисах. Я предпочитаю добавлять сервисы только по необходимости, если логика неуместна в коде агрегата или описывает взаимодействие между агрегатами. Лучший вариант, когда домен содержит точные названия для сервиса — «касса», «склад», «кол-центр». В этом случае постфикс «Service» можно опустить. Набор методов в каждом классе соответствует набору use case'ов, сгруппированных по элементам пользовательского интерфейса. Работает хорошо, если интерфейс разработан в стиле
Task Based UI
.Write-методы принимают на вход сущности или DTO. Валидация запроса производится в отдельном слое строго до выполнения метода. Если метод может завершиться неудачей, следует явно обозначить это в сигнатуре с помощью типа
Result
. Исключения остаются для исключительных ситуаций.Read-методы возвращают DTO для сериализации и отправки на клиент. Благодаря
Queryable Extensions
в AutoMapper и Mapster можно использовать маппинги для трансляции в выражения для Select
, что позволяет не тащить всю сущность из БД целиком.Менеджеры (Manager)
Использую редко, для операций в рамках одного агрегата.
AspNet.Identity
, например содержит UserManager
. В основном менеджеры нужны, когда необходимо реализовать логику над агрегатом, не относящуюся непосредственно к домену.TPT для union-type
Иногда одна сщуность может быть связана с одной из нескольких других. Для создания непротиворечивой системы хранения можно использовать TPT, а для control flow — pattern matching. Этот подход подробно описан в отдельной статье.
Queryable Extensions для проекций в DTO
Использование
DataMapper
позволяет снизить количество boilerplate-кода, а использование Queryable Extensions
— строить запросы на получение DTO без необходимости писать Select
вручную. Таким образом можно повторно использовать выражения для маппинга в оперативной памяти и построения деревьев выражений для IQueryProvider
. AutoMapper
довольно прожорлив по памяти и не быстр, поэтому со временем заменил его на Mapster.CQRS для отдельных подсистем
При работе в условиях высокой неопределенности риск ошибки проектирования также велик. Прежде чем проектировать структуру БД, принимать решения о денормализации или писать хранимые процедуры есть смысл прибегнуть к быстрому макетированию и проверить гипотезы. Когда есть уверенность: что на входе, а что на выходе можно заняться оптимизацией.
В отсутствии команд реализации
IQuery
возвращают одинаковые результаты на одинаковых входных данных. Поэтому тела таких методов можно агрессивно кешировать. Таким образом, после замены реализаций инфраструктурный код (контроллеры) останется без изменений, а модифицировать придется только тело метода IQuery
. Подход позволяет оптимизировать приложение точечно по небольшим кусочкам, а не все целиком.Подход ограничено-применим для очень-очень нагруженных ресурсов из-за накладных расходов на IOC-контейнер и memory traffic для per request lifestyle. Однако, все IQuery
можно сделать singleton'ами, если не инжектировать зависимости от БД в конструктор, а вместо этого использовать конструкцию using.
Работа с унаследованным кодом
При работе с существующей кодовой базой следует определиться с форматом работы: «поддержка» или «развитие». В первом случае не предполагается появление новой функциональности и доработка системы. Максимум — добавить несколько новых отчетов, пару форм тут и там. Во втором — есть необходимость значительной переработки предметной модели и / или архитектуры в целом. Если проект необходимо именно «поддерживать», а не «развивать», лучше следовать существующим правилам, независимо от того на сколько они удачные. Если перед вами откровенный говнокод, от предложения посупортить его лучше отказаться.
Развитие проекта — задача более сложная. Тема рефакторинга выходит за рамки данной статьи. Отмечу лишь два самых полезных паттерна: «антикоррупционный слой» и «душитель». Они очень похожи. Основная идея — выстроить «фасад» между старой и новой кодовыми базами и постепенно
Комментарии (12)
ATwn
03.02.2018 22:01-1Ваш подход к структурированию кода понятен :)
Непонятно только только, почему при использовании DDD нельзя описывать бизнес логику в сервисах
Ведь DDD — это лишь методология взаимодействия разработчика с заказчиком, а не предписания к архитектуре проекта.:)
Еще интересно было бы узнать как выглядит схема базы данных и как она развивается по ходу проекта
У вас для каждой сущности своя таблица или вы храните все данные в виде набора событий?
marshinov Автор
03.02.2018 23:02Непонятно только только, почему при использовании DDD нельзя описывать бизнес логику в сервисах
Почему же нельзя? Сервисам посвящен целый параграф. Другой вопрос, что есть смысл хранить не всю логику. Обратите внимание на методDecline
. Он четко показывает: чтобы отклонить заявку необходимо добавить непустой комментарий с пояснением, почему компания не может быть аккредитована. Представьте, у нас в домене три бизнес-сценария, где мы должны отклонять аккредитацию. Где гарантия, что все три метода будет реализовывать один разработчик? Даже если так, он может забыть правило про обязательный комментарий. В варианте с отдельным методом это невозможно.
Кроме того в реальном приложении при отклонении необходимо было хранить историю заявок, в том числе с указанием менеджера, работающего с юр.лицом. При переходе из одного статуса в другое необходимо было менять 2-3 разных поля по определенным правилам. Объектная модель с поведением здорово помогает с пониманием «что здесь происходит и почему».
Более подробно этот вопрос раскрывает Дино Эспозито. Ближе к концу доклада он приводит следующий пример. Допустим, в поддержку приходит баг. Пользователь объясняет, что он совершил некоторое действие, но ему не были начислены бонусные балы (или типа того). Если в коде программист видит «репозитории» и изменение каких-то свойств ему потребуется приложить дополнительные усилия, чтобы интерпретировать слова пользователя и сопоставить с кодом. А если он видит в коде нечто вроде:
if(user.IsVip) { user.AddBonus(100); }
Он может сразу уточнить статус пользователя. Может быть, про бонусные баллы он услышал от друга, а про VIP-статус позабыл. Может быть у нас баг и пользователю не был присвоен VIP-статус, хотя должен был. В любом случае, диалог получится куда более предметным.
Atorian
03.02.2018 23:55Спасибо за статью.
Всегда интересно посмотреть как это делают другие разработчики.
Мы решили полностью следовать советам книги Patterns, Principles, and Practices of Domain-Driven Design. Тут подробно описано, с примерами, что зачем и почему, куда что класть.
Все то что Эванс говорил, но разжевано.
У меня так же есть несколько вопросов по примерам:
Обоснуйте пожалуйста, почему метод Accept принадлежит сущности Company. Ведь аккредитация выдается Ведомством. А то получается что компания сама себя куда хочешь может аккредетировать… Ну или как минимум в этот метод должен передаваться Policy ведомства или экземпляр объекта Аккредитация, с данными кто выдал и почему.
Почему метод DangerouslyChangeInnAndKpp вообще содержит в себе эту приставку? Если предметной области это нормальное явление — это бессмысленно. Может стоило тогда ввести другую сущность?
Я не знаком с .Net вообще, но вы упоминаете «луковую» и «чистую» архитектуры, а потом в домене используете public class Specs. Разве это не кусок вашего фреймворка только что просочился в домен? Т.о. вы нарушаете направление связей.marshinov Автор
04.02.2018 10:13Спасибо за вопросы.
Обоснуйте пожалуйста, почему метод Accept принадлежит сущности Company. Ведь аккредитация выдается Ведомством. А то получается что компания сама себя куда хочешь может аккредетировать… Ну или как минимум в этот метод должен передаваться Policy ведомства или экземпляр объекта Аккредитация, с данными кто выдал и почему.
Именно в данном приложении ведомство было одно, а аккредитация проходила автоматическом режиме при закрытии квартала при соблюдении определенных условий. Сущность в проекте называется по другому и там довольно увесистый агрегат. Возможно более естественно смотрелись бы названия методовBecomeAccepted
иBecomeDeclined
, чтобы субъект и объект действия не путались.
Почему метод DangerouslyChangeInnAndKpp вообще содержит в себе эту приставку? Если предметной области это нормальное явление — это бессмысленно. Может стоило тогда ввести другую сущность?
Смена ИНН и КПП — это перерегистрация. Т.е. в реальном мире нельзя просто так взять и сменить. Однако, предполагалось, что в подаваемых сведениях могут быть ошибки и к системе были предъявлены требования дать возможность эти ошибки исправлять.
НазваниеDangerouslyChangeInnAndKpp
навеяно React'ом. Видимо, зря я удалил аннотации, в которых эта логика объяснялась.
Я не знаком с .Net вообще, но вы упоминаете «луковую» и «чистую» архитектуры, а потом в домене используете public class Specs. Разве это не кусок вашего фреймворка только что просочился в домен? Т.о. вы нарушаете направление связей.
В .NET на уровне платформы встроены «деревья выражений» (Expression Trees
). Например:
Expression<Func<Company, bool>> acceptedSpec = x => x.State = CompanyState.Accepted;
Expression<Func<Company, bool>>
— довольно многословная конструкция.Spec<T>
— более читаемая и сразу специализирована для сигнатурыT -> bool
. Плюс добавлена перегрузка операторов&&
и||
. Спецификация по определению представляет правила бизнес-логики, т.е. является частью домена.
Необязательно создавать спецификации внутри класса сущности, можно положить их рядом. Мне просто удобно использовать такой синтаксис:Company.Specs.Accepted
. Все бизнес-правила фильтрации агрегатаCompany
под рукой и intellisense поможет их найти. Можно использовать и другие правила, например отдельный статический класс. Тогда будет так:CompanySpecs.Accepted
.
mirypoko
05.02.2018 13:40Я не специалист в проектировании программ. Зачем смешивать логику хранения, бизнес логику и логику отображения в одном и том же классе? Какой в этом практический смысл? Есть ли у такого подхода преимущества в сравнении с 3-х уровневым или onion подходом, где все разделено на слои?
Anton_Zh
05.02.2018 13:56Как вы считаете, в сущности можно внедрять зависимости через конструктор если они помогают достичь большей выразительности при создании методов? Например Product::price() может обращаться внутри к стратегии (PricingStrategy), которая определит цену товара исходя из скидочных кампаний, акций производителя и т.п.?
marshinov Автор
05.02.2018 14:14На одном из проектов есть такая задача. У нас отдельно
Product.BasePrice
и естьCartCalculator
. Внутри калькулятора — еще другие зависимости, которые расчитывают цену в зависимости от лицензии, роялти, накопительной скидки и других бизнес-правил.Anton_Zh
06.02.2018 06:08А чтобы узнать в указанном проекте конечную цену товара нужно было вызвать метод Product::price() или CartCalculator::price(product) или что-то в этом роде? CartCalculator используется внутри Product или снаружи?
dmitry_dvm
Оно же может вызвать NRE из-за того, что ByInnAndKpp может вернуть null, нехорошо.
marshinov Автор
Спасибо за комментарий. Ошибся при редактировании кода.
ToList
там вообще не скомпилируется, потому чтоByInnAndKpp
выполняетFirstOrDefault
. Добавил пример методаByInn
и отредактировал статью.