Pull to refresh

Comments 203

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

Отличная статья. Спасибо. Особенно за главный посыл: «Все мы пираты по натуре».
Хорошая статья!
Про отсутствие знака равенства между MVP и Clean Architecture (и любой архитектурой приложения в целом) можно бы и поподробнее, а то заблуждение довольно распространенное. Мне даже как то минуса прилетали за попытки его опровергнуть

Вы правы, и на этот раз вам за это прилетают плюсы. :)

Продолжается соревнование сколько же классов надо чтобы вывести список заказов на экране :)

Не знал, что есть такое соревнование. Есть ссылка? ;)

Сколько слоев, абстракций, интерфейсов и классов…
Но потом в 9 из 10 случаев выясняется что никто и не планирует никогда менять БД, менять протокол доступа или чтобы то ни было вообще менять, а всё равно через 3 года потребуется переписать.

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

Меня сильно напрягает стремление каждый слой отделить строго своим собственным набором сущностей/DTO объектов (абсолютно идентичных), а потом перегонять всё через AutoMapper туда-сюда по несколько раз. Поверх них еще навернуть абсолютно на любую зависимость интерфейсы, на каждую сущность интерфейсы либо абстрактные классы. Потом приходят люди и пытаются это осознать месяцами.

Полностью согласен с последней мыслью «Лучше красивый, хорошо работающий код, чем архитектурное спагетти с соусом из недопонимания.», и благодарен что она прозвучала. :)
Но всё равно часто забывают про целесообразность накручивания, именно целесообразность, а не «потому что могу забубенить такое».
отделить строго своим собственным набором сущностей/DTO объектов (абсолютно идентичных)

Зачем, если они идентичны?
Потому что — было так канонично. На самом деле в статье про это упоминается, что в своё время маниакально стремились под каждый слой сделать отдельные DTO, аукается до сих пор.

Согласен с тем, что не нужно overengineering делать только для соблюдения "канонов". И старался это донести в статье. Даже хотел добавить о принципах YAGNI и KISS.

C KISS и YAGNI и надо начинать любую работу и придерживаться их до тех пор пока не станет очевидно, что архитектура конкретного приложения требует усложнения. Ну и всем начинающим программистам следует понять и освоить именно эти принципы проектирования и придерживаться их как можно дольше. Согласно Старджону, 90% всех проектов - очень простые и не требуют сложновыдуманных архитектурных подходов :)

Тем не менее в этой же статье и сказано, что не всегда стоит так делать.

Потому что это вопрос не разделения слоев (слои разделять надо), а выделения слоев. Описанный вами случай получается именно тогда, когда слои выделяют преждевременно, а не когда их в принципе разделяют. Ну а навешивание интерфейсов на все подряд — это банальное непонимание DI (который инъекция), к слоям оно имеет такое себе отношение.

Как тут не вспомнить Эйнштейна: «Всё должно быть изложено так просто, как только возможно, но не проще».
UFO just landed and posted this here

Кажется, это не фабричный метод, это фабрика оформленная как статический метод.

Т.е. вы предлагаете, чтобы объект User знал об объекте UserDto? (И в случае изменения объекта UserDto нам надо помнить, что надо поправить методы в User (User2, User3 и т.п....)?)

Ну, да. А потом мы в коде наблюдаем тут и там ::getInstance и кучу других замечательных статических методов, отсутствие DI-контейнера (и незнание, что это такое вообще) и каких бы то ни было тестов. А что? Всё равно ж БД менять не будем, всё равно не понимаем отличие сущности от DTO и называем и то и другое моделями. Поэтому зачем усложнять?

Я всегда думал, что Entity — это POJO. Тогда что вы понимаете под объектами бизнес-логики? Статью я прочитал, поэтому прошу не определение, а конкретные примеры

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


Есть проблема в сочетании "бизнес-логика". В понимании разработчиков оно стало синонимом слова "логика" в целом, как мне кажется. Я поэтому стараюсь избегать его и говорить "логика бизнеса" или "логика приложения".

Можете, пожалуйста, привести примеры когда это «просто логика», а это «логика бизнеса»?

Конечно. Просто логика — это логика в общем понимании, любая. А логику бизнеса я описал выше.
Другими словами, есть те, кто путает понятия логики приложения и логики бизнеса.


А вы хотите похейтить или какой-то конструктив будет? ;)

Конструктив, наверное, но сначала хотел услышать мнение, не испортив его своим. :)
По моему опыту, я как то перестал видеть грань между «просто логикой» и «бизнес-логикой», став называть всё «логикой».
Обычно под «просто логику» попадает сначала то, до чего Бизнесу пока нет дела. Но со временем, этот кусок логики может затронуть бизнес и начать влиять на него. Тогда она превращается в бизнес-логику.
Касалось это по сути всего: обработки ошибок, значений по-умолчанию, валидаций и т.д.
Больше похоже на middleware или behaviors

Вот это вы зря привели такой пример. Если попробовать написать такой код, то будет сложно отделить Entity от UseCase. Как раз скидка 20% это usecase (interactor), а entity станет plain-object. Если масштабировать пример на разные правила скидок, то каждое правило будет своим объектом usecase, а список покупок будет List. Так удобнее даже в самых простых случаях когда один товар продается со скидкой.

Все зависит от того, что вы понимаете под use case. Скидка это Entity. И это не plain-object. Так же как вы написали можно масштабировать и с Entity, не вижу разницы.

Какой класс будет заниматься собственно применением скидки к заказу? Класс заказа, класс скидки?

А какая разница?
Да хоть обычная функция, принимающая в качестве параметров скидку и заказ.
ООП — это офигенно, но часто люди понимают слишком буквально.

Разница есть, потому что "обычных функций" много и их распределение по "модулям" важно. В этом и суть архитектуры. Иначе получится что каждая сущность превратитcя в god-object.

Важно, но распределение по слоям — важнее (на порядки).
Фактически (на крайняк) можно считать entities тупо одним модулем (с каким-то количеством классов или даже PODO-структур внутри, вокруг которых набросаны методы/функции). Главное не смешать entities и view или entities и db (например), а разные entity как-то между собой разберутся.

Все зависит от use case когда нам нужна скидка. Пусть надо показать товары и цену. Для этого будет use case. Он получит Entity для расчета скидки, товары. Используя то что у него есть и применив логику для расчета скидки получит цену. И отдаст дальше.
Поэтому я отвечу, что Interactor, используя логику из Entity.

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


Какую логику из Entity будет использовать Interactor?

Пусть у тебя там 11 печенек. Берешь этот лист печенек, даешь их CookiePriceEntity, например. Та смотрит, что 10 делится на 2 и считает их цену за вычетом 20%, плюс цена 11-й печеньки. Ты получил свою цену в CalculatePriceInteractor-e своём, и отдал в UI.

CookiePriceEntity имеет только одну функцию получения цены со скидкой? Это только entity? Что тогда делает interactor?

В предыдущем посте же я написал, что он делает…
Реализует use case, то есть то, что нужно пользователю — отображение цены. Используя entity для подсчета цены со скидкой в нашем примере.

Я кажется понимаю, что именно у всех вызвало "несварение". Смотрите: Ентити = список полей с данными + набор функций над этими полями.


Там не может быть методов, которые затрагивают другие поля или Ентити.


Никто же не будет выносить метод toString или hashcode в UseCase? В Entity только методы самого объекта, которые одинаковые в независимости от платформы, где используется этот объект.


Например представим метод getShortDescription, который возьмет только первый 20 символов из поля description.


  • Если эта логика обусловлена бизнесом, то такому методу самое место в Entity.
  • Если это особенность какой-то логики обединения description из разных Entity — то это будет в интеракторе.
  • Если это только особенность UI, то это будет в презентере

Уходим от темы в неизвестном направлении. Давайте вернемся.


Рассмотрим простой случай. Есть простая логика расчета скидки. "Три по цене двух". Есть простой класс, который эту логику реализует.- — берет заказ на входе и возвращает "рассчитанный заказ" на выходе.


Этот класс будет entity или interactor\usecase? И почему?
Класс "логики скидки" своих данных не имеет и никуда не сохраняется.

По сути это будет Domain Service — вырожденный случай Entity без собственных данных (хотя внутри может быть какая-нибудь мемоизация, например). В слоях и поста он будет относиться к слою entity, а вызываться из слоя interactor\usecase. Почему? Скорее всего потому, что правило вычисления скидки независит от сценариев использования. Сценарии могут определять вычислять скидку или нет, по какому из доступных способов вычислять скидку ("десять процентов", "три по цене двух", "накопительная скидка по бонус-карте"), но сам алгоритм вряд ли зависит от сценария использования и может использоваться во многих.

Вы привязались к примеру со скидкой.
Он не абсолютно удачный, так как можно сказать, что скидка на печеньки — особенность мобильного приложения одного конкретного магазина, при таком взгляде скидка будет в юзкейсе. Так как при переносе Entity на другую платформу, скидку переносить не нужно.
Но если взять пример Jeevuz, в котором скидка на печеньки — некоторая базовая штука (представьте, что это правило всех землян — делать скидку на несколько печенек), то это правило описывается, например, прямо в классе печеньки — и тогда все будет как описано

Если нет бизнеса, и наше приложение само по себе, то скидка все равно может быть в Entity по определению:


