Pull to refresh

Comments 24

Автору стоит рассмотреть питон, там и классы можно намного красивее описывать (начиная с 3.7 можно без attrs, ибо в комплекте появились датаклассы), и sqlalchemy по нормальному разделяет данные и хранилище.
Концепция MVC настолько популярна в среде разработки всевозможных, не только серверных, приложений на разных языках и платформах, что мы уже и не задумываемся о том, что это такое и зачем оно вообще нужно.

"Мы" как минимум задумываемся не лучше ли будет использовать MVVM в той или иной ситуации :)

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

Это гейтвей а не репозиторий. Репозиторий это коллекция доменных моделей.


При использовании именно репозитория, выборки данных для отдачи на клиент, например, будут в гейтвее(возвращать dto/структуры)


Ну и MVC все же не про структуру папок, и то что в вебе называют MVC на самом деле является Model-View-Adapter(контроллер=адаптер, а в MVC контроллер не должен знать про представление)


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

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

Но контроллер вообще не должен быть классом, это должен быть скорее модуль

Почему тогда в примере не сделать контроллер модулем? Зачем ему стейт?

Вместо

class ListInvoices

  def initialize(invoice_repository)
    @repository = invoice_repository
  end

  def call
    @repository.get_all
  end
end

Куда проще

module ListInvoices

  def self.call(repository)
    repository.get_all
  end
end


PS. Какое-то время назад, осознав, что все больше и больше пишу на Руби в функциональном стиле, просто перешел на функциональный язык.

Тогда вам надо repository постоянно "таскать с собой" чтобы передавать как параметер в модуль. А так вы его в контроллер "получаете автоматом" при помощи dependency injection.

Почему тогда в примере не сделать контроллер модулем?

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

//let repository, mail_service, request_params;
// ...
const context = {repository, mail_service, request_params};
// ...
function listInvoices({repository}) {
  repository.getAll();
}


Но это приводит к дополнительной рутине — по пути до этого вызова нужно собрать необходимый контекст руками. Dandy решает эту проблему использованием Dependency Injection с IoC, который может работать с временными объектами: в конструкторе будет всё, что вам нужно.

Зачем ему стейт?

Так или иначе всё имеет состояние. Функция в JS также имеет своё внутреннее состояние, как предложенный Action.
По
HTTP GET
из статьи я полагаю, речь идет о веб приложении. В этом случае вряд ли больше одного раза будет вызываться экшен контроллера за срок жизни запроса?

Почему цепочка вызовов, разве экшен контроллера будет вызываться не из роутера?

Потом, это вы же сами написали
Но контроллер вообще не должен быть классом, это должен быть скорее модуль

теперь вы этот тезис оспариваете?

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

Тогда вам по идее всё это надо регулировать в роутере. То есть какой модуль какие параметры получает при вызове. И repository надо будет пихать в каждый метод как параметр. При этом он может быть совсем не одинок и тогда вам надо будет либо передавать каждый раз н-ное количество параметров, либо вводить dto.
Плюс контроллеров у вас тоже может быть куча разных и тогда ваш роутер ужасно распухнет.


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


Еще вариант работы с зависимостями — использовать контейнер.

В принципе можно и service locator какой-нибудь использовать. Но тогда юнит тесты писать не так удобно.

Тогда вам по идее всё это надо регулировать в роутере

Да. Ну, если мы говорим, что DI это хорошо, то да, цена этого — вызов с зависимостью в параметре. Не хочется протаскивать — можно захардкодить зависимости или использовать контейнер. Это всегда вопрос trade off'a

Я хочу сказать, что если язык объектный, совсем не обязательно все делать объектами. Объект – сложная вещь. Если можно обойтись функцией – лучше обойтись. У контроллера очень простая задача — переадресовать вызов из роутера в соответствующую службу и вернуть отрендеренный результат. Мне кажется это идеальный кандидат для функции. Свой стейт ему не особо нужен.
У контроллера очень простая задача — переадресовать вызов из роутера в соответствующую службу и вернуть отрендеренный результат. Мне кажется это идеальный кандидат для функции. Свой стейт ему не особо нужен

Ну это опять же от ситуации зависит. В контролере обычно(как минимум у меня) сидит вся "фронтэнд-зависимая логика". И она местами может быть не такой уж и маленькой.


То есть стейт ему конечно обычно не нужен, но от этого сильно легче не становится.


И естественно всё что мы сейчас дискутируем это вопрос привычки и удобства. Ну и немного вопрос best practice и имеющихся решений/тулов/библиотек в различных фреймворках.


