Комплекс доменной неполноценности — это когда веришь, что доменный слой приложения должен быть самым большим и самым важным, и винишь себя в том, что в твоём коде это не так. Это происходит, если воспринимать «Чистую архитектуру» как единственно верный способ писать код.

Привет, меня зовут Саша, я Head of mobile в компании «Метр квадратный». Под катом — почему появился этот комплекс и как с ним бороться. Сразу оговорюсь, в статье много моего личного мнения, и будет круто, если в комментах вы поделитесь своим.

Как мы до такого докатились?

Перенесёмся в 2009 год. Только‑только вышел Android 2.0.1, а Android‑разработка — занятие почти неизвестное. Телефоны совсем слабые, а приложения — простые и легковесные. У скромного комьюнити разработчиков есть столь же скромная документация, в которой рекомендуется держать весь код приложения в многочисленных Activity. Если какой‑то код вы вынесете в отдельный класс, это будет считаться верхом искусства ООП.

Но с 2009 года многое изменилось: часть разработчиков начали писать по канонам «Чистой архитектуры», приложения стали гигантскими, а бизнес‑логики в них стало пропорционально меньше. Пришлось отдать это «голодающим» бэкенд‑разработчикам.

Конечно, можно привести примеры приложений с жирным доменным слоем (и хрустящей корочкой из юнит‑тестов), но многим из нас (и мне в том числе) чаще приходилось писать совсем другие приложения. Что‑то по ТЗ типа: «Возьми вот эти данные и покажи их вот в этом списочке».

Так в чём же здесь проблема, спросите вы? Я расскажу.

Умные книжки учат нас, что приложение — это бизнес‑логика. Всё остальное: UI, сеть, база данных — это всего лишь детали реализации, мелкие библиотечки, которые должны пресмыкаться перед могучим доменным слоем. На деле UseCase иногда состоят всего из пары строчек, а Entity — вообще нет.

Да-да, те data-классы, что гуляют по вашему приложению, — это ни разу не Entity, а Input/Output Data. Все вопросы к Дядюшке Бобу, это его диаграмма
Да-да, те data-классы, что гуляют по вашему приложению, — это ни разу не Entity, а Input/Output Data. Все вопросы к Дядюшке Бобу, это его диаграмма

Из‑за этого получается неприлично тонкий доменный слой — стыдно показывать даже друзьям. И так повторяется из проекта в проект. Кажется, у кого угодно вырастут комплексы от такого.

Я попробую копнуть в причины этого феномена, а затем будем «лечиться» от этих комплексов.

Книжки идеализируют реальность

Знаете, что общего у книг Роберта Мартина, Кента Бека и Мартина Фаулера? Они изобилуют примерами кода, «не замусоренного» UI‑фреймворками, получением данных с бэкенда и прочими мелкими деталями реализации.

Нам показывают код таким, каким он должен быть: пронизанным духом настоящего ООП, не зависящим ни от чего, прекрасно тестируемым, лаконично решающим задачу. Читаешь и радуешься: если я буду писать так же, то смогу иметь 100%‑е покрытие тестами, код будет независим от сторонних библиотек, а значит, доменный слой будет основой всего, а presentation‑ и data‑слои — лишь плагинами к доменному. Я даже смогу просто взять и заменить UI‑слой на командную строку!

Конечно, все эти идеи подкупают, в них хочется верить. Но есть одно «но».

В реальном мире всё не так

В реальной разработке мы зажаты в тиски: дизайнер принёс макеты, а бэкенд‑разработчик — API. Часто единственное, что нужно сделать, — написать адаптер от одного к другому. Такие приложения‑адаптеры обычно зовутся тонкими клиентами, бизнес‑логики в них мало.

Получается, что мы — всего лишь прослойка (как бы грустно это ни звучало).

Так android‑разработчики зарабатывают комплекс доменной неполноценности: с одной стороны, чистая архитектура учит нас чтить доменный слой, с другой стороны, кода у нас в нём — с гулькин нос.

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

Оба эти варианта ни к чему хорошему не приведут, я рекомендую остерегаться их. Моя рекомендация — немного расслабиться и сбавить градус ортодоксальности. Как это сделать? Надеюсь, следующая секция с этим поможет.

Ответы на неудобные вопросы

Дисклеймер

Осторожно! Я уже предупреждал, но предупрежу ещё раз. Вас ждёт много вкусовщины и личного мнения (хоть оно и основано на горьком опыте). Читайте на свой страх и риск.