If you don’t have an enterprise, and are just writing a single application, then these entities are the business objects of the application. They encapsulate the most general and high-level rules. They are the least likely to change when something external changes.

Просто скидка что-то очень общее. То что вряд ли будет меняться часто. И не будет зависеть от того надо ли показывать ее с бонусами или без скидки цену выводить и тп.


И опять же:


прямо в классе печеньки

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

Просто скидка что-то очень общее. То что вряд ли будет меняться часто.

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

<offtop>
На самом деле Класс не может быть таким.
Класс обязан иметь данные (необязательно известные, например, abstract/pimpl) или быть таким, что сам факт его создания/уничтожения на что-то влияет (RAII), иначе это просто набор функций, а не класс.
Класс не набор функций, класс — это спецификация для экземпляра. Когда мы декларируем класс Foo, мы должны сразу задуматься над тем, что олицетворяет собой один экземпояр этого класса; на что будет влиять то, вызвали ли мы foo1.bar() или foo2.bar() или даже foo1.clone().bar(); если ни что / ни на что, то это не класс.
Хотя, конечно, в языках, которые не поддерживают нормальные namespace'ы, каждую функцию приходится волей-неволей пихать в «класс» (с приватным конструктором, «static class» или singleton). Но они от этого классами не становятся, просто наборы функций. Java/C# в этом плане здорово подосрали, испортив восприятие многими людьми парадигмы ООП.
</offtop>

А так полностью согласен в ответом terramok, что этот «класс» (на самом деле не класс, а просто subnamespace, но кто ж из современных ЯП такое выводит в отдельную категорию) должен быть или в entities, или в use-case в завимисоти от роли скидок.
Java/C# в этом плане здорово подосрали, испортив восприятие многими людьми парадигмы ООП.

Как по мне, то началось это с C++, но дело не в неймспейсах, а в отходе от парадигмы "всё есть объект и даже класс". С этой точки зрения статические методы вполне допустимы, если воспринимать класс как объект, знающий как создавать экземпляры определенного типа, как фабрику. Ну а ответом на вопрос "как создавать" вполне может быть "Никак, запрещено".

Та в том, то и дело, что не всё — класс.
В этом и разница между ООП и культом карго а-ля ООП; как в процедурных языках не всё — процедуры, в функциональных языках не всё — функции, так и в объектно-ориентированных не всё — объекты/классы.
Статические методы запрещать не стоит. А вот запрещать функции прямо в namespace'е (в Java и C#) было маразмом. И делать namespace' неполноценной сущностью (почему нельзя в одном файле сделать несколько namespace'ов, в смысле сразу дочерние namespace'ы? почему нельзя implement'ить concept/interface namespace'ом? и т.д.)
Ну а ответом на вопрос «как создавать» вполне может быть «Никак, запрещено».
И тогда это не класс :) по-моему (если запрещено создавать вообще, а не protected constructor / abstract class).
Так что C++ даже больше ООП, чем…

Есть языки, в которых всё объект, ну или всё выглядит как объект. Правда, мне для таких языков больше нравится "объектные", а не "объектно-ориентированные".


И тогда это не классИ тогда это не класс

Класс, поскольку является спецификацией объектов определенного типа. Спецификация гласит "Объекты данного типа в истеме запрещены" :)

Есть языки, в которых всё объект, ну или всё выглядит как объект.
Я в курсе. Но я с этим не согласен: нужно отделять мухи от котлет. И уж особенно убого попытка запихнуть всё в классы выглядит в современных мейнстримовых языках (в каких-то очень хитрожопых языках спецназначения концепция «всё объект» может быть и прокатила бы, но для универсального языка это не так).

Спецификация гласит «Объекты данного типа в истеме запрещены» :)
Эээ :). Это было бы логично, если бы не одно но: объекты такого вида реально не запрещены. Вот если бы можно было декларировать какие-то общие требования а-ля: «запрещено создавать объекты с методом equals, но без метода hashCode», «запрещено создавать объекты, у которых есть поля foo и bar одновременно» (второе очень абстрактный, почти бесполезный вырожденный пример, но показывает corner-cases идеи о требованиях) и т.п. — то это было бы нормально. Но ведь если задекларировали class {X integer, Y integer} или class {method getSomething() {…}} как static (или с private constructor'ом), то никто не мешает рядом задекларировать такое же без пометки static (с публичным constructor'ом) и создавать экземпляры уже его. Поэтому логически это лишено смысла.

Зачем декларировать что-то, что нельзя создавать? Я понимаю, что существующие средства (классы) пытаются натянуть на свои цели (создать группу методов). И вроде как потребность удовлетворена, концепцию namespace'ов можно до конца не развивать. Но это ошибочный путь, свернув с логичной дороги, мы только теряем и будем терять ещё больше.
Редакция не прокатила:
В каких-то экспериментальных или специальных языках, где само понятие «объекта» более размазано, такое может и прокатило бы. Но для универсального языка это неправильно (причём проблема даже не только в засорении понятия объект, а и недоразвитии параллельных направлений из-за этого).
ИМХО, конечно. Но это ИМХО для меня важное. Я просто не понимаю, как люди могут мыслить по-другому.

Будет Entity. Потому что это логика общая для всего бизнеса, а не только для приложения.

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


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


логики обединения description из разных Entity

Эта фраза подразумевает, что Entity объект. С чем я не согласен, как уже сказал.
Это не обязательно. А когда Entity не объект, то такая логика может быть в Entity, а не в интеракторе. Интерактор возьмет Entity, даст ей список разных description и Entity сделает что нужно с ними.


Интерактор же может потом еще другую логику из другой Entity применить или логику, которая не от бизнеса, а специфичная для приложения.

Тут надо четко разграничать в каком контексте мы говорим об Entity. Entity дяди Боба — это Domain Эванса, состоящий из его Entity, ValueObject, Repository, Event, Service и т. д.

Оно может быть просто функцией

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

Так и сделать сущность отдельную. А функция её примет. Зачем логике внутреннее состояние?
Не обязательно смешивать логику и сущность, я об этом.

Так соль-то в том, что как раз сущность и обладает бизнес-логикой. Иначе она будет анемичной с геттерами да сеттерами.

А что в этом плохого?

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

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

Тогда это уже не сущность, а DTO :)

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

Смешивать не проблема. Просто я говорю, что это не является обязательным.

Запоздало конечно, но только сейчас увидел статью, за которую отдельное спасибо, ибо всё правильно: рекомендации, а не жесткие правила, как порой встречается в энтерпрайзах.

Замечание по примеру:

Сегодня бизнес решил что 2 печеньки продаем всегда(!) со скидкой в 20% .. и это внеслось в общие бизнес-правила, где-то построился метод энтити "печенька::Цена()" с учетом скидки. Но .. бизнес - дело такое. Чаще всего, это далее развивается так:

  1. Нет. Продаем 3 печеньки со скидкой;

  2. Нет. Продаем без скидки, кроме "ключевых клиентов";

  3. Нет Продаем 2 печеньки со скидкой 20%. (вернулись на круги своя)

И вот тут, во всей красе и проявляется кмк "Чистая Архитектура" как в части отделения слоев входа/выхода от бизнес-правил и сущностей, а также пропущенное в статье правило "единой ответственности" - в данном случае entity должно иметь ровно одну ответственность - т.е. ровно один тип пользователей, а не классов приложения! Дядя Боб помнится это тоже выделял явно и оно точно также путается много где..

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

Уже становится сложным представить современное Android-приложение без RxJava.

Посмотрите официальный исходный код телеграмм клиента. По моему достаточно большой проект. Но там нет данных паттернов и rxjava. (насколько я разобрался)

Это все хорошо — разделение на независимые компоненты итд + dependency injection итд. Но очень часто нет четкого понимания всего приложения — каким оно будет в конечном итоге, какие фичи будут реализованы, какие нет. Требования к приложению часто меняются очень быстро и банально приходится переписывать целые куски.

Если же сразу закладываться на расширяемую архитектуру, то не понятно насколько ее потребуется расширять в будущем(чтобы пользоваться преимуществами такой архитектуры). Это я все к тому, что часто проще начать писать как можно проще, чтобы как можно быстрее сделать прототип. Конечно какие -то базовые вещи надо соблюдать — отдельно модели, отдельно http client, что-то вроде single responsibility.

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


Не понял при чем тут Телеграмм и Rx… Использовать технологию или нет — дело разработчика.

Я вот не очень понимаю что в вашем понимании "архитектура"? Это отдельные артефакты, которые не являются кодом приложения? Или отдельные классы, которые делают что-то (что?) Или отдельная активность (какой результат этой активности)?


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

Архитектура это набор рекомендаций/правил к тому как это делать.

Тогда что значит "без архитектуры"? на каждый блок кидать кубик — как лучше делать? Все равно человек своими внутренними правилами руководствуется, потому что так мозг меньше напрягается.

Спасибо за прекрасную статью! Многое разложилось по полочкам.
Есть вопрос по реализации репозитория, который Gateway. Если мы радостно впустили Rx в свою жизнь, чему я несказанно рад, то кто отвечает за управление потоками, в виде навешивания subscribeOn(...) или ещё чего-то? Я встречал два противоположных взгляда:


  1. "Я умный репозиторий, и я сам решу, в каком потоке у меня будут идти запросы в сеть"
  2. "Я синхронный репозиторий, и мне всё равно, в каком потоке меня будут использовать"

