Заблуждения Clean Architecture

    Превращаем круги в блоки

    ­­ 


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


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


    Истоки


    В 2011 году Robert C. Martin, также известный как Uncle Bob, опубликовал статью Screaming Architecture, в которой говорится, что архитектура должна «кричать» о самом приложении, а не о том, какие фреймворки в нем используются. Позже вышла статья, в которой Uncle Bob даёт отпор высказывающимся против идей чистой архитектуры. А в 2012 году он опубликовал статью «The Clean Architecture», которая и является основным описанием этого подхода. Кроме этих статей я также очень рекомендую посмотреть видео выступления Дяди Боба.


    Вот оригинальная схема из статьи, которая первой всплывает в голове разработчика, когда речь заходит о Clean Architecture:


    Оригинальная схема


    В Android-сообществе Clean стала быстро набирать популярность после статьи Architecting Android...The clean way?, написанной Fernando Cejas. Я впервые узнал про Clean Architecture именно из неё. И только потом пошёл искать оригинал. В этой статье Fernando приводит такую схему слоёв:


    Схема от Fernando Cejas


    То, что на этой схеме другие слои, а в domain слое лежат ещё какие-то Interactors и Boundaries, сбивает с толку. Оригинальная картинка тоже не всем понятна. В статьях многое неоднозначно или слегка абстрактно. А видео не все смотрят (обычно из-за недостаточного знания английского). И вот, из-за недопонимания, люди начинают что-то выдумывать, усложнять, заблуждаться…


    Давайте разбираться!


    Сlean Architecture


    Clean Architecture объединила в себе идеи нескольких других архитектурных подходов, которые сходятся в том, что архитектура должна:


    • быть тестируемой;
    • не зависеть от UI;
    • не зависеть от БД, внешних фреймворков и библиотек.

    Это достигается разделением на слои и следованием Dependency Rule (правилу зависимостей).


    Dependency Rule


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


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


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


    Слои


    Uncle Bob выделяет 4 слоя:


    • Entities. Бизнес-логика общая для многих приложений.
    • Use Cases (Interactors). Логика приложения.
    • Interface Adapters. Адаптеры между Use Cases и внешним миром. Сюда попадают Presenter’ы из MVP, а также Gateways (более популярное название репозитории).
    • Frameworks. Самый внешний слой, тут лежит все остальное: UI, база данных, http-клиент, и т.п.

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


    Переходы


    Переходы между слоями осуществляются через Boundaries, то есть через два интерфейса: один для запроса и один для ответа. Их можно увидеть справа на оригинальной схеме (Input/OutputPort). Они нужны, чтобы внутренний слой не зависел от внешнего (следуя Dependency Rule), но при этом мог передать ему данные.


    Поток данных на оригинальной схеме


    Оба интерфейса относятся к внутреннему слою (обратите внимание на их цвет на картинке).


    Смотрите, Controller вызывает метод у InputPort, его реализует UseCase, а затем UseCase отдает ответ интерфейсу OutputPort, который реализует Presenter. То есть данные пересекли границу между слоями, но при этом все зависимости указывают внутрь на слой UseCase’ов.


    Чтобы зависимость была направлена в сторону обратную потоку данных, применяется принцип инверсии зависимостей (буква D из аббревиатуры SOLID). То есть, вместо того чтобы UseCase напрямую зависел от Presenter’a (что нарушало бы Dependency Rule), он зависит от интерфейса в своём слое, а Presenter должен этот интерфейс реализовать.


    Точно та же схема работает и в других местах, например, при обращении UseCase к Gateway/Repository. Чтобы не зависеть от репозитория, выделяется интерфейс и кладется в слой UseCases.


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


    Особенности мобильных приложений


    Надо отметить, что Clean Architecture была придумана с немного иным типом приложений на уме. Большие серверные приложения для крупного бизнеса, а не мобильные клиент-серверные приложения средней сложности, которые не нуждаются в дальнейшем развитии (конечно, бывают разные приложения, но согласитесь, в большей массе они именно такие). Непонимание этого может привести к overengineering’у.


    На оригинальной схеме есть слово Controllers. Оно появилось на схеме из-за frontend’a, в частности из Ruby On Rails. Там зачастую разделяют Controller, который обрабатывает запрос и отдает результат, и Presenter, который выводит этот результат на View. Многие не сразу догадываются, но в android-приложениях Controllers не нужны.


    Ещё в статье Uncle Bob говорит, что слоёв не обязательно должно быть 4. Может быть любое количество, но Dependency Rule должен всегда применяться.


    Глядя на схему из статьи Fernando Cejas, можно подумать, что автор воспользовался как раз этой возможностью и уменьшил количество слоев до трёх. Но это не так. Если разобраться, то в Domain Layer у него находятся как Interactors (это другое название UseCase’ов), так и Entities.


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


    Заблуждение: Слои и линейность


    Сравнивая оригинальную схему от Uncle Bob’a и cхему Fernando Cejas’a многие начинают путаться. Линейная схема воспринимается проще, и люди начинают неверно понимать оригинальную. А не понимая оригинальную, начинают неверно толковать и линейную. Кто-то думает, что расположение надписей в кругах имеет сакральное значение, или что надо использовать Controller, или пытаются соотнести названия слоёв на двух схемах. Смешно и грустно, но основные схемы стали основными источниками заблуждения!


    Постараемся это исправить. Для начала давайте очистим основную схему, убрав из нее лишнее для нас. И переименуем Gateways в Repositories, т.к. это более распространенное название этой сущности.


    Упрощенная оригинальная схема


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


    Превращаем круги в блоки


    Как я уже сказал выше, цвета обозначают слои. А стрелка внизу обозначает Dependency Rule.


    На получившейся схеме уже проще представить себе течение данных от UI к БД или серверу и обратно. Но давайте сделаем еще один шаг к линейности, расположив слои по категориям:


    Слои по категориям

    Я намеренно не называю это разделение слоями, в отличие от Fernando Cejas. Потому что мы и так делим слои. Я называю это категориями или частями. Можно назвать как угодно, но повторно использовать слово «слои» не стоит.


    А теперь давайте сравним то, что получилось, со схемой Fernando.


    Сравнение со схемой Fernando Cejas

    Надеюсь теперь вcё начало вставать на свои места. Выше я говорил, что, по моему мнению, у Fernando всё же 4 слоя. Думаю теперь это тоже стало понятнее. В Domain части у нас находятся и UseCases и Entities.


    Такая схема воспринимается проще. Ведь обычно события и данные в наших приложениях ходят от UI к backend’у или базе данных и обратно. Давайте изобразим этот процесс:


    Поток данных от UI и обратно

    Красными стрелками показано течение данных.


    Событие пользователя идет в Presenter, тот передает в Use Case. Use Case делает запрос в Repository. Repository получает данные где-то, создает Entity, передает его в UseCase. Так Use Case получает все нужные ему Entity. Затем, применив их и свою логику, получает результат, который передает обратно в Presenter. А тот, в свою очередь, отображает результат в UI.


    На переходах между слоями (не категориями, а слоями, отмеченными разным цветом) используются Boundaries, описанные ранее.


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


    Заблуждение: Слои, а не сущности


    Как понятно из заголовка, кто-то думает, что на схемах изображены сущности (особенно это затрагивает UseCases и Entities). Но это не так.


    На схемах изображены слои, в них может находиться много сущностей. В них будут находиться интерфейсы для переходов между слоями (Boundaries), различные DTO, основные классы слоя (Interactors для слоя UseCases, например).


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


    Схема классов из выступления Uncle Bob


    Видите двойные линии? Это границы между слоями. Разделение между слоями Entities и UseCases не показаны, так как в видео основной упор делался на том, что вся логика (приложения и бизнеса) отгорожена от внешнего мира.


    C Boundaries мы уже знакомы, интерфейс Gateway – это то же самое. Request/ResponseModel – просто DTO для передачи данных между слоями. По правилу зависимости они должны лежать во внутреннем слое, что мы и видим на картинке.


    Про Controller мы тоже уже говорили, он нас не интересует. Его функцию у нас выполняет Presenter.


    А ViewModel на картинке – это не ViewModel из MVVM и не ViewModel из Architecture Components. Это просто DTO для передачи данных View, чтобы View была тупой и просто сетила свои поля. Но это уже детали реализации и будет зависеть от выбора презентационного паттерна и личных подходов.


    В слое UseCases находятся не только Interactor’ы, но также и Boundaries для работы с презентером, интерфейс для работы с репозиторием, DTO для запроса и ответа. Отсюда можно сделать вывод, что на оригинальной схеме отражены всё же слои.


    Заблуждение: Entities


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


    Мало того, что почти никто (включая меня до недавнего времени) не осознает, что же это такое на самом деле, так их ещё и путают с DTO.


    Однажды в чате у меня возник спор, в котором мой оппонент доказывал мне, что Entity – это объекты, полученные после парсинга JSON в data-слое, а DTO – объекты, которыми оперируют Interactor’ы…

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


    Что же такое Entities?


    Чаще всего они воспринимаются как POJO-классы, с которыми работают Interactor’ы. Но это не так. По крайней мере не совсем.


    В статье Uncle Bob говорит, что Entities инкапсулируют логику бизнеса, то есть всё то, что не зависит от конкретного приложения, а будет общим для многих. Но если у вас отдельное приложение и оно не заточено под какой-то существующий бизнес, то Entities будут являться бизнес-объектами приложения, содержащими самые общие и высокоуровневые правила.


    Я думаю, что именно фраза: «Entities это бизнес объекты», – запутывает больше всего. Кроме того, на приведенной выше схеме из видео Interactor получает Entity из Gateway. Это также подкрепляет ощущение, что это просто POJO объекты.


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


    Это также подтверждается в разъяснении от Uncle Bob’а, которое я нашел недавно:
    Uncle Bob говорит, что для него Entities содержат бизнес-правила, независимые от приложения. И они не просто объекты с данными. Entities могут содержать ссылки на объекты с данными, но основное их назначение в том, чтобы реализовать методы бизнес-логики, которые могут использоваться в различных приложениях.


    А по-поводу того, что Gateways возвращают Entities на картинке, он поясняет следующее:
    Реализация Gаteway получает данные из БД, и использует их, чтобы создать структуры данных, которые будут переданы в Entities, которые Gateway вернет. Реализовано это может быть композицией


    class MyEntity { private MyDataStructure data;}

    или наследованием


    class MyEntity extends MyDataStructure {...} 

    И в конце ответа фраза, которая меня очень порадовала:


    And remember, we are all pirates by nature; and the rules I'm talking about here are really more like guidelines…
    (И запомните: мы все пираты по натуре, и правила, о которых я говорю тут, на самом деле, скорее рекомендации…)

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


    Итак, слой Entities содержит:


    • Entities – функции или объекты с методами, которые реализуют логику бизнеса, общую для многих приложений (а если бизнеса нет, то самую высокоуровневую логику приложения);
    • DTO, необходимые для работы и перехода между слоями.

    Кроме того, когда приложение отдельное, то надо стараться находить и выделять в Entities высокоуровневую логику из слоя UseCases, где зачастую она оседает по ошибке.


    Заблуждение: UseCase и/или Interactor


    Многие путаются в понятиях UseCase и Interactor. Я слышал фразы типа: «Канонического определения Interactor нет». Или вопросы типа: «Мне делать это в Interactor’e или вынести в UseCase?».


    Косвенное определение Interactor’a встречается в статье, которую я уже упоминал в самом начале. Оно звучит так:


    «...interactor object that implements the use case by invoking business objects.»


    Таким образом:


    Interactor – объект, который реализует use case (сценарий использования), используя бизнес-объекты (Entities).


    Что же такое Use Case или сценарий использования?
    Uncle Bob в видео выступлении говорит о книге «Object-Oriented Software Engineering: A Use Case Driven Approach», которую написал Ivar Jacobson в 1992 году, и о том, как тот описывает Use Case.


    Use case – это детализация, описание действия, которое может совершить пользователь системы.


    Вот пример, который приводится в видео:


    Пример Use Case для создания заказа

    Это Use Case для создания заказа, причём выполняемый клерком.


    Сперва перечислены входные данные, но не даётся никаких уточнений, что они из себя представляют. Тут это не важно.


    Первый пункт – даже не часть Use Case’a, это его старт – клерк запускает команду для создания заказа с нужными данными.


    Далее шаги:


    • Система валидирует данные. Не оговаривается как.
    • Система создает заказ и id заказа. Подразумевается использование БД, но это не важно пока, не уточняется. Как-то создает и всё.
    • Система доставляет id заказа клерку. Не уточняется как.
      Легко представить, что id возвращается не клерку, а, например, выводится на страницу сайта. То есть Use Case никак не зависит от деталей реализации.

    Ivar Jacobson предложил реализовать этот Use Case в объекте, который назвал ControlObject.
    Но Uncle Bob решил, что это плохая идея, так как путается с Controller из MVC и стал называть такой объект Interactor. И он говорит, что мог бы назвать его UseCase.
    Это можно посмотреть примерно в этом моменте видео.


    Там же он говорит, что Interactor реализует use case и имеет метод для запуска execute() и получается, что это паттерн Команда. Интересно.


    Вернемся к нашим заблуждениям.


    Когда кто-то говорит, что у Interactor’a нет четкого определения – он не прав. Определение есть и оно вполне четкое. Выше я привел несколько источников.


    Многим нравится объединять Interactor’ы в один общий с набором методов, реализующих use case’ы.
    Если вам сильно не нравятся отдельные классы, можете так делать, это ваше решение. Я лично за отдельные Interactor’ы, так как это даёт больше гибкости.


    А вот давать определение: «Интерактор – это набор UseCase’ов», – вот это уже плохо. А такое определение бытует. Оно ошибочно с точки зрения оригинального толкования термина и вводит начинающих в большие заблуждения, когда в коде получается одновременно есть и UseCase классы и Interactor классы, хотя всё это одно и то же.


    Я призываю не вводить друг друга в заблуждения и использовать названия Interactor и UseCase, не меняя их изначальный смысл: Interactor/UseCase – объект, реализующий use case (сценарий использования).


    За примером того, чем плохо, когда одно название толкуется по-разному, далеко ходить не надо, такой пример рядом – паттерн Repository.


    Доступ к данным


    Для доступа к данным удобно использовать какой-либо паттерн, позволяющий скрыть процесс их получения. Uncle Bob в своей схеме использует Gateway, но сейчас куда сильнее распространен Repository.


    Repository


    А что из себя представляет паттерн Repository? Вот тут и возникает проблема, потому что оригинальное определение и то, как мы понимаем репозиторий сейчас (и как его описывает Fernando Cejas в своей статье), фундаментально различаются.


    В оригинале Repository инкапсулирует набор сохраненных объектов в более объектно-ориентированном виде. В нем собран код, создающий запросы, который помогает минимизировать дублирование запросов.


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


    Подробнее об этом можно прочесть в статье Hannes Dorfmann’а.


    Gateway


    Сначала я тоже начал использовать Repository, но воспринимая слово «репозиторий» в значении хранилища, мне не нравилось наличие там методов для работы с сервером типа login() (да, работа с сервером тоже идет через Repository, ведь в конце концов для приложения сервер – это та же база данных, только расположенная удаленно).


    Я начал искать альтернативное название и узнал, что многие используют Gateway – слово более подходящее, на мой вкус. А сам паттерн Gateway по сути представляет собой разновидность фасада, где мы прячем сложное API за простыми методами. Он в оригинале тоже не предусматривает выбор источников данных, но все же ближе к тому, как используем мы.


    А в обсуждениях все равно приходится использовать слово «репозиторий», всем так проще.


    Доступ к Repository/Gateway только через Interactor?


    Многие настаивают, что это единственный правильный способ. И они правы!
    В идеале использовать Repository нужно только через Interactor.


    Но я не вижу ничего страшного, чтобы в простых случаях, когда не нужно никакой логики обработки данных, вызывать Repository из Presenter’a, минуя Interactor.


    Repository и презентер находятся на одном слое, Dependency Rule не запрещает нам использовать Repository напрямую. Единственное но – возможное добавления логики в Interactor в будущем. Но добавить Interactor, когда понадобится, не сложно, а иметь множество proxy-interactor’ов, просто прокидывающих вызов в репозиторий, не всегда хочется.


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


    Заблуждение: Обязательность маппинга между слоями


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


    А можно использовать DTO из слоя Entities везде во внешних слоях. Конечно, если те могут его использовать. Нарушения Dependency Rule тут нет.


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


    Маппинг DTO на каждом слое:


    • Изменение данных в одном слое не затрагивает другой слой;
    • Аннотации, нужные для какой-то библиотеки не попадут в другие слои;
    • Может быть много дублирования;
    • При изменении данных все равно приходится менять маппер.

    Использование DTO из слоя Enitities:


    • Нет дублирования кода;
    • Меньше работы;
    • Присутствие аннотаций, нужных для внешних библиотек на внутреннем слое;
    • При изменении этого DTO, возможно придется менять код в других слоях.

    Хорошее рассуждение есть вот по этой ссылке.


    С выводами автора ответа я полностью согласен:


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


    Заблуждение: маппинг в Interactor’e


    Да, такое заблуждение существует. Развеять его несложно, приведя фразу из оригинальной cтатьи:
    So when we pass data across a boundary, it is always in the form that is most convenient for the inner circle.
    (Когда мы передаем данные между слоями, они всегда в форме более удобной для внутреннего слоя)


    Поэтому в Interactor данные должны попадать уже в нужном ему виде.
    Маппинг происходит в слое Interface Adapters, то есть в Presenter и Repository.


    А где раскладывать объекты?


    С сервера нам приходят данные в разном виде. И иногда API навязывает нам странные вещи. Например, в ответ на login() может прийти объект Profile и объект OrderState. И, конечно же, мы хотим сохранить эти объекты в разных Repository.


    Так где же нам разобрать LoginResponse и разложить Profile и OrderState по нужным репозиториям, в Interactor’e или в Repository?


    Многие делают это в Interactor’e. Так проще, т.к. не надо иметь зависимости между репозиториями и разрывать иногда возникающую кроссылочность.


    Но я делаю это в Repository. По двум причинам:


    • Если мы делаем это в Interactor’e, значит мы должны передать ему LoginResponse в каком-то виде. Но тогда, чтобы не нарушать Dependency Rule, LoginResponse должен находиться в слое Interactor’a (UseCases) или Entities. А ему там не место, ведь он им кроме как для раскладывания ни для чего больше не нужен.
    • Раскладывание данных – не дело для use case. Мы же не станем писать пункт в описании действия доступного пользователю: «Получить данные, разложить данные». Скорее мы напишем просто: «Получить нужные данные»,– и всё.

    Если вам удобно делать это в Interactor, то делайте, но считайте это компромиссом.


    Можно ли объединить Interactor и Repository?


    Некоторым нравится объединять Interactor и Repository. В основном это вызвано желанием избежать решения проблемы, описанной в пункте «Доступ к Repository/Gateway только через Interactor?».


    Но в оригинале Clean Architecture эти сущности не смешиваются.
    И на это пара веских причин:


    • Они на разных слоях.
    • Они выполняют различные функции.

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


    RxJava в Clean Architecture


    Уже становится сложным представить современное Android-приложение без RxJava. Поэтому не удивительно, что вторая в серии статья Fernando Cejas была про то, как он добавил RxJava в Clean Architecture.


    Я не стану пересказывать статью, но хочу отметить, что, наверное, главным плюсом является возможность избавиться от интерфейсов Boundaries (как способа обеспечить выполнение Dependency Rule) в пользу общих Observable и Subscriber.


    Правда есть люди, которых смущает присутствие RxJava во всех слоях, и даже в самых внутренних. Ведь это сторонняя библиотека, а убрать зависимость на всё внешнее – один из основных посылов Clean Architecture.


    Но можно сказать, что RxJava негласно уже стала частью языка. Да и в Java 9 уже добавили util.concurrent.Flow, реализацию спецификации Reactive Streams, которую реализует также и RxJava2. Так что не стоит нервничать из-за RxJava, пора принять ее как часть языка и наслаждаться.


    Заблуждение: Что лучше Clean Architecture или MVP?


    Смешно, да? А некоторые спрашивают такое в чатах.
    Быстро поясню:


    • Архитектура затрагивает всё ваше приложение. И Clean – не исключение.
    • А презентационные паттерны, например MVP, затрагивают лишь часть, отвечающую за отображение и взаимодействие с UI. Чтобы лучше понять эти паттерны, я рекомендую почитать статью моего коллеги dmdev.

    Заблуждение: Clean Architecture в первых проектах


    В последнее время архитектура приложений на слуху. Даже Google решили выпустить свои Architecture Components.


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


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


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


    Фффух!


    Статья получилась немаленькой. Надеюсь она будет многим полезна и поможет лучше разобраться в теме. Используйте Clean Architecture, следуйте правилам, но не забывайте, что «все мы пираты по натуре»!

    MobileUp 60,20
    Мобильная разработка повышенной сложности
    Поделиться публикацией
    Комментарии 192
    • +2

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

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

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

          • +2

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

            • 0

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

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

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

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

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

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

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

                        • 0

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

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

                        Как тут не вспомнить Эйнштейна: «Всё должно быть изложено так просто, как только возможно, но не проще».
                    • +1

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

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

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


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

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

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


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

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

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

                            • 0

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

                              • 0

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

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

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

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

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

                                    • 0

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


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

                                      • +1

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

                                        • +1

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

                                          • 0

                                            ответил ниже

                                            • 0

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

                                  • +2

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


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


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


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


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

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


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


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

                                      • 0

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

                                        • 0

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

                                          • 0

                                            Если нет бизнеса, и наше приложение само по себе, то скидка все равно может быть в 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.

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


                                            И опять же:


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

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

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

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

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

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

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

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

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


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

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

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

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

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

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

                                                • 0

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


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


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

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


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

                                                  • +2

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

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

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

                                                      • 0

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

                                                        • +2

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

                                                          • 0

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

                                                            • 0

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

                                                              • 0

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

                                                                • 0

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

                                                          • 0

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

                                                            • 0

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

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

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

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

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

                                                • 0

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


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

                                                  • +1

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


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

                                                    • 0

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

                                                      • 0

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

                                                • +1

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


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

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

                                                  • 0

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

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

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

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

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

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

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

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

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

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

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

                                                                      • +2
                                                                        Эмм, а зачем для объектных СУБД 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 ни к каким реляционным возможностям доступ не ограничивает. Объектное возможности — это строгое надмножество реляционных возможностей. Делая объектный адаптер к реляционному, ми теоретически ничего не теряем.
                                                                        • 0
                                                                          Хорошая ORM ни к каким реляционным возможностям доступ не ограничивает. Объектное возможности — это строгое надмножество реляционных возможностей. Делая объектный адаптер к реляционному, ми теоретически ничего не теряем.

                                                                          Чисто только теоретически. Это тоже самое, что и связывание таблиц в SQL запросах. На бумаге и в теории все просто. В реальности уже на join таблиц > 8 получаешь проседание, а при >16 оно может быть просто критическим.
                                                                          В реальности например всегда требуется просмотр всего инвойса. Соответственно ORM тянет все содержимое инвойса — целостной сущности. Меняем 1 строку и записываем — ORM по этой команде должна обновить инвойс — что делается почти всегда 2 операциями — удалить старую сущность, а затем добавить новую сущность. При этом вы всегда попадаете на удаление/вставку туевой хучи данных. Это как использовать 1с из коробки. Все прекрасно пока листьев мало. Т.е. все прекрасно — но только для конкретных условий. Разработчик не может мыслить реляционно — бога ради пускай использует ORM пока не нарвется (а в большинстве случаев — и нет) в провале производительности. Вопрос для примера — заказчик требует просмотра сразу отсортированных 10000 записей на экран смарта(ему так нравиться). Как это организовать в RecyclerView(помним что всего одна строчка кода — cursor.getCount() приводит к fetch всех 10000 строк в БД)? Но самый страшный вопрос — как организовать обновление этого списка при новомодном livedata(обновление UI при изменении данных). Помним, что при этом он может с легкостью выйти из просмотра списка(destroy fragment и все прелести после этого) и при этом он должен быть в рамках нашей архитектуры, когда UI отделен от выборки данных.
                                                                          • +2
                                                                            В реальности уже на 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).
                                                                            • 0
                                                                              Я максимально пытаюсь уйти от архитектуры андроид с ее неполноценными жизненными циклами. И максимально адаптировать архитектуру под активный легкий клиент — пассивный сервер. Но жизнь возвращается и все эти livedata, над которыми смеялись разрабы 20 лет назад — как будто и не было этих лет. И в данных условиях приходится отказываться от пассивных серверов, к каким-то другим решениям. И в настоящее время ищется такое решение. Которое видится в архитектуре ядро + подключаемые легкие модули (аналог Clipper 5, если кто знаком с его архитектурой). Админ только управляет загрузкой/связыванием модулей. Не путать c Dagger 2 — правильной будет архитектура ядро + подключаемые(именованные) процессы (c EventBus внутри каждого) — но она очень тяжеловесна — но позволяет все.
                                                                              • +1
                                                                                Честно говоря, мало понял, о чём Вы говорите. Не в смысле, что Вы в чём-то неправы (и поэтому не понял), а просто не понял из-за малой осведомлённости.
                                                                                • –1
                                                                                  В стародавние времена был такой язык Clipper (писала писала группа разрабов объединенных под крылом Nantucket). Они предложили революционную тогда технологию для приложения — микроядро(загружала/выгружала модули и делала сборку мусора). Идею портировали под Linix — но проект умер. По сравнению с Clipper Clean Architecture — просто дитя той архитектуры. Ты мог подключить динамически любые модули в любое время, разбить свое приложение как тебе нравиться — поддерживая интерфейс только модульности. Поэтому вопрос о Clean Architecture — это вопрос о частном разбиении на слои(модули), где каждый может разбивать свое приложение как хочет — и будет прав. Одно плохо, что в android начинают затаскивать кусочки всякой всячины. И приходится следовать моде. То узкие штаны, то широкие.
                                                                            • +1

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


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

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

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

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

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

                                                                                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();

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

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

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

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

                                                                                    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);
                                                                                    • –2
                                                                                      Ну значит Entity Framework, с моей точки зрения — ещё один отстой, не реализующий до конца ORM. Но спасибо за экскурс по нему. На практике часто приходится пользоваться тем, что есть, даже если оно некрасивое (а не велосипедить по каждому чиху).

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

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

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

                                                                                            Нет, на вход передается подготовленное компилятором 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));
                                                                                            • 0
                                                                                              Круто.

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

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

                                                                                            • +1

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


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

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

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

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

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

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

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


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

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

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

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

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


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

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

                                                                                              • 0
                                                                                                То, с чего началась эта подподветка:
                                                                                                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, […])
                                                                                                    ]
                                                                                                  )
                                                                                                ]);

                                                                                                • 0

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


                                                                                                  var bill = ctx.Bills.Find(3287)
                                                                                                      .LoadItemCount(ctx)
                                                                                                      .LoadTotalPrice(ctx)
                                                                                                      .LoadCustomers(ctx, ...)
                                                                                                  • 0
                                                                                                    Хех, так это, с моей точки зрения, сама суть 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.

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

                                                                                                      Лолшто?

                                                                                                  • 0

                                                                                                    Както-так:


                                                                                                    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.


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

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

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

                                                                                                        • 0
                                                                                                          Хреново, что план будет страшным.
                                                                                                          В идеале, по-моему, это стоит добывать двумя запросами: один (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 — без группировки).
                                                                                                          В идеале оно бы должно само разбить. Странно, что оно делает один. (Я ожидал, что оно по тупости наоборот слишком много запросов сделает.)
                                                                                                          • 0

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

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

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


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


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

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

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


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


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

                                                                                                                      • 0
                                                                                                                        Уточню: получить первые 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>.
                                                                                                                        • 0

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


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


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

                                                                                                                          • 0
                                                                                                                            Т.е. 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 основных экземпляров + несущественная сопровождающая информация для них).
                                                                                                                            • +2
                                                                                                                              WHERE row_number() OVER (PARTITION …) <= 10 — так что ли?

                                                                                                                              да


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

                                                                                                              • 0

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

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

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

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

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

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

                                                                                                                      • 0
                                                                                                                        Надо отобразить не результат джоина 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-й — бубячки (аналогично)).

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

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

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


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

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

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

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

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


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

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

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

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

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