Что делать с юзкейсом, содержимое которого — одна строчка? Вызов репозитория, и всё. Может, в этом случае вызывать репозиторий напрямую?

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

Если уж вы ввязались в «Чистую архитектуру» (вас же никто не заставлял?), то юзкейсы придётся писать всегда, даже однострочные. Ну а тесты уже на вашей совести ????

У меня огромная вью‑модель (презентер, контроллер, etc.) с кучей логики. Она готовит данные для отображения. Может, эту логику унести в доменный слой? Там всё равно пусто.

Хорошая попытка, но нет. В доменном слое не должно быть никаких намёков на то, как и кем будут отображаться данные. А вью‑модель можно просто разбить на части.

Ладно, а навигация? Ведь удобно же, когда юзкейс сам общается с роутером/навигатором.

Нет, нет, и ещё раз нет! Какие у вас экраны и как они сменяются — дело презентационное, и точка. Если юзкейсу суждено быть однострочным, смиритесь с этим, не надо пытаться его искусственно раздуть.

Если presentation‑ и data‑слои — мелочи и детали реализации, то какой процент останется от приложения, если их удалить, а оставить только доменный слой?

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

А что, если рассматривать Android‑приложение лишь как часть большой системы? Ведь тогда всё встаёт на свои места: доменный слой располагается где‑то на бэкенде, а наше приложение и есть презентационный слой системы.

Можно и так. Взять какую‑нибудь архитектуру из семейства MVx. То, что в «Чистой архитектуре» занимает целых два слоя, domain и data, в MVx — всего лишь буква M.

Если эта самая модель совсем маленькая, не надо размазывать её по всяким UseCase, Entity и Repository, не тратьте своё время.

Вместо заключения будем препарировать торт

Кодовая база — она как торт. Ну, согласитесь. Сложность приложения — это высота торта, и в этом измерении количество слоёв очень важно. Если торт высокий, ему нужно много слоёв, а если совсем низенький, то будет достаточно и трёх слоёв.

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

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

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

И в нашем случае торт — это не ложь!
И в нашем случае торт — это не ложь!