Вот с тем, что навешивать observeOn(mainThreadScheduler) на Interactor — это ответственность презентера, всё вроде ясно, хотя и тут нет единообразия. А как быть с фоновыми потоками?

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

Спасибо за статью.
Можете объяснить, какой будет поток данных в случае, если необходимо получать данные от сервиса геолокации, к примеру, и обновлять UI без явного запроса пользователя?

Геолокация = данные. Данные получает репозиторий. Явного участия юзера нет, но косвенно он захотел получать обновления местоположения установив/открыв приложение. Поэтому где-то на открытии мы можем вызвать свой "use case отображения местоположения" а тот уже попросит репозиторий уведомлять его и будет отдавать результат в UI.
Примерно так.

Самое главное в этой статье — чтобы у всех в том числе у автора пришло бы понимание, что данная архитектура пришла из больших бизнес систем. Где основное требование — это их полная адаптивность и независимость, позволяющая быстро перестраиваться под изменяющиеся внешние требования. Поэтому например автор ратует за RxJava(которую я думаю специально притащили с винды) — не понимая, что она жестко требует от системы наличия ПОСЛЕДОВАТЕЛЬНОСТЕЙ и только ПОСЛЕДОВАТЕЛЬНОСТЕЙ. И если вам потребуется завтра где-то ввести параллельную новую ветку — вся ваша система слетит. В мире Clean Architecture царят только события. И правильная(продуманная) обработка событий — основа данной архитектуры. Поэтому вопрос как делить на слои и сколько будет слоев — абсолютно вторичен и не играет большой роли. Важно чтобы слои были абсолютно независимы. Чтобы объект слоя принимал и отдавал события (объект, который содержит все что нужно для дальнейшей обработки). Поэтому бесперебойная транспортная система — это основа данной архитектуры, от которой зависит вся система в целом. И на андроиде просто приходиться подстраиваться под ее lifecycle, соответственно подстраиваясь под все повороты. Как и в жизни. То заказчик отъехал, то деньги не заплатил, то ему уже не нужен товар. Clean Architecture — это попытка достоверно описать нашу жизнь. Ее оптимизировать и улучшить

Последовательность != Прямолинейность. RxJava отлично справляется с параллельными ветками. И любой поток — последовательность событий. Так что, я не понимаю к чему первая половина коммента.

Последовательность — это разделенные по времени(асинхронные) события. Т.е. если смотреть сверху в течении промежутка времени — то параллельный поток событий можно отобразить на последовательность событий. Т.е. RxJava возможно использовать, если Вы гарантируете, что при дополнительном ветвлении параллельного потока событий не произойдет смена логики. Например — вы поставляете товар заказчикам. Обычная последовательность. Вдруг ваш основной заказчик — вдруг не может оплатить/разгрузить товар или любое др событие. Ваша последовательность заказал — доставил — оплатил ломается. Вывалиться по ошибке — можно (у RxJava только 2 состояния). Но это ваш основной заказчик. Или на Android — например Activity имеет несколько взаимозависимых фрагментов. Но Activity создает фрагменты в любом порядке — ей плевать на все ваши зависимости. Или жизненный цикл activity — когда событие onDestroy activity может приходить после события onCreate этой же activity — хотя по логике activity должна быть уничтожена, а потом создана — но это Activity — ей можно все (это встречается повсюду в тесте monkey, когда события сыплются как из пулемета). Вот на таких последовательностях и ломается RxJava. Или более сложная система — система с гарантированной доставкой. Поддерживает несколько каналов связи (абсолютно ничем несовместимых кроме интерфейса — принять/передать) и динамически сменяет каналы в зависимости от возможностей среды и имеет сложную логику контроля доставки, которая не подразумевает 100% вероятности доставки. Т.е. RxJava — это решение, когда подразумевается 100% вероятные решения (положительные/отрицательные), когда внешняя логика поведения гарантированно неизменна. А не так как на Android — захочется ей и тебе прилетит в listener null вместо объекта (monkey очень дает много пищи для размышлений)

Вы вешаете проблемы Android на плечи RxJava.
Это инструмент. Не нравится не пользуйтесь.
Если вам не нравится ArrayList вы можете писать на массивах. Это ваш выбор.
Не вижу особой связи со статьей. Я сказал, что можно не париться о RxJava, но вы не обязаны этому совету следовать. Не хотите — не используйте. В чем проблема-то?

RxJava пришла с платформы Windows. Она имеет все особенности Windows (потому что на ней писалась). Попытки привязать к ней lifecycle и прочие плюшки android — остаются плюшками. Использовать систему написанную под одну систему на другой — глупо, но можно. Как например использовать реляционные БД для хранения объектов (которые разрабатывались вообще не для этого и должны использоваться не для этого) и соответственно писать ORM для реляционных БД. Использовать можно все. И микроскопом можно забивать гвозди. Но давайте использовать лучше молоток для этого, а микроскоп использовать для другого. Т.е. оставим каждому инструменту свою область применения.

Rx никаким боком к windows не привязана ;)

Сама идея прекрасна, но ее лучше всего реализовать на объектных БД. Она начинает сильно проседать на объектах с большим количеством листьев. Т.е. возьмем счет-фактуру — пока в ней до 10 строк все более менее прекрасно, но когда более 100 записей — тянуть хвост в 100 записей, когда дай бог изменяется 1 строка глупо. Т.е. для каждой области должны быть свои решения. Много деревьев и мало листьев — самое место ORM. Где надо работать с огромными простынями — глупо использовать ORM. Просто решения для БД в 2 Мб одни, а для 500 Мб на мобиле с андроид 2.36 и 48 метрами под activity совсем другие. Тогда уже смотрят как выбирать данные — курсорно (аля Oracle) или листами(кортежами — наборами строк как в MS SQL). Тогда уже не до объектов — а строго только через View БД, включающими только столбцы для просмотра.

А зачем тянуть 100 строк? Почему нельзя вытянуть одну строку и поменять её?
И при чем тут ORM?

Эмм, а зачем для объектных СУБД ORM? ORM как раз и нужна для того, чтобы сымитировать объектную СУБД из реляционной. Если СУБД уже объектная (по-настоящему объектная, а не маркетологически), то ORM не нужен.

тянуть хвост в 100 записей
Это вопрос качества ORM. Хорошая ORM (правда на практике я таких не видел, кроме недописанной своей) позволяет загружать любые данные сразу, по востребованию или никогда. А-ля var bill := Invoices.byId(7631).load('itemCount', 'totalPrice', 'items(filter: $.enabled, order: [$.title ASC], offset: 20, limit: 10)') — загружает счёт №7631, при этом в дополнение к стандартным свойствам загружает лишь количество строк, общую стоимость и те-из-строк-которые-enabled-отсортированные-по-title-начиная-с-20й-по-29ю (остальные связи один-ко-многим и даже многие-к-одному у Invoice пока не загружаются).

Много деревьев
По-моему, деревья всегда очень глубокие.

строго только через View БД
Хорошая ORM ни к каким реляционным возможностям доступ не ограничивает. Объектное возможности — это строгое надмножество реляционных возможностей. Делая объектный адаптер к реляционному, ми теоретически ничего не теряем.
Хорошая ORM ни к каким реляционным возможностям доступ не ограничивает. Объектное возможности — это строгое надмножество реляционных возможностей. Делая объектный адаптер к реляционному, ми теоретически ничего не теряем.

Чисто только теоретически. Это тоже самое, что и связывание таблиц в SQL запросах. На бумаге и в теории все просто. В реальности уже на join таблиц > 8 получаешь проседание, а при >16 оно может быть просто критическим.
В реальности например всегда требуется просмотр всего инвойса. Соответственно ORM тянет все содержимое инвойса — целостной сущности. Меняем 1 строку и записываем — ORM по этой команде должна обновить инвойс — что делается почти всегда 2 операциями — удалить старую сущность, а затем добавить новую сущность. При этом вы всегда попадаете на удаление/вставку туевой хучи данных. Это как использовать 1с из коробки. Все прекрасно пока листьев мало. Т.е. все прекрасно — но только для конкретных условий. Разработчик не может мыслить реляционно — бога ради пускай использует ORM пока не нарвется (а в большинстве случаев — и нет) в провале производительности. Вопрос для примера — заказчик требует просмотра сразу отсортированных 10000 записей на экран смарта(ему так нравиться). Как это организовать в RecyclerView(помним что всего одна строчка кода — cursor.getCount() приводит к fetch всех 10000 строк в БД)? Но самый страшный вопрос — как организовать обновление этого списка при новомодном livedata(обновление UI при изменении данных). Помним, что при этом он может с легкостью выйти из просмотра списка(destroy fragment и все прелести после этого) и при этом он должен быть в рамках нашей архитектуры, когда UI отделен от выборки данных.
В реальности уже на join таблиц > 8 получаешь проседание
Можете привести пример реального проседающего запроса из практики? Чтобы я конкретно представлял, что именно Вы имеете в виду, а не думал о чём-то другом.
В моём представлении join из 8 таблиц будет сильно проседать только если СУБД по глупости выбрало неправильную стратегию join'а.
И как раз дополнительный слой между СУБД и потребилитем данных имеет все шансы позволить решать эту проблему автоматически; например, если СУБД стабильно выбирает неправильный порядок nested look для запроса SELECT i.*, o.* FROM order_items i JOIN orders o ON i.order_id=o.id WHERE o.date BETWEEN? AND ?, делать автоматически SELECT * FROM orders WHERE date BETWEEN? AND? и SELECT * FROM order_items WHERE order_id IN (<список id из прошлого запроса>).
Хотя, может, я слишком оптимистично на это смотрю.

