Comments 32
В этом контексте чистая архитектура (clean architecture) подразумевает организацию проекта таким образом, чтобы в нем можно было легко разобраться, и чтобы его можно было легко изменять по мере роста проекта.
Ой. А такая существует? Было бы просто здорово всегда иметь возможность «легко изменять по мере роста проекта».
Для этого нам необходимо разделять файлы и классы на компоненты, которые могут изменяться независимо друг от друга.
Стоп. А такое бывает? Смотрите: мы изменяем логику работы одного компонента, но не меняем логику работы другого компонента. И всё продолжает работать?
Из представленных иллюстраций можно сделать выводы о том, что архитектуру, представленную на втором изображении, гораздо проще изменить.
Пока проще только на бумаге.
Эта же концепция является архитектурой, которая упростит обслуживание и изменение вашего программного обеспечения.
Концепция чего?
Вообще, здесь центральный вопрос: а зачем нам надо что-то менять? Допустим, мы строим настоящее здание. Что нам может потребоваться в нём изменить уже после постройки? Мы же не сможем снести несущие стены, на то эти стены и несущие. Тут одно из двух: либо мы промахнулись с архитектурой или ... не знаем, чего хотим.
Стоп. А такое бывает? Смотрите: мы изменяем логику работы одного компонента, но не меняем логику работы другого компонента. И всё продолжает работать?
Да, такое бывает. Это и есть чистая архитектура, а всё остальное в статье лишнее. Как такое возможно? Да легко. Вот есть трактор с плугом, можно пахать. А можно отцепить от трактора плуг и прицепить сеялку, и всё опять же прекрасно будет работать "без изменения логики работы трактора". Программы должны работать точно так же.
отцепить ... прицепить
Это, как раз, и означает композицию объектов различных классов. Вопрос, лишь, в том, когда это происходит — на этапе компиляции или во время работы приложения. Игры с наследованием, обычно, относятся к этапу компиляции, а для времени выполнения можно предусмотреть фабрику объектов-наследников, которая выдаёт объект требуемого класса в соответствии с поданной командой. Но и тут мы будем ограничены заданным набором классов.
Это вы очень сильно упростили. На деле "плуг" может быть не классом, а сложнейшим приложением из десятков микросервисов, написанных на разных языках. Тут вся суть в отсутствии циклических зависимостей и грамотных интерфейсах. Не только, интерфейсов из ООП, API - это ведь тоже интерфейс. В примере с трактором интерфейс - это посадочное место для навесного оборудования. И производитель трактора как раз не ограничен "заданным набором классов", т.к. он знать не знает, что захочет повесить конечный потребитель на его трактор. В то же время производитель плугов тоже не знает, какой именно будет трактор, он просто имплементирует интерфейс, то есть посадочное место в его случае. Сам по себе трактор тоже подчиняется этим принципам: можно поменять колёса, кабина может отличаться. И дальше можно продолжать дробить: в кабине можно менять приборную панель, в приборной панели тахометр, на тахометре циферблат. Всё чётко разделяется на узля, подузлы и т.д. И замена одного узла не нарушает работу остальных.
Вот точно так в приложении с правильной архитектурой должна быть модульность и грамотные зависимости. Например, ты делаешь для своего приложения модуль для защиты форм капчей. При правильной архитектуре при выключении модуля на формах просто пропадёт капча, но формы продолжат работу. А при неправильной формы перестанут работать, т.к. будет требоваться валидация элемента, который более не существует.
ну а теперь замените двигатель в тракторе чтобы добавить мощности, и смотрите как вся эта красивая картинка рассыпается
Что за глупости? Как будто не существует моделей тракторов, где доступны разные двигатели? Или как будто мало примеров, когда ставят нештатный двигатель с доработками в трактор и всё это потом прекрасно работает десятилетиями? Переводя на язык программиста, можно безболезненно ставить двигатель, который имплементирует нужный интерфейс. А если подходящего двигателя нет в наличии, можно поставить другой, используя паттерн адаптер.
Вообще, немного странно видеть, что методики проектирования, прекрасно работающие уже порядка ста лет, остаются непонятными для многих представителей, казалось бы, передовой отрасли.
нет таких примеров, более мощный двигатель это замена коробки, подвески, всей электроники двигателя и после этого возможно трактор слишком тяжелый чтобы работать в поле
конечно можно сказать что мы заменим на аналогичный, но в мире софта такое не возможно
Что значит нет таких примеров? В любой деревне можно встретить подобных Франкенштейнов.
в мире софта такое не возможно
Чушь полная. Как тогда, по-вашему получается одинаковые библиотеки использовать в разных проектах? Или у вас не получается? :D
В деревнях таких франкенштейнов достаточно на ремонте стоит, но это не то как мы хотим разрабатывать ПО.
Как тогда, по-вашему получается одинаковые библиотеки использовать в разных проектах
Ну если следовать аналогии то это замена одной библиотеки на другую, а один двигатель в разных моделях тракторов это конечно не новость
В деревнях таких франкенштейнов достаточно на ремонте стоит
А трактора в штатной комплектации на ремонте не стоят?
Да и фиг с ними, с тракторами, тут большинство их разве что на картинках видели, давайте вернёмся к программированию, я выше приводил пример про капчу с правильной и неправильной реализацией. Неужели недостаточно понятно?
При правильной архитектуре при выключении модуля на формах просто пропадёт капча, но формы продолжат работу. А при неправильной формы перестанут работать, т.к. будет требоваться валидация элемента, который более не существует.
Вот Вы и сформулировали довольно точно представление о том, что такое архитектура и что значит правильная архитектура.
Внутренний круг - это уровень предметной области вашего приложения. Здесь вы размещаете бизнес-правила.
Где это здесь? Во внутреннем круге? А зачем?
Под "бизнесом" мы необязательно подразумеваем компанию. Это просто означает суть того, что делает ваше приложение, основную функциональность кода.
Бизнес-правила — это ограничения на обработку данных, определяемые предметной областью.
Эти бизнес-правила, довольно стабильны, поскольку вы вряд ли будете часто менять суть того, что делает ваше приложение.
Именно они и меняются чаще всего. Иначе было бы довольно просто сделать монолитное приложение и уже больше его никак не поддерживать. Зачем поддерживать то, что и так работает, как часы?
Например, у вас больше шансов изменить внешний вид кнопки пользовательского интерфейса, чем способ расчета кредита.
Спорное утверждение.
Граница между доменом и инфраструктурой устанавливается таким образом, что домен ничего не знает об инфраструктуре. Это означает, что пользовательский интерфейс и база данных зависят от бизнес-правил, но эти правила не зависят от пользовательского интерфейса или базы данных. Это делает его архитектурой подключаемого модуля.
Кому-то удалось реализовать такой подход?
Адаптеры, также называемые интерфейсными адаптерами, являются трансляторами между доменом и инфраструктурой. Например, они берут входные данные из графического интерфейса пользователя и переупаковывают их в форме, удобной для вариантов использования и сущностей. Затем они берут выходные данные и переупаковывают их в форму, удобную для отображения в графическом интерфейсе или сохранения в базе данных.
Вот и возникает вопрос: не имеет место, в действительности, некоторое дублирование? Мы же должны в графическом интерфейсе сделать какую-то контекстную обработку. Вот и возникает зависимость.
... класс должен выполнять только одну задачу. У него может быть несколько методов, но все они работают вместе, чтобы выполнять одну основную задачу
Замечательно! Осталось понять, как именно следует декомпозировать крупные задачи на мелкие, чтобы для каждой мелкой задачи сделать свой отдельный класс. Такое разделение и есть архитектура приложения, в собственном смысле этого слова!
Например, если у финансового отдела есть одно требование, которое изменит класс, а у отдела кадров есть другое требование, которое изменит класс по-другому, то есть две причины для изменения. Класс должен быть разделен на два отдельных класса, у каждого из которых есть только одна причина для изменения.
А можно сделать внутри класса (некоторого достаточно общего типа) приёмник сообщений и предусмотреть различную реакцию на различные сообщения.
Таким образом, вы должны иметь возможность добавлять функциональность к классу или компоненту, но вам не нужно изменять существующую функциональность.
Вопрос в том, каким именно образом достигается специализация. Здесь, очевидно, говорится о наследовании. Но мы можем конструировать объекты как композиции объектов некоторых базовых классов.
Давайте рассмотрим обобщённый пример. В большинстве ситуаций, мы имеем дело со списками однородных объектов. Соответственно, у нас будет класс Список. Но, если у нас есть объекты определённого типа, то у нас появляется необходимость создавать производный класс СписокОбъектовОпределённогоТипа. В этом смысле, класс Список выступает как общий шаблон для произвольного количества производных классов.
Например, в Java ArrayList и связанный список реализуют интерфейс List, поэтому их можно заменять друг на друга.
Тут трудность заключается в том, что у каждого абстрактного типа данных может быть несколько различных реализаций. Мы можем взять простой линейный (в памяти) массив и реализовать любую структуру данных. Довольно трудно отделить класс от его реализации. Постоянное смешение вносит определённую путаницу в программирование и, очевидно, нарушает чистоту чистой архитектуры.
Интерфейс предоставляет только то подмножество методов, которое необходимо зависимому классу. Таким образом, изменения в других методах не влияют на зависимый класс.
Вопрос в том, почему в интерфейс входят именно эти методы.
Этот принцип означает, что менее стабильные классы и компоненты должны зависеть от более стабильных, а не наоборот.
А почему должны быть какие-то нестабильные классы?
А почему все классы должны быть стабильные? Удачи отредачить проект, где все классы и модули стабильные)) Нестабильность классов или модулей позволяют их изменять или заменять не боясь, что изменится всё что находится уровнем выше. Иначе в ооп парадигме это считай нужно будет править всех наследников, а потом ещё и тестировать, а потом ещё и разворачивать заново. На практике это выражается допустим в различных реализациях какого нибудь клиента для различных заказчиков. Или же как часто бывает на начальных этапах неизвестно о низкоуровневых деталях проекта, поэтому вместо них лепят абстракцию и делают временную реализацию этого интерфейса до выяснения обстоятельств, чтобы банально проект запустить.
Хотя это конечно не относится к одноразовым и лёгковесным проектам с небольшим набором критических бизнес правил. Если рассматривать в таком ключе, то классы или модули могут быть хоть все стабильные.
На практике это выражается допустим в различных реализациях какого нибудь клиента для различных заказчиков.
Так это и есть центральный вопрос архитектуры приложения! Где должно содержаться и реализовываться различие. ООП предлагает решить этот вопрос через наследование. Но является и этот подход единственным? Никто не мешает иметь внутри объекта набор правил и для каждой предметной ситуации делать свой набор правил. Боюсь, ООП выступает здесь как та "тёмная сторона Силы", которая затуманивает сознание.
Наследование возможно и вне ооп. Архитектура приложений может реализовываться в рамках разных парадм в зависимости от ситуаций. Например, если есть ограниченный набор данных и предлагается реализовывать различные функции над этими данными, то это идеальная задача для процедурного программирования, и наоборот если имеется куча данных и ограниченный набор функций над этими данными, то это задача для ооп. Решить эти задачи можно в любой из парадигм, но реализовав первую задачу в ооп парадигме при добавлении функции над данными придется реализовать функции всем наследникам, а если вторую задачу решить в процедурном стиле, то при введении очередного типа данных придется изменять все функции над набором данных. В рамках чистой архитектуры рассматриваются модули, а не классы, а модулем может быть хоть класс, хоть файл, хоть сервис или любая другая программная единица. Чистая архитектура не про ограничения и правила ради усложнений, а наоборот чтобы люди писали просто и понятно в рамках реализуемой задачи, и не в коем случае не пытались натянуть сову на глобус. От задачи к задаче всё разное. Чистая архитектура это некое философское течение, по типу "не что, а как"
Например, если есть ограниченный набор данных и предлагается реализовывать различные функции над этими данными, то это идеальная задача для процедурного программирования, и наоборот если имеется куча данных и ограниченный набор функций над этими данными, то это задача для ооп.
Интересный способ выбора. Наверное, так и есть. В первом приближении.
Решить эти задачи можно в любой из парадигм, но реализовав первую задачу в ооп парадигме при добавлении функции над данными придется реализовать функции всем наследникам, ...
Подождите, а в чём, собственно, заключается первая задача? Вы говорили о реализации различных функций обработки ограниченных данных. Значит, каждая новая функция обработки — это метод, замещающий абстрактный родительский метод, то есть — метод, реализующий некоторый общий интерфейс. Различие между процедурным программированием и ООП здесь, лишь, стилистическая, то есть, где-то на уровне синтаксиса языка программирования (с «синтаксическим сахаром» или без оного).
... а если вторую задачу решить в процедурном стиле, то при введении очередного типа данных придется изменять все функции над набором данных.
Это всё определённо требует написания целой серии статей. Придётся отправиться в прошлое и не малой долей критицизма пройтись по сложившимся представлениям.
В рамках чистой архитектуры рассматриваются модули, а не классы, а модулем может быть хоть класс, хоть файл, хоть сервис или любая другая программная единица.
Совершенно верно. Я даже помню, что, в своё время, были такие алголоподобные языки как Модула и Ада, в которых этой самой модульности было гораздо больше, чем в самом Паскале и, уж тем более, в Си/С++/С#.
Здесь центральный вопрос заключается в том, какую именно задачу решает отдельный модуль. Я привык, что модуль может быть ответственен, например, за пользовательский интерфейс, за работу с файлами определённого формата (например, модуль типа Comport, который позволяет открывать COM-порт как последовательный файл)... Но есть ещё и компоненты.
Чистая архитектура не про ограничения и правила ради усложнений, а наоборот чтобы люди писали просто и понятно в рамках реализуемой задачи, и не в коем случае не пытались натянуть сову на глобус.
Вот меня и интересует, удаётся ли когда-нибудь достигать простоты и понятности. Если существует такой действенный способ, то его надо использовать повсеместно. Вы, только, представьте, что у Вас всё просто и понятно, так это значит, что можно быстро и понятно объяснить любому новичку суть реализованной системы и ожидать от него быстрой отдачи в виде реально работающего кода. Я уже и не говорю о возможности быстрого исправления ошибок, а, в некоторых случаях, и вовсе исключения их появления.
От задачи к задаче всё разное. Чистая архитектура это некое философское течение, по типу "не что, а как"
Как было бы здорово написать про это статью! И не одну, но это должны писать завзятые специалисты. Прошедшие огонь и воду. Достигшие высокого уровня просветления. Но где же сыщешь таких? Вы возьмётесь?
... но в рамках данной статьи другие методы мы не рассматриваем.
Продолжения разговора не будет?
В этой статье мы рассмотрели основы так называемой чистой архитектуры и принципы ее использования при разработке.
Осталось много непонятного. Пример также нуждается в комментариях.
С помощью чистой архитектуры можно сделать архитектуру проекта гораздо более понятной для понимания и разработки.
Архитектура архитектуры архитектУрна и архитЕкторна.
Чистая архитектура для начинающих