Комментарии (12)


  1. ChPr
    00.00.0000 00:00

    Взять какую‑нибудь архитектуру из семейства MVx. То, что в «Чистой архитектуре» занимает целых два слоя, domain и data, в MVx — всего лишь буква M.

    MVx – это архитектурные паттерны презентационного слоя. Ничто не мешает использовать «чистую архитектуру» с ними, они всего-лишь один из слоев торта.


    1. kryanod Автор
      00.00.0000 00:00

      Да, более того, «по умолчанию» Роберт Мартин предлагает на презентационом слое делать MVCP (осмелюсь этот паттерн так назвать), где контроллер работает в одну сторону, а презентер — в другую. Я же имел в виду скорее то, что MVx зачастую воспринимается как полноценное, оформленное решение для организации кодовой базы. По крайней мере, я неоднократно с таким сталкивался. И бывает даже, что такого допущения вполне себе хватает.


      1. ws233
        00.00.0000 00:00
        +1

        Все верно пишите в части того, что слои можно объединять и декомпозировать по необходимости.

        Но вот то, как это делают – обычно неверно. Глядите, в чистой архитектуре общение с сервером или базой данных (БД) – это слой View. Внезапно. Он синенький. Это интерфейс общения с внешней системой. Это значит, что общение с сервером или БД должно зависеть от того, что ниже. А ниже у вас юз кейсы. А как делают обычно? Зависимость следующая V -> x -> M. А должно быть V -> x <- M. Видите? Зависимости обратные. Именно поэтому MVP (на андроид) и MVC (на iOS) рисуют с презентером (контроллером) вверху, а вью и моделью внизу. Картинка 7.2 отсюда.

        Если это понять, то все встает на свои места. MVx – это не какие-то там "архитектурные паттерны презентационного слоя", это полноценная клин архитектура, в которой буковка М - это композиция Controller и Web (для случая работы с сервером) или Gateway и DB (при работе с локальным хранилищем). Термины из этой картинки. Да, контроллер и презентер тут вступают в конфликт терминологии с паттернами MVx. Но если различать их значения, то проблем восприятия не будет. Мне понравились картиночки вот отсюда. (Там, где написано про слои и линейность.)

        Теперь про:

        Что делать с юзкейсом, содержимое которого — одна строчка? Вызов репозитория, и всё. Может, в этом случае вызывать репозиторий напрямую?

        По идее интерфейс общения с БД и сервером (View к внешним системам) у вас должны быть зависимы от внешней системы и независимы от ваших внутренностей (инверсия зависимости – раз. два – вы делаете визуальный интерфейс под удобство пользователя, а интерфейс сетевого слоя – под запросы сервера). Это значит, что эта самая View к внешним системам имеет свой собственный внутренний формат данных, отличный от вашего внутреннего формата приложения. Поэтому, юз кейс как минимум должен содержать преобразование данных от внешнего формата к внутреннему (к доменной области). Именно так. Видите же выше направления зависимостей? Применяем к ним инверсию зависимостей и получаем то, что нарисовано в чистой архитектуре в правом нижнем углу. Поэтому юзкейс не должен быть пустым. Он как минимум содержит преобразования данных от интерфейса внешних систем (сервера и БД), к интерфейсу с пользователем, через промежуточные внутренние Entity используемые в доменной области.

        Конкретный пример. Есть такой инструмент, как Swagger - автоматический генератор сетевого слоя по контракту с сервером. Он генерирует запросы/ответы в своем собственном формате, который вообще не обязан совпадать с тем, что вы используете в доменной области или покажете пользователю на экране. Конечно, есть вероятность того, что форматы совпадут. Тогда юзкейс действительно будет просто проектированием данных. Но это скорее исключение, чем правило. Если же это у Вас правило, то скорее всего Вы неправильно определяете направление зависимостей между интерфейсами внешних систем и внутренними юзкейсами (V -> x -> M).

        Что думаете?


        1. kryanod Автор
          00.00.0000 00:00
          +1

          Это значит, что общение с сервером или БД должно зависеть от того, что ниже. А ниже у вас юз кейсы. А как делают обычно? Зависимость следующая V -> x -> M. А должно быть V -> x <- M

          Да, согласен полностью. Мне кажется, тут проблема как раз в том, что зачастую когда говорят М, имеют в виду data+domain (М — это же модель, а модель сразу с доменным слоем ассоциируется). Если же воспринимать их как V ~ presentation, M ~ data, x ~ domain, становится чуть лучше. Я наверное в статье не совсем правильно выразился, и вы правы, некорректно в принципе называть MVx чисто презентационными паттернами. Я имел в виду следующее: обычно, когда говорят «У нас MVC», имеют в виду «У нас вся логика в контроллере». Когда же говорят «У нас Clean с MVC на презентационном слое», имеют в виду, что презентер уже не так нагружен, потому что есть юзкейсы (и иногда даже энтити). Возможно, так не совсем корректно говорить, да.

          По идее интерфейс общения с БД и сервером (View к внешним системам) у вас должны быть зависимы от внешней системы и независимы от ваших внутренностей

          А тут я или в корне не согласен, или не так вас понял. Скорее второе (слово интерфейс уж слишком многогранно), но всё же попробую ответить. Обычно интерфейс для общения с БД и сервером размещается "у нас", в доменном слое, а реализуется уже в data-слое. Это позволяет воспринимать БД, бэкенд и прочее как плагины и не зависеть от них напрямую. Если и интерфейс, и реализация взаимодействия с внешними системами находятся в data-слое, то доменный слой вынужден будет зависеть от data-слоя, а это противоречит принципу, что у нас все зависимости должны идти внутрь, а доменный слой должен находиться в центре и ни от кого не зависеть. А если доменный слой ни от кого не зависит, он ничего и не может знать о том, в каком формате там у нас приходят ответы от бэкенда. И к примеру ответственность за маппинг данных из data в domain ложится на плечи того, кто будет реализовать интерфейс репозитория, а реализовать его будет уже data-слой. И только так и можно реализовать полноценную инверсию зависимостей: domain определяет интерфейсы, но ему всё равно, кто и как будет их реализовывать.


          1. ws233
            00.00.0000 00:00
            +1

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

            Но разрешите все же донести свою мысль. И причину ошибки.

            Смотрите, в чистой архитектуре средний слой – это адаптер. Его главная цель, соединить между собой 2 других слоя, которые должны быть независимы и переиспользуемы (вью переиспользуется между приложениями, а доменный слой даже между платформами). Но у этого шаблона проектирования есть небольшой недостаток, он делает зависимым то, что снаружи (сверху) от того, что внутри. Это приводит к проблемам переиспользования: доменную модель мы можем переиспользовать даже между платформами, а вот вью между приложениями уже не можем, т.к. даже имеющиеся вьюшки мы должны адаптировать под протоколы более низких слоев. В итоге, становится тяжело взять, например, текстфилд и перенести его в другое приложение, потому что для него все равно приходится писать адаптер.

            Как это должно исправляться? Есть еще один шаблон. Он следует той же цели, но с одним существенным плюсом: слабая связность и отсутствие явных связей соединяемых объектов друг на друга. В таком случае мы не должны менять вью и переиспользовать ее становится гораздо проще. Вспомните типичную мобильную разработку (хоть на иос, хоть на андроид): Вы всегда вынуждены создавать экран... Вы его не можете переиспользовать без дополнительных танцев с бубном в случае с адаптером. А в случае с медиатором можете. И вот медиатор нас возвращает к MVx, в котором средний слой выше!, чем два соседних.

            Опять же, если рассматривать

            Мне настолько уже удобно использовать медиатор, что я и в чистую архитектуру его засунул. Хотя его там нет. А зря. Мне кажется, что если заменить адаптер в среднем слое clean на медиатор, то всем сразу заживется на порядок проще. И помирит, кстати, MVx, clean и кажется, все другие архитектуры :)

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

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


            1. kryanod Автор
              00.00.0000 00:00
              +1

              Это приводит к проблемам переиспользования: доменную модель мы можем переиспользовать даже между платформами, а вот вью между приложениями уже не можем, т.к. даже имеющиеся вьюшки мы должны адаптировать под протоколы более низких слоев.

              Да, всё так. (не случайно даже обложка у статьи представляет доменный слой королём кодовой базы). Предполагается ведь, что доменная область важнее всех, а вот эти ваши вьюшки можно выбрасывать и заменять на новые. Причём это же не только в чистой архитектуре такой перекос в доменную важность: Hexagonal Acrhitecture была сформулирована ещё раньше, чем Clean, а по сути всё то же самое.

              В реальном мире вьюшки тоже хотят защищённости, но в их защиту никто не становится. Их воспринимают как плагины, которые можно по щелчку пальцев менять. Тот же Роберт Мартин на своих выступлениях регулярно приводит пример: мол, вы должны иметь возможность выбросить свой UI-фреймворк и заменить его на CLI. А на практике, я убеждён: никогда ещё ни одному мобильному разработчику не приходилось выкидывать своё SDK и делать вместо него консольный интерфейс. Да и подменить всё равно не получится. Мне очень понравилось, как об этом сказал Мартин Фаулер: мол, чтобы инвертировать зависимости и обеспечить чёткую границу между слоями, вы конечно должны обернуть весь свой UI-фреймворк в интерфейсы и максимально притвориться, что вы от него не зависите, но всё равно вы не сможете на 100% от него изолироваться. Речь причём не об изоляции на уровне кода, а изоляции на уровне концепций. Всё равно где-то придётся подстраиваться под то, что какие-нибудь А и Б — это разные экраны, и им нужны разные данные. И мы всё-таки немного подстраиваемся под них.

              Вот это всё, как мне кажется, и есть основная причина комплекса доменной неполноценности.

              А насчёт адаптеров и медиаторов вы прямо супер интересную тему подняли.

              Если в этих терминах говорить, то можно сказать, что есть 2 уровня адаптеров. Я представляю доменный слой скорее как чёрный ящик, у которого со всех сторон есть рычаги и розетки. К нему с одной стороны приходит UI и дёргает за рычаг. А с другой стороны, в одну из его розеток воткнута вилка от репозитория, который уже сам там себе шуршит и получает данные. Так вот, розетка — интерфейс, а вилка с проводом — адаптер. Причём адаптер первого уровня. А можно и весь доменный слой воспринимать как один большой адаптер между presentation и data, но это уже тогда будет адаптер второго уровня.

              Так вот, почему я про эти уровни адаптеров заговорил. Мне кажется, вашу проблему можно решить и без изменения парадигмы чистой архитектуры, т.е. не трогая адаптер второго уровня (аки доменный слой). Чистой/шестиугольной/луковой архитектурам главное защитить доменный слой, на вьюшки им плевать, но нам это никак не мешает: мы можем в теории написать условную вьюшку так, чтобы она тоже заизолировалась своим интерфейсом. И тогда вьюшка тоже становится чёрным ящиком со своей розеткой. И получается, что нам уже нужен провод с двумя разными вилками: одна сторона включается в доменный ящик, вторая в презентационный ящик. И тогда, с одной стороны, вьюшку можно переиспользовать, а с другой стороны, с точки зрения доменного слоя вообще ничего не поменялось. Нужно ли так делать всегда универсально, и стоит ли все вот эти провода с вилками на концах (я даже не знаю, медиаторы ли они уже, или всё ещё адаптеры) вообще обозвать отдельным слоем — вопрос открытый :)


              1. ws233
                00.00.0000 00:00
                +1

                Рад, что тема зашла. Будет хоть с кем обсудить. А то я пока кому ни скажу, меня не очень понимают. Скорее потому, что сам еще, как следует, сформулировать не могу.

                Кстати, хорошая метафора. Адаптер – он и есть адаптер. С одной стороны "мама", с другой - "папа". Их мы все видим в хозяйстве и как использовать понимаем. А Медиатор - это шнур с двумя "папами". Такое у нас встречается все же реже. Но оно в обиходе требует меньше проблем. Представьте, если бы вы соединяли монитор с компом через адаптер, а не одним шнуром, да? :)
                Забрал метафору себе в коллекцию. Буду объяснять теперь на примере.

                Что касается вопроса, нужно ли так делать всегда, то есть ощущение, что нужно. Apple пишет так:

                View objects and model objects should be the most reusable objects in an application. View objects represent the "look and feel" of an operating system and the applications that system supports; consistency in appearance and behavior is essential, and that requires highly reusable objects. Model objects by definition encapsulate the data associated with a problem domain and perform operations on that data. Design-wise, it's best to keep model and view objects separate from each other, because that enhances their reusability.

                A goal of a well-designed MVC application should be to use as many objects as possible that are (theoretically, at least) reusable. In particular, view objects and model objects should be highly reusable. (The ready-made mediating controller objects, of course, are reusable.) Application-specific behavior is frequently concentrated as much as possible in controller objects.

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

                Если вы переиспользуете вьюхи, то результат вы получите быстрее, чем если не переиспользуете их и работаете с адаптерами.

                Это цель MVC. И MVP, я полагаю, тоже. А какая цель у clean?

                3. Independent of UI. The UI can change easily, without changing the rest of the system. A Web UI could be replaced with a console UI, for example, without changing the business rules.

                Вроде та же. Тоже сделать UI переиспользуемым. Но делает ли Clean переиспользуемым UI? Мой пример про сервер или дизайн показывают, что вряд ли. Я вынужден дорабатывать UI, чтобы он подключился к имеющемуся у меня промежуточному слою. А на кой черт тогда этот промежуточный слой, если он все равно заставляет меня дорабатывать одну из точек взаимодействия с внешним миром? А вот если промежуточный слой сделать по шаблону "Медиатор", то я его создаю сразу под обе стороны, которые я не контролирую, но зато обе эти стороны я использую, как есть. Взял код домена на КММ и впихнул его в веб :)

                При этом, получается, что все мое приложение, которое я пишу – это как раз медиатор. Все остальное я не трогаю. Причем, этот медиатор свой на иос, на андроид, и на вебе. А все остальное – переиспользуется.

                Кажется, таким и должно быть программирование? Все специфичное для конкретного приложения в самом верху, все остальное – внизу и переиспользуется или взаимозаменяется? У Мартина Фаулера это называется app specific logic.

                A screen and components with all application specific behavior extracted into a controller so that the widgets have their state controlled entirely by controller.

                А цитату Элла по этому поводу я привел выше в последнем предложении ^.^

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


                1. kryanod Автор
                  00.00.0000 00:00
                  +1

                  А какая цель у clean? Вроде та же. Тоже сделать UI переиспользуемым.

                  Не совсем. Вот здесь

                  Independent of UI. The UI can change easily, without changing the rest of the system.

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

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

                  С языка сняли! Я прямо загорелся вашей идеей про медиаторную архитектуру. Обязательно попробую на практике.


                  1. ws233
                    00.00.0000 00:00
                    +1

                    Спасибо за содержательную дискуссию. Еще один плюсик к моей теории. Действительно же. Независимый UI не есть переиспользуемый. А так хочется переиспользуемого UI и мне тоже! :)

                    А! и дайте знать в личку, что получится, пожалуйста. Мне это важно.


  1. Paul85
    00.00.0000 00:00
    +1

    Интересные мысли в статье. Хотя про такой комплекс слышу впервые.


  1. dmt_ovs
    00.00.0000 00:00
    +1

    А кто-нибудь кроме автора слышал вообще о таком "комплексе доменной неполноценности"?
    Без сарказма, реально не знал о таком)


    1. kryanod Автор
      00.00.0000 00:00
      +2

      Нет, думаю, никто не слышал :) Я его так обзываю, потому что феномен есть, а общепринятого термина нет.