В реальности например всегда требуется просмотр всего инвойса. Соответственно ORM тянет все содержимое инвойса — целостной сущности.
И правильно.
Меняем 1 строку и записываем — ORM по этой команде должна обновить инвойс — что делается почти всегда 2 операциями — удалить старую сущность, а затем добавить новую сущность. При этом вы всегда попадаете на удаление/вставку туевой хучи данных.
Мне кажется, даже наитупейшие ORM такого делать не должны. Только изменённая строчка записывается (одним UPDATE'м).

Разработчик не может мыслить реляционно
Проблема не в том, что разработчик не может мыслить, а проблема в том, что это неэффективно. Эффективно юзать объектную СУБД с объектным языком запросов. Но таких пока не подвезли (или те, что есть, по каким-то причинам не подходят), поэтому юзаем реляционную СУБД с прослойкой над ней.

Я не спорю, что большинство реальных имплементаций ORM suck. Но не стоит из-за этого считать ущербной саму идею.

Вопрос для примера… 10000 записей на экран смарта
Ну, 10000 записей на экран не влазят. По-любому должна быть какая-то page'инация или дозагрузка при прокрутке.
RecyclerView… livedata
Честно говоря, я не в курсе разработки для Android; можете для идиотов пояснить, в чём проблема?
UI отделен от выборки данных
Может быть я неправильно интерпретирую данную архитертуру, принимая желаемое (то, как считаю правильным писать я) за действительное (то, что описано в этой статье). Но по-моему действию, UI как раз должен управлять выбором данных — только через другие слои. То есть когда нужны ещё записи, он запрашивает не «ещё 10 строк из такой-то таблицы; а к каждой из них выкачать ещё то-то из таких-то таблиц; и произвести такие-то рассчёты над этим», а «ещё 10 entities по такому-то условию». Ну а дальше уже через Repositories в DB (хотя, ещё раз повторюсь, я никогда не писал для Android).
Я максимально пытаюсь уйти от архитектуры андроид с ее неполноценными жизненными циклами. И максимально адаптировать архитектуру под активный легкий клиент — пассивный сервер. Но жизнь возвращается и все эти livedata, над которыми смеялись разрабы 20 лет назад — как будто и не было этих лет. И в данных условиях приходится отказываться от пассивных серверов, к каким-то другим решениям. И в настоящее время ищется такое решение. Которое видится в архитектуре ядро + подключаемые легкие модули (аналог Clipper 5, если кто знаком с его архитектурой). Админ только управляет загрузкой/связыванием модулей. Не путать c Dagger 2 — правильной будет архитектура ядро + подключаемые(именованные) процессы (c EventBus внутри каждого) — но она очень тяжеловесна — но позволяет все.
Честно говоря, мало понял, о чём Вы говорите. Не в смысле, что Вы в чём-то неправы (и поэтому не понял), а просто не понял из-за малой осведомлённости.
В стародавние времена был такой язык Clipper (писала писала группа разрабов объединенных под крылом Nantucket). Они предложили революционную тогда технологию для приложения — микроядро(загружала/выгружала модули и делала сборку мусора). Идею портировали под Linix — но проект умер. По сравнению с Clipper Clean Architecture — просто дитя той архитектуры. Ты мог подключить динамически любые модули в любое время, разбить свое приложение как тебе нравиться — поддерживая интерфейс только модульности. Поэтому вопрос о Clean Architecture — это вопрос о частном разбиении на слои(модули), где каждый может разбивать свое приложение как хочет — и будет прав. Одно плохо, что в android начинают затаскивать кусочки всякой всячины. И приходится следовать моде. То узкие штаны, то широкие.

А для чего нужна куча джойнов? В крайнем случае один запрос можно разбить на несколько более мелких.


Вопрос для примера — заказчик требует просмотра сразу отсортированных 10000 записей на экран смарта(ему так нравиться).

Объяснить, что сразу 10 000 записей выводить на экран не стоит, а стоит сделать пагинацию или «бесконечную» подгрузку, и указать на производительность.

Клиент за 1 лицензию на 1 устройство платит на уровне лицензии 1с. Поэтому вопрос не стоит как-то отпадает сразу. Помним — это не Россия.
Использовалось несколько вариантов загрузки больших списков:
— тройная буферизация + метод скользящего окна
— постоянный курсор в памяти + fetch при достижении предпоследней страницы (самый тормозной)
— подгрузка данных в фоне небольшими порциями
у каждого метода есть свои плюсы и минусы. Хотелось бы узнать — кто еще что может предложить?

Не понял, как одно связано с другим.

Клиент хочет список из > 10000 строк и чтоб он появлялся с задержкой не более 2 сек и чтоб он лазил по-нему/скроллил/фильтровал/искал с задержкой не более 2 сек. И его не волнует, что это андроид например 2.36 (и 48 метров под activity). Он заплатил. И вот теперь может, кто подскажет еще решения полной работы с длинными списками. В свете последней новомодной livedata (помним что content provider в 99 случаях из 100 возвращает только то что таблица изменилась)
Для примера попробовать силы могу предложить — загрузите в андроид КЛАДР и попробуйте сделать в нем удобный поиск/фильтрацию (т.е. например проверку достоверности паспортных данных). И попробовать поработать например с местом жительства Солнечный/Советский
Как это организовать в RecyclerView(помним что всего одна строчка кода — cursor.getCount() приводит к fetch всех 10000 строк в БД)?
Paging Library недавно вышла. Да и до этого такое руками можно было сделать. В любом случае limit/offset в SQLite вроде есть.

как организовать обновление этого списка при новомодном livedata(обновление UI при изменении данных). Помним, что при этом он может с легкостью выйти из просмотра списка(destroy fragment и все прелести после этого)
LiveData является Lifecycle-aware. То есть, на нее может подписываться только обзервер, владеющий lifecycle-ом. И если обзервер в том состоянии, в котором не может обновить UI (stopped, например), LiveData просто не будет его уведомлять, а пнет подписчика новыми данными, когда он пересоздастся/вернется в started/resumed состояние.

Entity Framework:


var bill = ctx.Invoices.Single(x => x.Id == 7631);
ctx.Entry(bill).Collection(x => x.Items).Query().Where(x => x.Enabled).OrderBy(x => x.Title).Skip(20).Take(10).Load();
var itemCount = ctx.Entry(bill).Collection(x => x.Items).Query().Count();

Или вам принципиально чтобы это делалось одним запросом?

Из приведенного куска кода я не могу понять, в каком месте из БД загружаются поля самого bill. Вот это вот var bill = ctx.Invoices.Single(x => x.Id == 7631) — это чисто создание запроса (а потому будет lazy load или явный load по какой-то команде) или это уже загрузка?

Или вам принципиально чтобы это делалось одним запросом?
Безусловно. Само ORM из это потом может разбить на несколько SQL-запросов. Но запрос от пользователя к ORM должен быть строго 1.

И ещё Вы пропустили подсчёт суммарной стоимости (aggregate-сумма цен всех item'ов в счёте, даже тех, что не попали в выборку из-за не-enabled либо паджинации).

var bill = ctx.Invoices.Single(x => x.Id == 7631) — это уже загрузка.


Суммарная стоимость: var totalPrice = ctx.Entry(bill).Collection(x => x.Items).Query().Sum(x => x.Price * x.Count).


Полностью вот так получится:


var bill = ctx.Invoices.Single(x => x.Id == 7631);
var items = ctx.Entry(bill).Collection(x => x.Items).Query();
items.Where(x => x.Enabled).OrderBy(x => x.Title).Skip(20).Take(10).Load();
var itemCount = items.Count();
var totalPrice = items.Sum(x => x.Price * x.Count);
Ну значит Entity Framework, с моей точки зрения — ещё один отстой, не реализующий до конца ORM. Но спасибо за экскурс по нему. На практике часто приходится пользоваться тем, что есть, даже если оно некрасивое (а не велосипедить по каждому чиху).

И ещё — меня смущает — не будет ли Entity Framework выгружать все item'ы счёта для подсчёта стоимости (сделает ли она items.Sum(x => x.Price * x.Count) на уровне СУБД)?

Сделает на уровне СУБД, конечно же. Точно так же как на уровне СУБД делаются сравнение x.Id == 7631, проверка x.Enabled, сортировка по x.Title, а также операции .Skip(20) и .Take(10) и подсчет .Count()

Интересно, как это технически реализовано.
На вход Item::Sum передаётся closure.
Что дальше? Неужели как-то методами Reflection парсится эта кложура? Не думал, что в C# такое возможно.

Нет, на вход передается подготовленное компилятором AST.


Эквивалентный код:


var x = Expression.Parameter(typeof(InvoiceItem));
var body = Expression.Multiply(Expression.Property(x, "Price"), Expression.Property(x, "Count"));
var totalPrice = items.Sum(Expression.Lambda<Func<InvoiceItem, decimal>>(body, x));
Круто.

А в Java такое есть? (Я имею в виду: можно ли написать это сокращённо а-ля x -> x.Price * x.Count — явно-то дерево на любом языке можно задать.)

Нет, в Java такое не завезли.

PS Кстати, парсить байт-код через рефлексию тоже возможно, для этого есть библиотека DelegateDecompiler.


Но нам она не понравилась (в основном, из-за бага #52, который так и не исправили несмотря на мой PR).

То есть Вы фактически и решили багу, но они пока не приняли pull-request из-за отсутствия теста, формально соответствующего style guidelines — я правильно понял?

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

Тем, что ORM должен предоставлять действительно объектный интерфейс над реляционной СУБД — а не просто какой-то интерфейс (чуть побольше возможностей, чем чисто реляционный, но всё равно ни то, ни сё).

Вот в ту же степь: GraphQL.

P.S.: Хотя то, что C# в целом и Entity Framework в частности позволяют делать items.Sum(x => x.Price * x.Count) на уровне СУБД, меня, конечно, приятно удивило. Я ожидал чуть больших заморочек для проброса запросов на уровень СУБД (мой пример выше с totalPrice предполагал наличие явно объявленного в классе Invoice свойства totalPrice с некоторым атрибутом (аннотацией), принимающим строковый фрагмент квазиSQL-кода в качестве параметра). Но всё равно — они (Entity Framework) очень постарались, что бы реализовать неполный объектный интерфейс (лучше б наоборот: сделали красивый интерфейс, а потом постепенно улучшали реализацию, ИМХО).

Простите, кому должен? И кто вообще придумал эту глупость?
ORM помогает строить запросы к базе и отображать результаты запроса на объекты.


Ты еще больше удивишься, когда узнаешь что EF умеет делать "действительно объектный интерфейс над РСУБД", со всеми гадостями типа lazyload. Только им никто не пользуется в наше время.

«Должен» в смысле «„так правильно“ по моему личному мнению». Вы можете придерживаться другого мнения, кто ж спорит.

lazyload
«Загрузить всё то, что мне надо сразу (наиболее эффективным методом) по моему явному запросу» ≠ «lazyload».

Действительно объектного интерфейса в Entity Framework по этой причине не вижу. (Как и в большинстве других ORM, на самом деле.) (Хотя у него есть какие-то свои плюсы.)

EF умеет и LL, и практически любой запрос (с проекциями и джоинами) без ручного выпиливания SQL.


Действительно объектного интерфейса в Entity Framework по этой причине не вижу.

Тогда пример "Действительно объектного интерфейса" в студию!

То, с чего началась эта подподветка:
var bill := Invoices.byId(7631).load('itemCount', 'totalPrice', 'items(filter: $.enabled, order: [$.title ASC], offset: 20, limit: 10)') — загружает счёт №7631, при этом в дополнение к стандартным свойствам загружает лишь количество строк, общую стоимость и те-из-строк-которые-enabled-отсортированные-по-title-начиная-с-20й-по-29ю (остальные связи один-ко-многим и даже многие-к-одному у Invoice пока не загружаются)
— Entity Framework может?

То есть:
  • По умолчанию для каждого класса загружается какой-то его стандартный набор свойств. Каждое из которых может быть полем, значением-вычисляемым-СУБД, связью многие-к-одному, связью один-ко-многим или ещё чем-то. Хорошо бы стандартный набор свойств для каждого класса был настраиваемым — но обычно это просто все поля соответствующей таблицы (но без значений-вычисляемых-СУБД, связей и пр.).
  • Делая явный запрос к ORM (мол, загрузи мне то-то, то-то и то-то), есть возможность запросить свойства в дополнение к обычным (например, некоторые значения-вычисляемые-СУБД или часть из связей). Причём если дополнительное свойство — связь (т.е. фактически entity или набор of entities), то для него тоже можно запросить дополнительные свойства — и так до любой конечной глубины (аналогично вот этому). Причём это касается и запросов к ORM вида загрузи мне одну сущность, и запросов к ORM вида загрузи мне набор.
  • Потом, конечно, не загруженно можно дозагружать через lazy load или отдельными явными дозагрузками.


Вот меня интересует второй пункт, например:
var bill := Bills.byId(3287, [ //bill №3287 с доп. загр. сл. св.:
  Bill::itemCount, //значение-вычисляемое-БД
  Bill::totalPrice, //значение-вычисляемое-БД
  manyToOne(Bill::customer, [ //мн-к-од с доп. загр. след. св.:
    Customer::invoiceCount, //значение-вычисляемое-БД
    manyToOne(Customer::targetGroup, […]), //мн-к-од с доп. св.
    Customer::assignedManager //мн-к-од без доп. загр. свойств
  ]),
  Bill::amendments, //од-ко-мн (все строки, без доп. загр. свойств)
  oneToMany( //один-ко-многим (не все строки, с доп. свойствами):
    Bill::items,
    .orderBy = order(BillItem::title, ASC),
    .limit = 50,
    .rowExtraProps = [
      BillItem::productType,
      manyToOne(BillItem::performer, […])
    ]
  )
]);

При желании — нечто похожее несложно сделать средствами языка, поэтому не вижу смысла включать эту функциональность в ORM.


var bill = ctx.Bills.Find(3287)
    .LoadItemCount(ctx)
    .LoadTotalPrice(ctx)
    .LoadCustomers(ctx, ...)
Хех, так это, с моей точки зрения, сама суть ORM. ORM должен предоставлять пользователю объектный интерфейс (язык объектных запросов), иначе это не вполне ORM.

Потом, в Вашем варианте, насколько я вижу, операции выполняются пошагово. То есть бы сначала получаем Bill (ctx.Bills.Find(3287)), потом у него количество элементов (bill.LoadItemCount(ctx) — и возвращаем снова bill (offtop: method-chaining, IMHO, уже признак плохого дизайна)), потом получаем суммарную стоимость (bill.LoadTotalPrice(ctx) — и возвращаем снова bill), потом получаем заказчика (bill.LoadCustomers(ctx)). Это как раз то, чего я хочу избежать.

Мы должны просто передать в ORM спецификацию того, что мы хотим получить — и она должна обратиться в СУБД сама, сделав минимум запросов (точнее необязательно минимум, а сконструировав запросы наиболее оптимальным способом — а некоторых кривых СУБД разбить один запрос на два оказывается эффективнее). В нашем примере — это один однострочный запрос из таблицы bills и два многострочных запроса: из таблиц bill_amendments и bill_items.

method-chaining, IMHO, уже признак плохого дизайна

Лолшто?

Както-так:


from b in bills
where b.id = 3287
select new {
    Bill = b,
    ItemCount = b.Items.Count(),
    ItemsFiltered = b.Items.Where( i => i.Enabled).OrderBy(i => i.Title).Skip(20).Take(10)
};

Только смущает план этого запроса. Мне кажется лучше будет сделать два — один на bill и количество, а второй на items.


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

Неплохо. Но Вы потеряли totalPrice. И ещё вопрос: оно Bill, ItemCount и TotalPrice (если его добавить) одним SQL-запросом получать будет (или 3 разных)?

Если делать так — то одним. Но его план будет страшным, поэтому я никому не рекомендую так делать.

Хреново, что план будет страшным.
В идеале, по-моему, это стоит добывать двумя запросами: один (bills b JOIN (SELECT bill_id AS id, … FROM bill_items GROUP BY bill_id) s ON b.id=s.id) для свойств bill'а и агрегатов над item'ами (которые по сути тоже свойства bill'а) и второй собственно для item'ов (SELECT … FROM items — без группировки).
В идеале оно бы должно само разбить. Странно, что оно делает один. (Я ожидал, что оно по тупости наоборот слишком много запросов сделает.)

С чего бы оно много запросов делало? 1 запрос всегда преобразуется в 1 запрос.

(До слов «1 запрос всегда преобразуется в 1 запрос» я думал, что это временный баг, мол, пока генерится один запрос, а потом подправят.) Тогда это не то, что я думал, и смысла в этой фиче, соответственно, гораздо меньше.

Это не баг, а фича. EF (и другие linq-генераторы) не пытается умничать. Ты написал запрос — один запрос улетит в базу. Хочешь несколько — пиши несколько. Если написал что-то что не преобразуется в SQL — получи эксепшн, никакие воркэраунды EF делать не будет.


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


Вообще одно из качеств хорошего Фреймворка — он не умничает.

Я просто достаточно смутно представляю себе, как два несвязанные запроса из двух различных таблиц можно эффективно реализовать одним SQL-запросом. Например, получить все bill'ы с такой-то даты по такую, отсортированные по дате — и для каждого из них получить по 10 первых пунктов (пункты отсортированны по названию). Это как вообще одним SQL-запросом, по-моему, это рациональнее двумя делать — нет?

А в чем проблема?
1) Есть запрос, который возвращает нужные bill
2) Есть запрос, который возвращает по 10 первых пунктов для каждого bill
3) делаем джоин этих запросов


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


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

Уточню: получить первые 15 bill'ов с такой-то даты по такую (отсортированные по дате) — и для каждого из них получить по 10 первых пунктов (пункты отсортированны по названию).

То есть дублировать информацию про bill в каждом из 10 (или меньше) его item'ов? Ну, мне кажется, рационально так было бы делать в том случае, если основной запрашиваемой сущностью был item (и мы бы его джоинили с родительским bill'ом). Но когда (в нашем случае) основной запрашмиваемой сущностью является bill — это как-то странно.

