Изоляция модели предметной области

в 6:30, , рубрики: DDD, Анализ и проектирование систем, иммутабельность, ооп, Программирование, проектирование, Проектирование и рефакторинг, рефакторинг, Совершенный код

Эта статья является переводом материала «Domain model isolation».

Термин «изоляция модели предметной области» уже давно используется, но его значение может быть не таким очевидным, как многие думают. В этом посте автор оригинала попытается описать, что значит правильно изолировать модель предметной области и почему это важно.

Изоляция модели предметной области

Понятие изоляции модели предметной области очень похоже на то, что предлагает функциональная архитектура: вы делаете код, который составляет ядро вашего приложения, чистым, одновременно перемещая побочные эффекты и работу с внешними зависимостями (такими как база данных) к краям бизнес-операции (либо в начале, либо в конце). Иммутабельным ядром в этом смысле является модель предметной области. Изменяемая оболочка - это уровень сервисов приложений.

Однако это еще не все, и понятие иммутабельного ядра в функциональной архитектуре не полностью соответствует модели изолированной предметной области. Другими словами, модель предметной области может быть полностью изолированной, но в то же время не полностью чистой в функциональном смысле. Возможна и обратная ситуация:

Изоляция модели предметной области - 1

Главное различие между ними заключается в том, как они относятся к иммутабельности. В то время как в функциональном программировании побочные эффекты, как правило, полностью удаляются из иммутабельного ядра, DDD в целом не запрещает их, если они ограничены границами модели предметной области.

Другими словами, операция, вызываемая в модели предметной области, может изменять состояние объектов в ней, но сама по себе не должна приводить к изменению где-то в файловой системе или в стороннем API. Вот для чего нужны сервисы приложений. Модель предметной области отвечает за принятие бизнес-решений, в то время как уровень сервисов приложений преобразует эти решения в видимые биты, такие как изменения в БД.

Что делает доменную модель изолированной?

Самый простой способ определить, является ли ваша модель предметной области полностью изолированной, - это составить диаграмму сущностей и объектов значений вместе с классами из окружающего мира. Если эти сущности и объекты значений взаимодействуют с любыми классами, отличными от своих соседних сущностей и объектов значений, ваша модель предметной области лишена изоляции:

Изоляция модели предметной области - 2

Здесь, например, связь между Person и Address не противоречит концепции изоляции модели предметной области. Однако вы также можете видеть, что сущность Person ссылается на соответствующий репозиторий, а объект значения Address использует API местоположения. Оба этих класса являются шлюзами во внешний мир, и, таким образом, взаимодействие с ними нарушает изоляцию модели предметной области.

Другой способ взглянуть на это - использовать понятие луковой архитектуры. Если мы поместим вышеперечисленные классы на соответствующие слои в луковице, мы получим следующее:

Изоляция модели предметной области - 3

Отношения, которые вы видите на этой диаграмме, направлены вверх, от внутреннего ядра лука к внешним слоям. В то же время луковая архитектура говорит нам, что все коммуникации должны идти только в обратном направлении, от верхних слоев к внутренним. Нарушение этого правила говорит нам об утечке модели предметной области.

Изолированная модель предметной области является закрытой. Все операции над моделью домена должны быть закрыты в соответствии с ее сущностями и объектами значений. Другими словами, аргументы этих операций, явные или неявные, и их возвращаемые значения должны состоять только из примитивных типов или самих классов предметной области.

Вот пример, иллюстрирующий эту идею:

public sealed class Address: ValueObject<Address>
{
    public string AddressString { get; }
    public string ZipCode { get; }
 
    public Address(string addressString)
    {
        AddressString = addressString;
        ZipCode = new LocationApi().LookupZipCode(addressString);
    }
}

Объект значения Address принимает адресную строку и использует Location API для поиска соответствующих почтовых индексов. Класс LocationApi здесь является неявным аргументом конструктора. Это не доменный класс и не примитив. Следовательно, данная реализация модели предметной области не замкнута сама по себе, не изолирована.

Обратите внимание, что понятие изоляции применимо только к сущностям и объектам значений. Они являются сердцем любой модели предметной области. Другие доменные классы, такие как фабрики, доменные службы и, очевидно, репозитории, могут и обычно ссылаются на внешний мир.

Другой пример - паттерн Active Record:

public class User : Entity
{
    public void Save()
    {
        /* Saves itself to the database */
    }
 
    public void Load(int id)
    {
        /* Loads itself from the database */
    }
}

Сущности, реализующие этот шаблон, работают с базой данных напрямую, чтобы сохранять или загружать себя в базу данных и из нее. Неявный аргумент, который они используют для этого - база данных - также нарушает замыкание.

Изоляция модели предметной области и внедрение зависимостей

Давайте посмотрим на обычную практику решения проблемы изоляции. Иногда автор оригинала видит, как люди соглашаются с тем, что следующий код имеет запах:

public sealed class Address : ValueObject<Address>
{
    public string AddressString { get; }
    public string ZipCode { get; }
 
    public Address(string addressString, LocationApi locationApi)
    {
        AddressString = addressString;
        ZipCode = locationApi.LookupZipCode(addressString);
    }
}

И тут же предлагают «исправить» этот запах введя интерфейс:

public sealed class Address : ValueObject<Address>
{
    public string AddressString { get; }
    public string ZipCode { get; }
 