И грубо говоря если вы возьмёте какой-нибудь .Net MVC, то у вас и примеры/туториалы будут с DI и все так делают. И поэтому скажем помощь на stackoverflow проще найти если тоже так делать.


В других языках/фреймворках всё может быть совсем наоборот. А кто-то вообще MVVM больше любит и/или от MVC плюётся :)

С чем дискутирую я: автор в статье заявил, что «Но контроллер вообще не должен быть классом» — с чем я согласен. Но затем последовал пример с контроллером-классом. Что меня удивило.

Вообще дискуссия полезна для проверки своих аргументов и новых знаний. Мой опыт привел меня к тому, что для веб-приложений объектный подход часто избыточен и приводит к ненужным усложнениям. Этим я и хотел поделиться.
С чем дискутирую я: автор в статье заявил, что «Но контроллер вообще не должен быть классом» — с чем я согласен. Но затем последовал пример с контроллером-классом. Что меня удивило.

Попытаюсь перефразировать, если этот момент настолько принципиален. Controller не должен быть классом, если это всего лишь набор малосвязанных методов. Да, можно было бы его сделать модулем с тем же набором методов. Но, поскольку это ничего не даёт с практической точки зрения кроме демагогии, я предложил замену понятию Controller — Action (не путайте). По задумке автора Action должен быть классом ввиду необходимости корректного использования DI (в комментарии выше есть обоснование, зачем это нужно). Это с точки зрения ООП.
Теперь с точки зрения «ФП». Я не знаю на каком языке вы пишете в стиле ФП, но вот, надеюсь, что для вас не является секретом, что функции в JavaScript это тоже объекты? О том, что каждая функция является «объектом», надеюсь, не нужно рассказывать? И не вызовут удивления вещи типа:

new Function(<functionArgs...>, functionBody)
Function.prototype.constructor
Function.prototype.call

?
Ничего не напоминает? :)
если этот момент настолько принципиален
Дело не в принципиальности и не придирках

это ничего не даёт с практической точки зрения кроме демагогии

… не знаю, что на это сказать.

Action должен быть классом ввиду необходимости корректного использования DI

Может. (Кстати, в Hanami экшены контроллера реализованы отдельными классами в отличие от Rails.) Так же экшен может быть модулем. Для DI не обязателен OOP и классы.

но вот, надеюсь, что для вас не является секретом, что функции в JavaScript это тоже объекты

Не очень понимаю отсылку к JS, учитывая примеры на Руби.
Кстати, в Руби функции тоже объекты и методы объектов тоже объекты, и классы тоже объекты. Надеюсь, для вас это тоже не секрет. Но это не меняет сути. Это разные вещи с разными свойствами вне зависимости от того, что и то, и то реализовано через объекты.
Вообще, не очень понимаю этот аргумент. Если функции реализованы в языке объектами, то что?

PS. Ладно, дискуссии не получилось
Хорошо, вместо контроллера теперь есть набор «функций» для доступа к модели, которые, кстати, тоже можно структурировать, используя каталоги файловой системы, например.

Это пока еще не функции, а паттерн Command, так же как и UseCase/Interactor являются разновидностью этого же паттерна. Технически можно сделать их чистыми функциями, здесь Greg Young рассказывает как это сделать.

С точки зрения Crean Architecture, Front/Page Conttroller — это всего лишь IO-устройство. А с точки зрения Hexagonal Architecture, это всего лишь один из портов. Поэтому, для Clean Architecture вопрос реализации роутинга и пр. не имеет принципиального значения.

У Вас чередуются термины Dependency Injection и Dependency Inversion, — это вещи немного разные, хотя последний и может реализовываться посредством первого.
При этом не совсем понятно, что вы имеете ввиду под DI, обычно, чтобы их отличать, Dependency Inversion пишется полностью — Dependency Inversion Principle (DIP).

сторонник практики инверсии зависимостей (Dependency Inversion, D в SOLID). Поэтому подобные зависимости мне нужно инициализировать где-то снаружи и передавать в конструктор контроллера.

DIP — это политика. Во втором предложении цитаты вы говорите уже о механизме пассивного внедрения зависимостей. Кстати, не обязательно в конструктор, можно и через сеттеры. В случае с конструктором можно использовать каррирование для управления зависимостями (не уверен что в Ruby это возможно).

# Плохо (инициализируем зависимость в конструкторе)

Вы, наверное, имели ввиду Service Locator или Plugin Pattern.

Но мы пойдём дальше и реализуем паттерн Unit of Work

В популярных ОРМ, объект Session реализует не Unit of Work, а паттерн Facade, чтобы облегчить работу с множеством компонентов ORM. Unit of Work не должен содержать метод .load(), так как это уже обязанность Repository/DataMapper.

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