И вообще, как Вы таки напишете запрос, возвращающий по 10 item'ов для каждого bill'а, соответствующего определённому условию? Может, как-то можно, но я этого способа просто не знаю. Только с двумя запросами.

В идеальном мире SQL должен был бы поддерживать табличный тип данных. Тогда бы мы написали:
SELECT b.*, (SELECT i.* FROM bill_items i WHERE i.bill_id=b.id ORDER BY i.title LIMIT 10) AS items /*в этой ячейке целая таблица (или курсор view)*/ FROM bills b WHERE …
Но в SQL пока такого не подвезли.


Offtop: хренасе хабр SQL-код рендерит, лучше уж <code>, чем <source>.

Напишу такой запрос через row_number() over (partition… order by ...). Это совсем несложно. В EF этакое делается как раз через skip\take


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


Кроме того затягивать в приложение за раз больше 1000 строк — вообще странная затея.

Т.е. WHERE row_number() OVER (PARTITION …) <= 10 — так что ли?

Что касается дублирования информации в возвращаемом resultset, то чаще всего оно ничтожно мало по сравнению со временем чтения с диска
Я понимаю, что дублирование информации в возвращаемом resultset практически не влияет на производительность по сравнению с чтением с диска. Тут дело не в производительности. А в том, что это банально против логики (и гнаться за производительностью конкретно в этом случае мне кажется чем-то в стиле «premature optimization»).

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