    public Address(string addressString, ILocationApi locationApi)
    {
        AddressString = addressString;
        ZipCode = locationApi.LookupZipCode(addressString);
    }
}

Это, конечно, не имеет значения. Внедрение интерфейса для некоторой внешней концепции и внедрение его в объект сущности или значения - это всего лишь уловка, позволяющая избежать усилий, необходимых для достижения подлинной изоляции модели предметной области. Причина, по которой это уловка, заключается в том, что такие интерфейсы не представляют собой абстракцию, они всего лишь инструмент, позволяющий проводить модульное тестирование.

Вы, конечно, можете применить практику внедрения зависимостей в модель предметной области, но эти зависимости должны представлять собой реальную абстракцию, значимую для вашего домена, а не что-то исходящее из внешнего мира. Во многих случаях это означает, что интерфейсы, которые вы вводите в свои сущности и объекты значения, должны быть реализованы другими сущностями и объектами значениями. Кроме того, принцип повторно используемых абстракций говорит нам, что для того, чтобы эти интерфейсы считались хорошей абстракцией, они должны иметь более одной реализации. Некоторые программисты даже предполагают, что для каждого интерфейса должно быть минимум 3 класса, реализующие их.

Это, кстати, предпосылка, лежащая в основе принципа инверсии зависимостей (DIP). Хотя вы можете легко реализовать внедрение зависимостей, используя некоторые произвольно выбранные интерфейсы, эти интерфейсы не следуют автоматически принципу DIP.

Все это вместе делает хорошо спроектированную изолированную модель предметной области маловероятной, чтобы у нее были какие-либо интерфейсы, «абстрагирующие» сущности и объекты значений. Сущности и объекты-значения сами по себе очень хорошо представляют предметную область, обычно нет необходимости их дальше абстрагировать.

Загрязнение семантики

Еще один признак того, что модель предметной области закрыта сама по себе, - это то, что она не управляет классами, которые происходят из других доменов/ограниченных контекстов. Если у вас есть класс, полученный из SDK какой-либо сторонней службы, вам не следует передавать его непосредственно в классы вашего домена, даже если этот класс сам по себе является просто пакетом данных и не относится к внешнему миру.

Причина в том, что такие внешние концепции почти всегда содержат данные, которые не нужны вашему основному домену. Поэтому эти данные должны быть отфильтрованы, чтобы соответствовать принципу YAGNI.

Другая причина - инварианты. У вас нет контроля над обещаниями, которые дают внешние службы, и даже если вы один раз проверили их вручную, вы не можете быть уверены, что они выполняются всегда. Чтобы обеспечить достоверность данных, поступающих от внешних служб, вам необходимо обернуть их собственными классами, которые будут частью вашего домена и в которых будут явно указаны инварианты, которые, как вы ожидаете, сохранит внешняя служба.

То же самое верно и для классов из других ограниченных контекстов. Лучший способ изолировать вашу модель домена от них - это реализовать антикоррупционный слой, который преобразует чужеродные понятия во что-то значимое для ваших классов домена.

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

Зачем изолировать модель предметной области?

Вышеупомянутые рекомендации требуют много работы, поэтому можно задать следующий вопрос: зачем вообще беспокоиться? Зачем прикладывать столько усилий?

Ответ, как всегда, заключается в том, чтобы бороться со сложностью. Правильное разделение проблем позволяет снизить когнитивную нагрузку, необходимую для размышлений о кодовой базе. Чем сложнее становится ваш проект, тем важнее становится этот вопрос. При достаточно больших базах кода очень важно иметь возможность думать о модели предметной области в отрыве от всех других проблем. Во многих случаях это фактически единственный способ справиться с постоянно растущей сложностью вашего проекта.

Другая причина - тестируемость. Замкнутая модель предметной области делает процесс модульного тестирования тривиальным. С помощью такой модели вы можете легко применить проверку вывода и состояния, что обеспечит наилучшую окупаемость инвестиций.

Обратите внимание, что полной изоляции сущностей и объектов значений не всегда можно достичь, и могут быть ситуации, когда вы просто не сможете этого сделать. Например, в условиях жестких требований к производительности у вас может не быть другого выбора, кроме как срезать углы. Тем не менее, изоляция модели предметной области - хорошая цель, к которой следует стремиться, поскольку она дает существенные преимущества.

Резюме

  • Изолированная модель предметной области - это модель, операции над которой закрыты в рамках ее сущностей и объектов значений.

  • Понятие изоляции модели предметной области применимо только к сущностям и объектам значений. Другие классы предметной области могут общаться с внешним миром.

  • Эта концепция похожа на понятие неизменяемого ядра из функциональной архитектуры. Основное отличие состоит в том, что изоляция модели предметной области допускает побочные эффекты, которые ограничены границами этой модели.

  • Замена внешней зависимости интерфейсом не означает, что вы исправляете утечку доменной модели. Чтобы придерживаться принципа инверсии зависимостей (DIP), интерфейс должен представлять значимую для бизнеса абстракцию в том смысле, что он должен помогать классам предметной области принимать бизнес-решения.

  • Загрязнение семантики - еще один признак того, что ваша модель предметной области не изолирована должным образом.

  • Изоляция модели предметной области дает два преимущества: она снижает сложность кода и обеспечивает лучшую тестируемость.

Автор: Аркадий

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js