Здесь вы немного поспешили с выводами. А что если Вашему проекту потребуется заменить RDBMS на Polyglot persistence (например, NoSQL + Graph Database) или даже ApplicationDatabase? Хороший архитектор максимизирует количество непринятых решений, и именно для решения этой задачи паттерн Repository и предназначен. Часто он, для достижения полного сокрытия источника данных, используется совместно с паттерном QueryObject или Specification.
Ну и еще такой вопрос — при использовании Polyglot Persistence, как вы думаете осуществлять двухфазные/компенсационные транзакции в Вашем случае?

То есть, мы можем отказаться от Repository и Controller в пользу единого унифицированного Action!

Только не Action, а паттерн Command. Такой подход активно применяется в CQRS.
Только Repository создается обычно для обслуживания одной доменной модели, или даже — одного агрегата.
Попробуйте добавить поддержку, например, Identity Map, и вы очень скоро обнаружите, что ваш подход приводит к Low Cohesion.

def initialize(attrs, payment_service)

Если уж вы и решили присовокупить сервис уровня приложения к доменной модели, то лучше было бы это сделать через Cross-Cutting Concern, Robert Martin как раз об этом пишет в Clean Code. Но лучше было бы их не присовокуплять, а использовать Domain Event.

Поэтому в рамках фреймворка Dandy я реализовал роутер

Задача Clean Architecture как раз и сводится к тому, чтобы минимизировать зависимость приложения от фреймворков.

P.S.: В целом, Вы на правильном пути. Видно желание докопаться до истины и результат. Надеюсь, что чем-то смог Вам помочь.
Большое спасибо за развёрнутый и обстоятельный комментарий!
1. Я намеренно не углублялся в паттерны и старался избегать упоминания терминов и известных личностей, дабы не разжечь нездоровую дискуссию о том, что чем является, а чем не является, особенно рассуждая на стыке разных языков и сред. Это я имел в виду в заключении. Изначально Action и назывался Command (в терминах CQRS). Но после «унификации» с репозиториями он был переименован. Что касается выгоды репозиториев как возможности замены RDBMS, я ни разу этого не делал на практике, поскольку это был бы сильный просчёт на этапе проектирования ошибиться с RDBMS и, простите, глупость. Даже если пришлось бы, то в чём принципиальная разница с апгрейдом Action вместо репозиториев?

2.
Кстати, не обязательно в конструктор, можно и через сеттеры

это «взлом» понятия инверсии зависимостей (для чего мы используем injection). Это было бы не трудно реализовать, но принципиально нет. Вот мой пост о том, как я разрабатывал Hypo, где я указал причины.

3. Да, пардон, DI — везде Dependency Injection.
В целом идея такова, что каждый здесь может увидеть свои интерпретации, кто-то «функции», кто-то «команды». Dandy — это всего лишь роутер, пусть и лихой :), он не налагает особых ограничений на использование. Хотите Domain Events, репозитории, пожалуйста, Actions будут лишь обёртками. А уж использовать его или нет, решать разработчику.
А что если Вашему проекту потребуется заменить RDBMS на Polyglot persistence (например, NoSQL + Graph Database) или даже ApplicationDatabase? Хороший архитектор максимизирует количество непринятых решений, и именно для решения этой задачи паттерн Repository и предназначен. Часто он, для достижения полного сокрытия источника данных, используется совместно с паттерном QueryObject или Specification.

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

Если же у вас примитивные задачи вроде поиска по ключу, то, конечно, там без разницы, искать в таблице, в документах какой-нибудь Mongo, или вообще в key-value. Но в таком случае и особой необходимости в сложной архитектуре я не вижу.

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

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

P.S. Это не значит, что я закидываю сиквел/ноу сиквел квери прямо в контроллеры. Они отделены, но без попыток построить specification + repository + UoW.
Если вы (корректно) используете реляционную базу, значит, она подходит под вашу модель данных и «просто» переехать, особенно целиком, на другое хранилище не просто сложно, а зачастую абсолютно не нужно.

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

Например, при распиле монолита, часто применяются принципы DDD, и агрегато-ориентированные базы данных становятся более привлекательными. Часто вопрос пересмотра выбора БД наступает в момент назревания необходимости шардить данные. Иногда БД перестает удовлетворять при добавлении каких-то новых бизнес-фич. Или, например, требуется внедрить Event Sourcing. Иногда приходится рассматривать проприетарные cloud-решения, предлагаемые крупными хостинг-провайдерами.

Именно поэтому, раз уж мы говорим о Clean Architecture, Роберт Мартин говорит:

«It is not necessary to choose a database system in the early days of development, because the high-level policy should not care which kind of database will be used. Indeed, if the architect is careful, the high-level policy will not care if the database is relational, distributed, hierarchical, or just plain flat files.»

«From an architectural point of view, the database is a non-entity—it is a detail that does not rise to the level of an architectural element. Its relationship to the architecture of a software system is rather like the relationship of a doorknob to the architecture of your home.»

Хороший архитектор максимизирует количество непринятых решений.
Звучит хорошо, но я никогда такого не видел. Это в принципе характерная черта Мартина — он хорошо ставит вопросы и описывает ситуации, которых почти никогда не бывает в реальном мире.

На мой взгляд, база данных и вообще данные это крайне важное решение, поэтому откладывать их на потом не следует, если у нас не стоит выбор между «реляционная база Х» и «реляционная база У», у которых вся разница — вендор.

Опять же, я не противник отделять базу от основной логики, но есть другие способы, кроме репозиториев — простейшее разделение на commands/queries уже очень помогает.
Звучит хорошо, но я никогда такого не видел.
Обе цитаты из «Clean Architecture: A Craftsman’s Guide to Software Structure and Design», 2017

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

простейшее разделение на commands/queries уже очень помогает.
Да, и я даже приводил в качестве примера ссылку на одно из лучших демонстрационных эталонных приложений (reference application). Это хорошо работает, когда логика простая, например, запрос возвращает сразу DTO. Для более сложных случаев, когда используются усложненные сценарии создания агрегата, используется Identity Map и т.п., такой подход может привести к понижению Cohesion, и росту таких Code Smells как Shotgun Surgery или Divergent Change, что негативно отражается на экономике разработки. Хороший код всегда стремится к Low Coupling & High Cohesion. До тех пор, пока Вы в этом балансе — все ок.
Обе цитаты из «Clean Architecture: A Craftsman’s Guide to Software Structure and Design», 2017

Я не видел такого в реальных проектах. Конкретно — абсолютно ignorant логика, который было бы всё равно, какое у нас хранилище. Такое случается только если никакие фичи хранилища не используются и нам действительно всё равно. В крупных проектах, что я видел, такой роскоши нет.

Да, и я даже приводил в качестве примера ссылку на одно из лучших демонстрационных эталонных приложений (reference application)

При всём уважении к МС, это едва ли эталонное приложение, на мой взгляд.

Для более сложных случаев, когда используются усложненные сценарии создания агрегата, используется Identity Map и т.п., такой подход может привести к понижению Cohesion, и росту таких Code Smells как Shotgun Surgery или Divergent Change, что негативно отражается на экономике разработки. Хороший код всегда стремится к Low Coupling & High Cohesion. До тех пор, пока Вы в этом балансе — все ок.

Опять же, звучит очень хорошо, и в теории я со всем согласен. Изначально мой поинт был в том, что эта теория разбивается о практику, в которой репозиторий зачастую не предоставляет корректной абстрации, а является крайне глупым прокси к фреймворку доступа к базе. Соответственно, грубо говоря, недостаточно создать у себя в проекте папочки Repository, Unit of work, etc и гордо думать, что у вас хорошая архитектура. Надо понимать. какие задачи они решают и действительно ли помогут переехать на другую базу, и нужно ли это вообще. С этим у людей проблемы, и Мартин не помогает, потому как его слова — или абстрактная вода, или примеры на уровне магазина, где, конечно же, всё идеально работает.
При всём уважении к МС, это едва ли эталонное приложение, на мой взгляд.
Возможно, но это субъективно (спасибо что прямо об этом говорите). Другое субъективное мнение по этому поводу можно услышать от Greg Young (автора термина CQRS): «A perfect example of this [you can see] if you go look at the CQRS and Event Sourcing by Microsoft Patterns and Practices, which is heavily focused on doing this inside of Azure using their toolkits.»

создать у себя в проекте папочки Repository, Unit of work, etc и гордо думать, что у вас хорошая архитектура
При всем уважении, лучше не измерять качество архитектуры папочками. Качество архитектуры измеряется экономикой разработки, и соответствием поставленной задаче достигнутого баланса Quality Attributes (сейчас они стандартизованы).
Хороший архитектор максимизирует количество непринятых решений.

В целом согласен, поэтому и являюсь приверженцем Persistence Ignorance. Только, как мне кажется, после детального моделирования решение уже всё-таки пора принять. Здесь я не противоречу дяде Бобу.

Главное в плане непринятых решений до абсурда не доводить. Наш брат не особо любит говорящие бестолковые головы, которые всячески стараются снять с себя любую ответственность :).
Sign up to leave a comment.

Articles