Опять же таки: а что если мы для родительской сущности запрашиваем сразу несколько видов дочерних сущностей? Ну, например, если хотим такой результат в UI:
• bill #1 (первые 3 item'а этого bill'а) (первые 3 amendment'а bill'а)
• bill #2 (первые 3 item'а этого bill'а) (первые 3 amendment'а bill'а)
• …
• bill #10 (первые 3 item'а этого bill'а) (первые 3 amendment'а bill'а)
— неужели Вы будете JOIN-ить всё со всем, и потом выковыривать эти 10 bill'ов, размазанные по 90 записям?

Кроме того затягивать в приложение за раз больше 1000 строк — вообще странная затея.
А где Вы увидели больше 1000 строк? Во-первый, цифры LIMIT'ов были от балды (просто чтобы продемонстрировать возможную необходимость сортировки/лимитирования/прочая, а не что нам достаточно тупо достать все item'ы для каждого bill'а в любом порядке); во-вторых, даже при тех цифрах получалось максимум 15 bill'ов + 15*10 item'ов = 165 записей (а на самом деле логически всего 15, потому что цель запроса именно bill'ы, а остальное просто сопровождающая информация; в новом примере же максимум 10 bill'ов + 10*3 item'ов + 10*3 amendment'ов = 70 записей — а логически опять таки всего 10 основных экземпляров + несущественная сопровождающая информация для них).
WHERE row_number() OVER (PARTITION …) <= 10 — так что ли?

да


Что касается скорости. Я напишу для начала как проще (меньше писать), а потом буду смотреть планы. Гадать, не имея реальной схемы, реальных данных и реальных запросов, вообще бессмысленно.

На практике один "большой и страшный запрос от EF" имеет план лучше, чем два запроса с передачей данных вручную. Потому что большинство баз умеют строить планы исходя из повторного использования частей запросов (подзапросов и джоинов) и дают лучший результат, чем ручное последовательное скармливание запросов базе.

Что значит «с передачей данных вручную»?

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

Та дело не в скорости. Дело в логике. А если три-четыре-пять разных видов дочерних entity для каждого счёта нужно доставать, например, ожидается такая табличка в UI:
№ счёта | Первые 3 к-агента | П-вые 5 пунктов | П-е 4 правки | П-е 3 foo | П-е 3 bar
--------+-------------------+-----------------+--------------+-----------+----------
…       | …, …, …           | …, …, …, …, …   | …, …, …, …   | …, …, …   | …, …, …
--------+-------------------+-----------------+--------------+-----------+----------
…       | …, …              | …, …, …         |              |           | …, …
--------+-------------------+-----------------+--------------+-----------+----------
…       | …                 | …, …, …, …, …   | …            | …, …, …   | …, …, …
--------+-------------------+-----------------+--------------+-----------+----------
…       | …, …              | …, …, …         |              | …         | 
--------+-------------------+-----------------+--------------+-----------+----------
…       | …                 | …, …, …, …, …   | …            | …, …, …   | …, …, …
--------+-------------------+-----------------+--------------+-----------+----------
…       | …, …, …           | …, …, …, …, …   | …, …, …      | …, …, …   | …, …, …
--------+-------------------+-----------------+--------------+-----------+----------
…       | …, …              | …, …, …         |              |           | …, …
--------+-------------------+-----------------+--------------+-----------+----------
…       | …                 | …, …, …, …, …   | …            | …, …, …   | …, …, …
--------+-------------------+-----------------+--------------+-----------+----------
…       | …, …              | …, …, …         |              | …         | 
--------+-------------------+-----------------+--------------+-----------+----------
…       | …                 | …, …, …, …, …   | …            | …, …, …   | …, …, …

— да, вот такую вот детальную сводную таблицу по последним 10 счетам заказчик, допустим, захотел — Вы действительно будете скрещивать всё со всеми (не обращая внимания, что основая цель запроса — счета (нам нужно вернуть 10 счетов по определённому признаку), а всё остальное — только сопровождающая информация)?

Не пойму сути вопроса. Если надо отобразить результат джоина из 10 таблиц, то придется сделать джоин из 10 таблиц. Можно написать одним linq запросом, можно несколькими.

Надо отобразить не результат джоина 10 таблиц.

Надо отобразить несколько счетов по какому-то условию (например, последние 10 какого-то типа).

Но счета нужно отображать с подробностями: по каждому счёту мы отображаем:
  • первые несколько пунктов этого счёта (много не надо, много некрасиво, допустим, первые 5, а если их больше, то дальше троеточие);
  • список контактных лиц со стороны заказчика касательно этого счёта (обычно одно, иногда 2, если больше — троеточие);
  • историю правок этого счёта (опять таки не всю — первые 3 правки, а если их больше, то после них троеточие);
  • первые M сепулек по этому счёту;
  • первые N бубячек по этому счёту.


В SQL способа сделать это красиво одним запросом, насколько я знаю, не подвезли.
Очевиднейшим решением было бы что-то типа:
SELECT
  b.*,
  (SELECT * FROM bill_items WHERE bill_id=b.id LIMIT 5) as items,
  (SELECT * FROM bill_contacts WHERE bill_id=b.id LIMIT 2) as contacts,
  (SELECT * FROM bill_amendments WHERE bill_id=b.id LIMIT 3) as amendments,
  (SELECT * FROM bill_foos WHERE bill_id=b.id LIMIT m) as foos,
  (SELECT * FROM bill_bars WHERE bill_id=b.id LIMIT n) as bars
FROM bills b
WHERE …;

Но даже тип данных ROW поддерживается (и может возвращаться) не всеми RDBMS (и не всеми клиентскими либами может приниматься), не говоря уже о типе данных ROW[] (a.k.a. ROWS a.k.a. TABLE), поэтому items, contacts, amendments, foos, bars запихать некуда.

Поэтому, по-моему, правильно (на данном этапе развития SQL) сделать это несколькими запросами (минимум 6-ю: 1-й — счета, 2-й — пункты (сразу для всех нужных счетов, по 5 для каждого), 3-й — контактные лица (сразу для всех нужных счетов, по 2 для каждого), 4-й — правки (сразу для всех нужных счетов, по 3 для каждого), 5-й — сепульки (аналогично), 6-й — бубячки (аналогично)).

А вот что Вы предлагаете сделать в такой ситуации, я так и не понял. Когда дочерняя сущность была одна (пункты, без контактов/правок/сепулек/пупячек), Вы предлагали джоинить¹ — я потому и привёл данный пример, чтобы показать, что джоинить в данном случае не вариант (причём даже не с точки зрения производительности, а чисто логически). Что Вы предлагаете теперь?

¹ Джоинить — нормальный вариант, когда связь многие-к-одному, а не один-ко-многим.

EF в такой ситуации использует (LEFT) JOIN + UNION ALL


(Что вполне нормально работает пока оптимизатор запросов на стороне СУБД укладывается в свои ограничения по времени)

LEFT JOIN кого с кем? — bill'ов с наборами детей каждого типа (в смысле отдельно bills LEFT JOIN bill_items, отдельно bills LEFT JOIN bill_contacts, отдельно bills LEFT JOIN bill_amendments и т.д.)?

А как тогда потом UNION'ить, если даже количество столбцов у разных типов детей может отличаться (не говоря уже об их типах и семантике)? — добить NULL'ам до ширины самого широкого ребёнка?

По-моему, это изврат.
И Вы уверены, что это будет действовать быстрее, чем сначала выбрать инфу по собственно bill'ам (и тем таблицам, с которыми они ассоциируются многие-к-одному), получив при этом набор id'шников — а потом по этому набору id'шников спокойно повыбирать детей (один-ко-многим) разных типов отдельными запросами? Ну, ок, ладно, пусть по-Вашему быстрее, но… даже не знаю… я б так не делал, во-вторых, какая-то низлежащая RDBMS может на излишне сложном запросе глюкануть, а во-первых, это просто извращение сути реляционной модели, по-моему.

Нет, у каждого дочернего элемента свой набор полей, чужие — NULLы.


Про глюки RDBMS на сложных запросах согласен, поэтому от таких запросов при оптимизации по возможности избавляюсь.

Нет, у каждого дочернего элемента свой набор полей, чужие — NULLы.
А. Уже лучше. Я как-то не подумал.

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

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

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

Поэтому, по-моему, правильно

Это на основании чего правильно?


А вот что Вы предлагаете сделать в такой ситуации, я так и не понял

Предлагаю сделать банально как удобнее. Мне удобнее например один запрос родить. А дальше уже смотреть планы и оптимизировать если надо.

См. #comment_10360412 — до данного момента я просто банально не мог додуматься, как красиво сделать один запрос. (Извиняюсь за зря потраченное время.)

Не ну хейт это конечно хорошо, а где конструктив, какие предложения? Callback-hell? Нет, спасибо.


Использовать надо то, что хорошо работает. Но не везде его сунуть, тут абсолютизация ни к чему. Раз RxJava хороша для чего-то, мы её используем. А там где не подходит, обойдемся без неё.


По-прежнему не понимаю посыл этих огромных комментариев от вас....

Вот не верю что после onDestroy в activity могут вообще прилетать хоть какие-то события. Такое ощущение, что у вас проблемы не с RxJava, а с избыточным использованием статических полей.

Реальный пример из жизни — крутой проект полностью на Rx. Дали оценить. Тыкаю пальцем и говорю — вот здесь может происходить смена логики — что делаете. Ответ — подменяем последовательность. Но этого же не видно в коде. Да не видно. И кто кроме разраба это знает? Сколько раз писались заново целые огромные куски кода — после того как уходил разработчик или забывал сам что и где. Как сопровождать такую систему. Да rxjava — мощная система — но лучше ее использовать для прямых по логике вычислений в условиях без смены внешнего окружения.
Спасибо за статью.
Подскажите, пожалуйста, мне не понятна ситуация с направлением зависимости от Repository (более внешний круг) к Use case (более внутренний круг). Я предполагал, что Use case делает вызовы к Repository, но если это так, то Use case должен знать интерфейс Repository тем самым зависимость идет в обратную сторону? Я что-то неверно понял?

Прочтите про dependency inversion. И раздел статьи про переходы между слоями.

Я не уверен, что LiaminRoman получил ответ на вопрос. Точно так же привык, что UseCase тянет данные из Repository (или нескольких), добавляет нужную логику и передает выше. Но тогда направление на диаграмме было бы другим. Как конкретно Вы это реализуете, чтобы Repository был слоем выше, чем UseCase и главное зачем это делать, если в статье четко написано, то UseCase ближе к Presenter'ам? Почему просто не построить цепочку Presenter --> UseCase --> Repository?

Не совсем понял вопрос. Если вы о направлении зависимостей, то применяем принцип инверсии зависимостей. Т.е. в слое логики лежит интерфейс и Интерактор зависит от него. А репозиторий, находясь в другом слое реализует этот интерфейс, то есть зависит от него. Так зависимости направлены во внутрь от репозитория к слою UseCases.
Так это должно быть. А как на практике другой вопрос. На деле этот репозиторий может никогда не меняться и делать для него интерфейс может быть лишней затратой. То есть можно и не сделать его, но это будет компромисс с собой. Надо знать как правильно, ну а делать исходя из обстоятельств. Если вы уверены что подменять репозиторий целиком не придется, то в этом интерфейсе нет практической ценности. Как-то так.

Спасибо, а зачем это делается? Какую проблему мы пытаемся решить, когда инвертируем зависимость между Interactor и Repository? Ведь направление запроса данных обратное от того, что нарисовано. Вот еще один пример на ту же тему: https://engineering.21buttons.com/clean-architecture-in-django-d326a4ab86a9. Судя по имплементации, Interactor просто использует Repository, получает его в конструкторе. Но это не соответсвует тому, что нарисовано. По хорошему, он должен был инвертировать зависимость, как вы описали выше. Но… зачем?

Это же зависимость. А любая зависимость означает связь и изменения зависимого при изменениях того от кого зависят. Если Интерактор зависит напрямую от Репозитория, то при замене репозитория мы должны будем вносить изменения в Интерактор тоже.
Уменьшить такую связанность и дать больше гибкости для изменений — это и есть цель Dependency rule.
Кроме изменений, связность можно почувствовать подумав о работе командой. Представьте вы бы работали над логикой огромного Интерактора, а ваш коллега пока делал бы все репозитории. Вы бы зависели от того сделал ли он уже нужный репозиторий или нет. И что там сделал. А с интерфейсом, вы бы работали над своей логикой спокойно используя интерфейс. А когда там и кто реализует его вас не волновало бы.

Вы привели общее описание Loose coupling. С таким же успехом можно было инвертировать вообще все зависимости на этой схеме, а чё б и нет, столько гибкости! Но почему-то между Presenter и Interactor этого нет, а между Interactor и Repository есть. Видимо преследуется какая-то неочевидная выгода. Опять же, судя по коду (пример выше), который часто приводят под Clean Architecture, сами же авторы не сильно заморачиваются с инверсией на этом участке.

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

Посмотрите на схему дяди боба, где нарисованы Boundaries, прочтите еще раз про передачу данных и потом снова скажите, что между Presenter и Interactor этого нет ;)
Заморачиваться или нет — выбор каждого.

Фактически, вся инверсия зависимости как раз и заключается в том, что репозиторий не создается внутри, а получается через конструктор — и именно это вы и наблюдали в вашем примере.


В Питоне динамическая утиная типизация, поэтому там нет никаких интерфейсов.

Гм, пожалуй нет. Просто constructor injection. В интеракторе поле репозитория, зависимость никуда не делась, инверсии нет.

В таком случае что такое, по-вашему, DI?

То, что объект больше не отвечает за создание другого объекта, а получает его в конструкторе еще не означает, что у вас инверсия зависимостей. Пока что это просто соблюдение SRP. DIP у вас будет, когда объекты из разных слоев будут общаться через промежуточные интерфейсы, а не напрямую. Тогда у вас пропадет жесткая связь между разными уровнями. Т.е. если Interactor явно дергает методы из Repository (пусть и внедренный через конструктор) — это не DIP, нет промежуточного интерфейса/контракта, который скроет Repository. Важно не путать эти связующие интерфейсы с личными интерфейсами, которые уже могут быть у этих объектов (какой-нибудь RepositoryInterface и тд.). Иллюстрация с вики должна лучше передать то, что я пытаюсь сказать: https://en.wikipedia.org/wiki/Dependency_inversion_principle

Мы все еще про Питон говорим? О каких интерфейсах идет речь?

Если в коде Interactor'a встречается слово "repository", то это не DIP. Я не знаю как еще объяснить. Интерфейсы, не интерфейсы уже на ваше усмотрение. С помощью Rx, например, оно и в Java без интерфейсов будет.

Интерфейс репозитория объявляется на, как правило, уровне бизнес-логики (слой entities), а реализуется на уровне инфраструтктуры (слой interface adapters). На уровень логики приложения (use cases) передаются конкретные интерфейсы как экземпляры абстрактного интерфейса, интеракторы оперируют только абстрактными методами, объявленными в слое бизнес-логики, хотя у конкретного репозитория могут быть и другие.

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

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

Хехе, я же даже жирным выделил "Dependency Rule говорит нам, что внутренние слои не должны зависеть от внешних". И весь раздел про Dependency Rule говорит об этом.


И дальше по тексту было, что dependency inversion principle используется чтобы этого добиться просто. ;)

Entities по праву занимают первое место по непониманию.

Я вот пытаюсь понять, но никак не понимаю.


Возможно, мой вопрос будет оффтопом, потому что я пишу вам из мира веб-разработки, но принципиально ничего не меняется, как мне кажется (Onion Architecture), а entity это вообще из DDD.


В целом концепция понятна, как и её плюсы.
Самое непонятное — как подружить Entity с каким-либо источником данных, например (чаще всего) ряляционной БД. Т.к. и в мобильной разработке есть базы данных, то надеюсь, этот комментарий будет уместен.


Допустим (кривой, очень пример), что у нас есть entity ShoppingCart, в которой печеньки. И у него метод MakeOrder(), который проверяет, что если в корзине две печеньки, то необходимо сделать скидку 20%. На первый взгляд, все просто


...
public Order MakeOrder()
{

   Order order = new Order(items);

   // На самом деле тут может быть много разных скидок и if не лучшее решение
  if (items.Count == 2)
  {
      // Предположим, что класс Order хранит RawPrice, Discount и сам вычисляет цену со скидкой
      order.Discount = 0.2;
  }
}

Да, но теперь вспоминаем, что items (и другие необходимые поля) хранятся в БД и прежде чем выполнять эту операцию необходимо всё загрузить. Что же делать?


Обычно, я реализую подобную логику запросами к БД (при помощи ORM) (может быть и другой источник данных).


int itemsCount = itemsRepository.Count(i => i.CardId == myCartId);

Order order = new Order();
order.RawPrice =itemsRepository.Where(i => i.CardId == myCardId).Sum(i => i.Product.Price); 

if(itemsCount == 2)
    order.Discount == 0.2;

// Еще записать список item'ов

ordersRepository.Insert(order);

// Коммит транзакции

Такой подход более понятен с точки зрения БД и реально происходящих действий. Недавно, в одной статье прочитал следующее:


Агрегат обычно загружается из базы данных целиком.

Т.е. получается вся сущность загружается, возможно, вместе с коллекциями, чтобы потом работать с ней OOP-style? Звучит, как много лишней нагрузки на БД. Действительно, такой подход часто применяется и преимущества перевешивают недостатки? (Я понимаю, что нет универсального решения).


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

Всё просто.
Если вашей сущности не хватает данных для проверки бизнес ограничения вы можете:
А) передать ей эти данные в аргументе функции
Б) вынести проверку бизнес ограничения в сервис доменного слоя


У Вернона есть упоминание о сервисах доменного слоя. Рекомендую почитать.

Тогда проще сразу все всю логику поместить в "сервис доменного слоя" (DomainService), потому что
1) Лучше иметь логику в однотипных классах, чем в разноипных
2) Нет проблемы перемещать потом код из Entity в DomainService если вдруг появятся параметры.
3) Вариант с передачей параметра в метод Entity нежизнеспособен, потому что нужен код, который эти "параметры" достанет из баз и это будет DomainService

Вариант с передачей параметра в метод Entity нежизнеспособен, потому что нужен код, который эти "параметры" достанет из баз и это будет DomainService

Как мне кажется, тут дело не в том, кто что откуда достаёт, а кто решает, что с этим делать. Т.е. сущность получает данные и именно она знает, как с ними работать.

Именно. Сущности можно передавать сервис доменного слоя, а бизнес логику всё равно лучше выполнять в сущности.


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

Самое непонятное — как подружить Entity с каким-либо источником данных, например (чаще всего) ряляционной БД.

Взять адекватную реализацию ORM.

Entity Framework — достаточно адекватная? Я описал, какие затруднения вызывает подход. Сущность (точнее, Aggregation Root) для выполнения бизнес-правил должна быть загружена (и это легко сделать с помощью того же EF), но это overhead — зачем загружать всю сущность?


Код с непосредственными LINQ запросами получается, как мне кажется, более эффективным.

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

А это вообще законно — иметь в памяти неполностью загруженный объект?


PS Если очень хочется грузить сущность по частям — можно разные части вынести в отдельные сущности с отношением 1 к 1.

А можно просто задавать нужную проекцию (select, ага).

Ну обычно я так и делаю — в сервисе select, select, select...

Действительно, такой подход часто применяется и преимущества перевешивают недостатки?

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


Т.к. нередко операция затрагивает несколько сущностей, есть несколько вариантов по размещению реализации.

Если сущности являются частями одного агрегата, то хорошее место — ближайший общий "подкорень". Если слабо связаны между собой и сложно определить к какой из сущностей относится, то часто лучшее место отдельный доменный сервис. Ваш пример выглядит таким. Вариант — создание ордера на основе корзины ответственность фабричного метода класса ордера или фабрики ордеров. Дабы сильно не связывать корзину и ордер, собственно проверку можно делать используя паттерн спецификация как-то типа (псевдокод)


class Order {
  public static makeFromCart(ShoppingCart cart) {
    var order = new self();
    var spec = new ItemsCountCartSpecification(2);
    if (spec.satisfiedBy(cart)) {
      order.discount = 0.2;
    }
    return order;
  }
}

Я видел уже эту схему, но она мне показалась странной. Я ее не понял и прошел мимо.
Теперь я понял эту архитектуру.
Спасибо вам за это.
Правда мне всё равно она кажется немного странной.
Мне больше по душе классическая архитектуру DDD приложений + CQRS.

На мой взгляд Clean Architecture не очень подходит для больших и долгоиграющих проектов со сложной бизнес логики (читай DDD).


Классическая архитектура DDD приложений имеет слои:


  • Presentation layer
  • Application layer
  • Domain layer
  • Infrastructure layer
  • Persistence layer

И вот вся это катавасия из Clean Architecture:


  • Controller
  • Presenter
  • Reques modal
  • Rrsponse model
  • Interactor

Очень похожа на CQRS и находится на одном уровне — Application. Мы просто разделяем потоки и то что могло выполнятся в контроллере выполняется в Interactor или Command Heandlers в CQRS подходе.


А все самое интересное из DDD оказалось спрятано за неоднозначным понятием Entities.


Application layer оказался эквивалентен Infrastructure layer, а Presentation layer эквивалентен Persistence layer.


В общем, очень странная архитектура на мой взгляд. Хотя многие могут не согласиться со мной.

Вопрос в том, должен ли зеленый слой (Репозитории) знать о желтом (Энтити). Мне кажется, Дядька Боб переборщил с использованием термина «Entity» на своей flow-диаграмме, из-за чего у вас образовалась стрелка от Repositories к Entity на последнем рисунке в разделе «Заблуждение: Слои и линейность». На мой взгляд, как раз-таки интерактор (розовый) должен лезть через гейтвэй (зеленый) в БД, получать от него DTO и с помощью них инициализировать энтити (розовый создает желтый), выполняя в них бизнес-логику в нужном порядке и реализуя таким образом юз кейс. Тогда мы получим ровно такой же флоу, как на кольцевой диаграмме, где каждый внешний слой именно что отделяет внешнего соседа от внутреннего. В более простых приложениях, в которых вообще нет никакой предметной области, слой Энтити просто будет отсутствовать как факт.
А как может быть приложение без предметной области?

Наверное речь о модели предметной области.

Любой CRUD как простейший пример. Вообще, что считать предметной областью — вопрос интересный. По сути это бизнес-правила из реального мира, которые, что логично, не зависят от конкретного приложения. Расчет налогов, например. Какое приложение не пиши, но бизнес-компоненты, реализующие подсчет условного НДФЛ, будут применять одну и ту же логику при калькуляции (поэтому Мартин и говорит, что Entities — это объекты, которые можно таскать за собой из приложения в приложение, если, конечно, их предметные области пересекаются). Если вся бизнес-логика — это специфичные для приложения воркфлоу и сущности, то вряд ли можно говорить о наличии предметной области и соответствующего (желтого) слоя.
что считать предметной областью — вопрос интересный
Вообще, да, согласен, что разделение между entities и use-cases (между бизнес-логикой и логикой приложения) достаточно условное.
Таким образом согласен и с Вашим предыдущим комментарием.

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

Репозиторий, как впрочем и гейтвэй — это фасад/прокси к источнику данных. Его задача извлечь данные и отдать их запрашивающему. Создавать объекты предметной области — не его задача. Это не считая того, что бизнес-объект может инициироваться аккумулированными данными из запросов к разным репозиториям/гейтвэям.
Да вряд ли бы не согласился. «Conceptually, a Repository encapsulates the set of objects persisted in a data store and the operations performed over them, providing a more object-oriented view of the persistence layer». В заблуждение тут может ввести термин «domain object». С доменами вообще нынче дикая путаница. Data Mapper маппит результаты запроса к источнику данных (например, БД) в объекты-сущности. Сущности эти относятся к зеленому слою (слою доступа к данным, DAL). Репозиторий хранит коллекцию этих сущностей и предоставляет упрощенные методы работы с ней (т.е. он фасад). На выход репозиторий выдает те же самые объекты «зеленых» сущностей. Интерактор (розовый слой) берет эти сущности и с помощью содержащихся в них данных инициирует объекты предметной области (желтый слой), которые, в отличии от сущностей, содержат методы обработки этих данных (т.е. правила предметной области).

Если предметной области нет, то розовый слой вполне себе может взаимодействовать сразу с «зелеными» сущностями, просто прогоняя полученные из них данные через какой-то рабочий процесс и выплевывая их наверх презентатору. Собственно, такой подход и показан на диаграмме в книге Фаулера.
Пардон, сам уже с цветами запутался и вас запутал.

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

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

В примере из книги Фаулера в итоге ничего не нарушается, если принять во внимание, что «client» — это контроллер, а получает он от репозитория те самые зеленые сущности.

Вообще-то, по Эванса, сущность это слой бизнес лигики (domain), а контроллеры это слой приложения (application).
Репозитории это тоже слой бизнес логики, а реализация репозиторий под конкретное хранилище это вообще слой инфраструктуры которого нет в Clean Architecture

UFO just landed and posted this here

Доброе утро! У меня возникла пара вопросов:


  1. Может ли интерактор иметь состояние? Например, кол-во уже загруженных элементов списка для пагинации. Но, если он может иметь состояние, то один интерактор может использоваться только с одним презентером...
  2. Вся ли бизнес-логика должна содержаться в интеракторе? К примеру, на экране авторизации кнопка "Войти" должна быть enabled, только если в loginEditText введено больше 4 символов. Нужен ли в этом случае метод boolean canLogin(String login) в интеракторе?

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


Поэтому проще просто не делать так.

Спасибо за статью очень интересно было почитать. Все разложено по полочкам.
Так же спасибо за ссылки на оригинальные статьи.
Было бы здорово увидеть:
1) Ваше описание clean architecture
2) Код приложения по данной архитектуре.

Ещё одна из путаниц которую я встречал - Services.

Насколько я понимаю, понятие сервисов приходит из спецификации DDD, но в канонических описаниях/выступлениях от Uncle Bob встречаются только Interactor и Use Case.

Просто разные уровни. Usecase и Interactor выше по уровню чем Service.

Sign up to leave a comment.