Русский
Русский
English
Статистика
Реклама

Clean architecture

Как скрам помогает стать более сильным разработчиком?

28.11.2020 00:23:46 | Автор: admin

Тема методологий и процессов разработки, как правило, не особо интересна разработчикам. Абсолютно нормально услышать: Должен быть менеджер, который этим занимается. Как мне кажется, большинство разработчиков попросту не видят достаточно ценности в том, чтобы понимать процессы компании. Однако, по моему опыту, это крайне важный компонент, который позволяет программистам становиться сильнее именно с технической точки зрения, а также двигаться по карьерной лестнице вверх. Эту связь я и попытаюсь показать.

 Copyright Max Degtyarev (http://personeltest.ru/aways/www.behance.net/maxdwork) Copyright Max Degtyarev (http://personeltest.ru/aways/www.behance.net/maxdwork)

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

Что до Soft skills, то, к сожалению, это считается чуть ли не лишним ингредиентом, пустой тратой времени, или же несущественным элементом. Ценность Hard Skills сильно выше, чтобы переставать работать с человеком из-за проблем с Soft Skills.

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

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

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

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

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

Цель

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

Однако, здесь не достаёт чуточку цели.

Одна из очевидных целей любой коммерческой организации - заработок денег в обмен на продукты или экспертизу, которую они поставляют на рынок. Работники компаний формируют команды, а они, подобно оркестру, исполняют свои обязанности в нужном количестве и качестве, а главноев срок.

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

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

Итерации

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

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

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

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

Если что-то из вышесказанного кажется недостаточно аргументированным, то я хочу порекомендовать вам набор статей по Theory of constraints, который содержит выборку необходимой и достаточной информации по выше изложенным фактам.

Для разработчиков из всего вышеизложенного важно одно: направление развития продукта (и компании) будет меняться, точно!

Работая в стартапах сложно сказать какую проблему придётся решать через 6 месяцев. Никто не знал, что видео-сервис для свиданий станет самым популярным в мире видео-стриминговым сервисом YouTube. Так же, как никто не предполагал, что игра для социализации может превратиться в чат для коллег, как в случае Slack. Бизнесы, которые не смогли повернуть свои продукты в правильном направлении, либо уже исчезли, либо близки к провалу.

Пример плохого развития событий в фазовом подходе Waterfall.Пример плохого развития событий в фазовом подходе Waterfall.

Гибкие методология стали появляться как раз-таки на волне понимания, как важно сохранить возможность гибкости стратегии, возможность в нужный момент резко повернуть в сторону, а не продолжать двигаться по длинным фазам, делая проект, в целях которого уже не уверен. Одним из важнейших отличий гибких методологий от фазовых подходов, вроде Waterfall, являются короткие итерации. Укажу лишь несколько причин по которым организация может видеть итерации полезными:

  • Возможность скорректировать путь развития продукта, определив новое направление или возможности заработка.

  • Снизить потери в случае неправильных решений или не оправдавших себя ожиданий.

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

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

 Пример инкрементальной доставки изменений в продукте. Пример инкрементальной доставки изменений в продукте.

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

Гибкая архитектура

Есть одна фундаментальная вещь, которую нужно понять с точки зрения разработки: преждевременное принятие решений (up-front design)это не всегда хорошо, а чаще плохо.

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

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

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

Последние как раз и являются самыми проблематичными. И когда я говорю про Up-Front design, подразумевается именно ряд решений, которые невозможно или слишком долго повернуть вспять.

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

  1. Допустим, сегодня вы готовы полностью положиться на какого-либо вендора и писать код, не заботясь об абстракциях, отделяющих ваше решение от специфичных этому вендору сущностей. Будете ли вы считать это правильным выбором через 2 года?

  2. Выбрав фреймворк для написания программы, подумали ли вы насколько легко будет переключиться на другой фреймворк в случае необходимости?

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

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

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

Ещё один сложный вопрос: как вести разработку в условиях изменяющихся требований? Изменяющиеся требования среди разработчиков принято считать чем то очень плохим.
Это происходит из-за того, что необходимость менять требования заложена в понятие программного (Softгибкий) обеспечения, но не указана явно.

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

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

When requirements change, the difficulty in making such a change should be proportional to the scope of the change, not to the shape of the change. The difference between scope and shape often drives the growth in software development costs. It is the reason that the first year of development is much cheaper than the second, and the second year is much less expensive than the third.

The goal of software architecture is to minimize the human resources required to build and maintain the required system.

Robert C. Martin, Clean Architecture: A Craftsmans Guide to Software Structure andDesignRobert C. Martin, Clean Architecture: A Craftsmans Guide to Software Structure andDesign Роберт Мартин, Чистая Архитектура: искусство разработки программного обеспечения. Роберт Мартин, Чистая Архитектура: искусство разработки программного обеспечения.

Позвольте добавить один совет, которого в книге нет: разработчики должны создавать настолько мало кода, насколько это вообще возможно, а в идеале вообще обойтись без кода. При этом нужно всё также добавлять функционал. Феноменально, не правда ли?

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

  • Меньше нового кода, означает меньше поддержки.

  • Меньше нового кода также означает меньше всего что может сломаться (работает, не трогай! помните?).

  • Меньше нового кодаменьше когнитивной нагрузки.

  • Меньше нового кодабольше уверенности в результате работы.

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

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

  2. Ровно так же, не стоит писать свой код реализации шифрования. Не стоит писать код там, где вы не являетесь специалистом. Ваши пользователи, как и бизнес, этого не оценят.

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


Если говорить о коде, то программисты нанимаются для того, чтобы вносить изменения в поведение программного обеспечения так быстро, насколько это возможно. Разработчики могут поддерживать скорость изменений на достаточном уровне только создавая достаточно гибкую архитектуру. Для этого требуется своевременная коммуникация с бизнесом для выяснения что на самом деле ему надо. Также, требуется тщательно продумывать каждое изменение, контролируя архитектуру и технический долг, и постоянно уточняя требования. Если не обратить внимание хотя бы на одну из этих составляющих (перестать коммуницировать с бизнесом или же не обращать внимание на архитектуру), то так или иначе, но скорость внесения изменений очень быстро упадёт, и к вам возникнут вопросы. В последнее время всё чаще встречаются термины Agile Architecture и Lean Architecture. Я предпочитаю объединить эти понятия в одном термине: Гибкая архитектура.

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

Сильный разработчик

Понятие очень размытое и меняет своё значение от компании к компании, от бизнеса к бизнесу, и от команды к команде. Оно часто зависит от мнения начальства, менеджеров, и вообще коллег. Soft skills как раз нужны для того, чтобы это мнение было положительным. Но многим разработчикам эти навыки даются нелегко. Зато, одно остаётся верным всегда: сильный разработчик тот, кто умеет проектировать программное обеспечение, которое соответствует нуждам бизнеса.

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

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

Представьте, насколько сильно изменилась архитектура Android, который изначально был задуман как операционная система для камер, и только. А теперь это одна из двух самых популярных мобильных платформ. Другой пример, PayPal, который создавался как сервис перевода денег между телефонов с операционной системой Palm OS. А теперь сервис обрабатывает миллионы платежей по всему миру.
Врядли, развитие кодовой базы этих проектов было монотонным ростом функционала из года в год. Уверен, что такие изменения, словно большой взрыв, порождают массу работ по адаптации, рефакторингу и переписыванию с ноля. Это конечно одни из самых сложных случаев. Но кто может заранее предположить путь вашего проекта?

А где жескрам?

После того как я провёл параллели между необходимостью бизнеса и техническими навыками разработчиков, а также поверхностно описал трудоёмкость стоящих перед разработчиком задач, думаю настало время суммировать, чем же именно правильно интегрированный скрам поддерживает технический навыки разработчиков. Да, я не оговорился, именно правильно интегрированный скрам. Я видел примеры плохой интеграции методологий, но видел и успешные случаи. С полной ответственностью могу сказать, что одно без другого не получится. Итак:

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


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

  • Скрам позволяет командам действовать независимо, самостоятельно принимать решения и не бояться пробовать новые идеи. Представьте, что вы приняли неверное решение в одной из 2-недельных итераций, внесли изменения, а во время презентации работы выяснились неожиданные подробности.


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

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


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

Вывод

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

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

Подробнее..

Архитектурные паттерны в iOS привет от дядюшки Боба, или Clean Architecture

03.06.2021 10:20:31 | Автор: admin

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

Все, кто хочет не просто знать что стоит за названием той или иной архитектуры, но ещё и в каком случае какую использовать наливайте чай и устраивайтесь поудобнее, будет лампово. Разбираем паттерны, реализующие концепцию Чистой Архитектуры самые масштабируемые и надёжные :)

Введение

Привет, Хабр! Я всё ещё ведущий инженер-разработчик iOS в КРОК и аспирант-препод в МЭИ. В этом посте я рассказывала про архитектурные паттерны MV(X). У всех MV(X) архитектур есть один общий недостаток: они не описывают как должно происходить взаимодействие между экранами, только то как данные циркулируют внутри экрана.

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

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

По сути, MV(X) архитектуры и не являются архитектурами вовсе это презентационные паттерны, которые описывают слой представления и не более того. В этом-то и кроется вся проблема! Мы пытаемся использовать MVC как архитектуру, когда это просто паттерн для описания её кусочка.

Чистая архитектура

Основная часть заблуждений относительно того, является MV(X) архитектурой или нет кроется в том, что в MV(X) всегда отделяют слой Модели, и кажется что этого как-то достаточно. Но на самом деле нет.

Дело в том, что зачастую бизнес-логику удобно поделить на два уровня:

  1. Логика предметной области (Enterprise business logic) описывает собственно бизнес-процессы. Например объект студент можно преобразовать в магистра и в программера (по отдельности или одновременно) это описание предметной области. Также при преобразовании студента в магистра ему необходимо выдать шапочку то есть это уже целый бизнес-процесс.

  2. Логика приложения (Application logic) описывает процессы и потоки данных внутри приложения. Как правило, они мало связаны с логикой предметной области и больше зависят от UI/UX дизайна и внутренней кухни платформы. Например, чтобы этот человечек получил красивую шапочку, нужно запустить бизнес-процесс его преобразования из студента в магистра, а для этого надо перейти на экран выдавания шапочек и нажать кнопку дать шапку это и есть логика приложения. Иначе она может называться как сценарии использования (Use Cases) и, вообще говоря, описывает то, как модели предметной области применяются в нашем приложении.

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

Разворачивая зависимости дальше увидим, что от логики приложения зависят все наши контроллеры и/или презентеры: они сами по себе просто не знают что и когда показывать, они знают только как показывать: через Виды.

Виды и весь UI-специфичный слой зависит, понятное дело, от презентеров и контроллеров.

Непокрытыми у нас остались только всякие нетворк-слои, data access layers и различные внешние зависимости. Обобщая, мы увидим, что во внутренних слоях, описанных выше, им места нет, а значит, придется их вывести наружу.

Получим примерно вот такое [1]:

Каждый круг изображает части ПО. Внешние круги описывают механизмы взаимодействия, а внутренние правила взаимодействия [1]. Названия в секторах на иллюстрации примерные и не обязаны быть именно такими [4] (может приложение вообще без нетворка работает имеет право!), и приведены просто чтобы вы представили что именно представляет из себя тот или иной слой. По сути их можно поделить следующим образом:

  1. Сущности (предметная область и логика)

  2. Сценарии использования (логика приложения)

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

  4. Внешние фреймворки (и/или устройства)

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

а) переиспользовать описание предметной модели, просто взяв сорсы и впихнув в другое приложение

Гексагональная архитектура

Похожий принцип выделения адаптеров и логики используется в так называемой гексагональной архитектуре (Hexagonal Architecture, она же Ports&Adapters Pattern) за исключением того, что в гексагональной архитектуре нет разделения на слои.

Вкратце, вот отличная иллюстрация этого подхода [1]:

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

Но почитать о ней подробнее можно здесь:

[1] Hexagonal Architecture for iOS. An architecture pattern that focuses on | by Oleksandr Stepanov

[2] Clean and Hexagonal Architectures for Dummies | by Lus Soares | CodeX | Mar, 2021

Если представить эту же диаграмму как пирамидку, то запомнить что UI это низкоуровневое, а предметная модель верхнеуровневое, становится совсем легко:

Более того, таких слоёв может быть и больше (и меньше): порой имеет смысл какие-то из них разделить и вложить друг в друга или наоборот. Если говорить о чистой архитектуре в целом, то такую задачу разделения слоёв приходится решать каждый раз заново это и есть проектирование архитектуры. В [4] предлагается разделять компоненты на верхнеуровневые политики и низкоуровневые детали:

  • политики это правила по которым что-то происходит с данными (бизнес-логика, правила валидации)

  • детали это компоненты, которые что-то делают с данными согласно политикам (СУБД, UIKit)

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

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

Правило зависимостей

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

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

Это называется правило зависимостей (Dependency Rule) [1], и означает оно следующее: ничто во внутреннем круге не может знать или как-либо ссылаться на что-либо во внешнем круге. К примеру, ни одно понятие (класс, функция, переменная), упомянутое во внешнем круге, не должно упоминаться во внутреннем.

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

Это правило относится и к форматам данных: внутренние круги оперируют одними форматами данных, а внешние другими. Мы не хотим, чтобы внешние круги влияли на работу внутренних. Их задача передавать нам информацию, а не ломать нас!

Примечание: под форматами данных тут конечно же имеются в виду модели: классы или структуры, которыми описываются те или иные объекты.

Источники:

[1] Clean Coder Blog | The Clean Architecture

[2] madetech/clean-architecture: A (work-in-progress) guide to the methodology behind Made Tech Flavoured Clean Architecture

[3] Заблуждения Clean Architecture / Блог компании MobileUp / Хабр

[4] Clean Architecture | A CRAFTSMANS GUIDE TO SOFTWARE STRUCTURE AND DESIGN Robert C. Martin

Примеров реализации чистой архитектуры среди архитектурных паттернов для iOS немало, наиболее известными из них являются пожалуй VIPER и CleanSwift.

VIPER

VIPER страшный сон большинства iOS разработчиков на рынке. На интервью вопросы про VIPER вызывают разные реакции, но самая распространенная бей и беги.

Кажется, VIPER это что-то прикольное, давайте разберёмся, что это все-таки за зверь.

VIPER расшифровывается в схожей манере с MV(X):

  • View показывает что скажет Презентер и передает ввод пользователя Презентеру

  • Interactor содержит описание сценария использования

  • Presenter содержит логику отображения и умеет подготавливать данные для представления пользователю, а также реагировать на ввод пользователя

  • Entity описание предметной модели

  • Routing описывает логику навигации между экранами

VIPER это про SOLID, так что у нас так много компонент для того чтобы обеспечить S из SOLID Single Responsibility Principle (принцип единственной ответственности: это когда каждый элемент отвечает за что-то одно).

Если рисовать, все это будет выглядеть примерно так:

В целом, картинка знакомая, добавился по сути всего один новый элемент: какой-то жёлтенький интерактор. Но я все равно расскажу вам про каждый:

  • Вид: наша любимая связка UIView+UIViewController.

    • знает о Презентере

      • посылает ему действия пользователя

      • получает от него запросы на обновление представлений

  • Презентер: содержит связанную с UI бизнес-логику (при этом не зависит от UIKit)

    • знает об Интеракторе

      • посылает ему запросы данных

      • получает от него события об обновлении данных

    • знает о Роутере

      • получает от него запросы на отображение Вида

    • влияет на Вид

      • посылает ему запросы на обновление представлений

  • Роутер: описывает навигацию между экранами (или VIPER модулями, если они не равны экранам)

    • влияет на Презентер

      • посылает ему запросы на отображение Вида

  • Интерактор: описывает взаимодействие с данными: что откуда взять и куда сохранить

    • влияет на Презентер

      • получает от него запросы данных

      • отправляет ему события об обновлении данных

    • знает о Моделях

      • использует Модели чтобы структурировать данные в принятый формат

  • Модель (Сущность, Entity): описывает структуру данных

    • больше ничего не умеет

    • это реально просто описание

Как мы видим, принципиально изменились две вещи:

  1. Модель стала тупым описанием данных, без какой-либо логики обработки или, боже упаси, CRUD

  2. Ответственность сильно поделилась между Презентером и Интерактором:

    1. Мама-Презентер отвечает за UI: просит данные у папы-Интерактора, подготавливает их для малыша-View и говорит малышу когда, как и что показывать

    2. Папа-Интерактор отвечает за данные: когда Презентер просит что-то показать, именно Интерактор идёт в базу, делает всякий CRUD, а потом отдает Презентеру готовый ответ (получилось или нет, вот данные или вот ошибка)

Так вот и выглядят VIPER модули. Если уж быть до конца честным, то в жизни они выглядят скорее вот так:

Интерактор может работать с несколькими Моделями, а Data Access Layer выделен в отдельный компонент, например сервис.

Глядя на диаграммы, несложно заметить, что собирать всё это дело так, чтобы соблюсти правило зависимостей непросто. Настолько, что проще всего выделить под сборку еще один, отдельный компонент, который знает все обо всех (похоже на MPV+C, как и весь VIPER похож на MVP) и где можно легко подставить вместо старого, например, интерактора новый. Такие компоненты называются Builder [6] (или Assembly [3]).

Рисовать это довольно страшно, давайте я просто покажу пример кода:

Тем временем в роутере другого модуля по имени Main:

Не идеальный, но показательный пример из https://github.com/theswiftdev/tutorials

Можно вместо этого сделать полноценный ServiceLocator или использовать уже готовый в составе Swinject и других библиотек. Подробнее про DI можно почитать в этой крутой статье.

Если полазить по репозиториям с VIPER кодом (особенно по шаблонизаторам и тем, которые описывают туториалы, например [3, 6, 7, 8]), можно больно напороться на кучу протоколов, классы-интерфейсы и все вот это вот ООП-шно абстрактное.

С одной стороны, все это велосипед, прикрывающий проблему сборки и неспособность VIPER соответствовать парадигме UIKit. С другой, каждый такой протокол и наследование очередной непокрытый участок кода, который придется тестировать отдельно. А значит, хоть VIPER и testable out of the box но тестов придется написать в X раз больше, чем хотелось бы.

Источники:

[1] Architecting iOS Apps with VIPER objc.io

[2] iOS Architecture Patterns and Best Practices for Advanced Programming - 2021

[3] strongself/The-Book-of-VIPER: the one and the only

[4] Getting Started with the VIPER Architecture Pattern

[5] The Good, The Bad and the Ugly of VIPER architecture for iOS apps.

[6] The ultimate VIPER architecture tutorial

[7] https://github.com/BinaryBirds/swift-template

[8] https://github.com/infinum/iOS-VIPER-Xcode-Templates

RIBs

Описанная выше вариация VIPER с билдером на самом деле очень близка к другому архитектурному паттерну, который придумали в Uber и назвали RIBs Architecture.

И если VIPER пытается привнести в Android какие-то iOS-специфичные проблемы (например высокую связность View и ViewController), то RIBs наоборот, привносит Android-специфичные заморочки в iOS :) Впрочем, и то и другое позволяет нам с наименьшими потерями переписывать код с одной платформы под другую, и, если верить Uber, RIBs с этой задачей справляется лучше.

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

RIBs заключается в том, что существуют определенные RIB-блоки, которые взаимодействуют между собой.

Router осуществляет навигацию между RIB-блоками

Interactor содержит бизнес-логику RIB-блока

Builder конструктор, который собирает RIB-блок

Вид и Презентер в этой иерархии опциональны: если в RIB-блоке нет необходимости рисовать UI, то Вид не нужен; если нет необходимости переводить данные из одного формата в другой Презентер не нужен. Формально тут Презентер как правило является протоколом, по которому Интерактор общается с Видом.

Еще есть Компонент это такая хитрая штука, которая управляет зависимостями RIB-блока. У RIB-блока есть определенные зависимости: такие вещи, которые спускаются ему указом сверху от родительского RIB-блока и хочется, чтобы они были корректно сформулированы. RIB Dependency это протокол, описывающий такие зависимости. А RIB Component это реализация такого протокола. (Если вы знакомы с Android и Dagger, то уже знаете о каких компонентах идёт речь ;))

То есть в Builder дочернего RIB-блока благодаря Component получаются от родителя все зависимости, которые этому ребёнку необходимы (и на шторы скинуться не забудьте!)

Выглядит немного запутанно, но на самом деле всё довольно просто. Presenter и View это одна целая штуковина, при необходимости опущенная или разделённая. Связь Builder с Component отображает процесс Dependency Injection на этапе сборки RIB-модуля. Router просто делает своё навигационное дело, к нему вопросов нет.

В итоге мы получаем возможность строить дерево из RIB-блоков не так, как захочет Navigation Controller и левая пятка UIKit, а так, как того требует бизнес-логика. Это удобно, прозрачно и понятно, к тому же сохраняется чистота наследований: дети не знают ничего о своих родителях, а родители знают о своих детях.

Вот дурацкий пример приложения-будильника-с-погодой:

  • Корневой RIB может включить в себя RIB с флоу будильника либо RIB с флоу погоды

  • RIB будильника умеет добавлять новый будильник и редактировать существующий

  • а RIB погоды показывать мою погоду и новости о погоде вообще

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

Основная фича этого конструкта в том, что каждый RIB может управлять состоянием (навигироваться) только внутри своей ветки: к примеру из Alarm RIB нельзя попасть в News RIB. И он не принимает никаких решений, как только мы попали в Add new alarm RIB.

Проблема тут состоит в том, что не все состояния можно отслеживать добавлением или удалением RIB блоков из дерева. В этом случае Uber предлагают использовать неизменяемые (immutable) модели данных, которые при изменении (право на которое имеют только сетевые ответы, например) распространяют разницу вниз по DI графу.

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

Источники:

[1] uber/RIBs: Uber's cross-platform mobile architecture framework.

[2] iOS Architecture: Exploring RIBs. Uber mobile architecture in details | by Stan Ostrovskiy | The Startup

[3] Как мы внедряли архитектуру RIBs. Доклад Яндекс.Такси

CleanSwift

CleanSwift это хорошая альтернатива VIPER и ещё один способ переложить концепцию Чистой Архитектуры на iOS разработку.

В отличие от VIPER, в CleanSwift парадигма UIKit с центральным элементом в виде UIViewController остается нетронутой, что позволяет нам сделать чистую архитектуру, не выдумывая велосипеды, а естественно вырастая из MVC.

CleanSwift ориентируется на VIP-модули: это тройка View-Interactor-Presenter, которая общается друг с другом посредством специальных структур данных, привязанных к взаимодействию одной компоненты с другой.

Так, например, ViewController запрашивает данные для отображения у Интерактора с помощью структуры Request, Интерактор передает данные в Презентер через Response, а Презентер преобразует полученные данные в вид, удобный для отображения Контроллером Вида, то есть собирает ViewModel и отправляет в ViewController:

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

Так, благодаря Request и Response, Интерактор ничего не знает о том как устроен изнутри Контроллер Вида. Ему нужны работники в виде массивов со сквозным индексированием, или в виде множества объектов? Интерактору все равно! Он отдаст Презентеру данные в том виде, в котором Презентер сможет их понять (Response), а уже Презентер приведет их в вид, нужный Контроллеру Вида (ViewModel).

Таким образом с помощью этих трех структур мы задаем интерфейсы взаимодействия между Видом, Интерактором и Презентером. А сами взаимодействия формируют однонаправленный поток данных: легко читать, приятно дебажить, понятно как тестировать красота!

Добавив в такой VIP модуль дополнительно компоненты для внешних взаимодействий (с другими модулями и с хранилищами данных) получим следующий концепт:

  • Контроллер Вида: делает всякие UIViewController штуки

    • знает об Интеракторе

      • посылает ему действия пользователя

      • запрашивает у него данные (с помощью Request)

    • знает о Роутере

      • запрашивает у него логику навигации, когда необходимо перейти с текущего экрана на другой

    • знает о Презентере:

      • получает от него запросы на отображение данных (via Viewmodel)

  • Интерактор: осуществляет логику приложения, знает как действовать в ответ на действия пользователя

    • знает о Презентере:

      • посылает ему запросы на отображение данных (с помощью Response)

    • знает о Воркере:

      • запрашивает у него данные из Data Storage (Persistence или Network API)

    • влияет на Контроллер Вида:

      • получает от него запросы данных (via Request)

      • получает от него действия пользователя

  • Презентер:

    • влияет на Интерактор:

      • получает от него данные для отображения

    • влияет на Контроллер Вида

      • передает ему данные, полученные от Интерактора (Response) в виде, удобном для отображения (ViewModel)

  • Роутер: осуществляет общение с другими модулями

    • влияет на Контроллер Вида:

      • получает от его запросы на навигацию и/или передачу данных другим модулям (другим ViewController)

  • Воркер: прослойка между Интерактором и Data Access Layer

    • влияет на Интерактор

      • получает от него запросы на получение данных из Data Storage (Persistence или Network API)

Такая структура позволяет нам полностью разделить непосредственно работу с UI (ViewController), адаптацию данных для вывода (Presenter), логику приложения (Interactor), работу с хранилищами данных (Worker), а также навигацию и сообщение между модулями (Router).

В итоге получаем в определённой степени лаконичные и понятные MVC-новичку классы-сателлиты UIViewController, которые он сам же и создает, когда Роутер из другого модуля создаёт его, чтобы отобразить на экране.

Источники:

[1] The Clean Swift Handbook

[2] Общее представление об архитектуре Clean Swift / Хабр

[3] Clean Swift GitHub

И?

Глядя на паттерны чистой архитектуры очень легко обмануться и решить, что вот только эти паттерны ТРУ, а остальные так, самописное что-то на коленочке. Это, разумеется, не так. И надо всегда четко понимать каких именно целей вы хотите достичь, выбирая архитектурный паттерн для своей кодовой базы (возможно что в разных частях приложения вы и вовсе захотите использовать разные паттерны?)

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

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

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

P.S. В своем предыдущем посте я рассмотрела MV(X) архитектуры - http://personeltest.ru/aways/habr.com/ru/company/croc/blog/549590/

Подробнее..

Перевод Пишем CRUD-приложение на Go с помощью Mysql, GORM, Echo, Clean Architecture

24.11.2020 14:17:12 | Автор: admin

Начнем сначала


В этой статье будет сказ о том, как на Clean Architecture написать API с функциями CR(U)D, где в качестве БД взят Mysql, фреймворк Echo, ORMapper GORM.

Что делаем


API с функциями Create, Read, (Update), Delete. Обновление на самом деле реализовать особо не удалось, милости прошу попробовать самостоятельно.

Целевая аудитория


Те разработчики, которые хотят создать простой API после освоения Go.

Основное cодержание


Что такое Clean Architecture, и с чем его едят


Это мы можем подробно рассмотреть на следующей картинке:

image

Цель Clean Architecture это разделение сфер. Чтобы удачно добиться этого разделения, нужно всегда держать в голове зависимости каждого слоя на картинке с остальными. Разделение на слои улучшает читаемость кода и делает его устойчивым к изменениям.

На рисунке выше стрелка указывает снаружи внутрь круга это направление зависимости. Важно: зависимости направлены извне внутрь, не наоборот.

Другими словами, вы можете снаружи вызывать вещи, объявленные внутри, но вы не можете вызывать вещи, объявленные снаружи, находясь внутри.

Код данного приложения написан с учетом зависимостей, как и завещает Clean Architecture.

О функциях


Endpointы каждой функции следующие:

POST: /users
GET: /users
DELETE: /users/:id

Структура директорий


image

Взаимное отображение слоев архитектуры и директорий выглядит так:

  • domain Entities
  • usecase Use Cases
  • interface Controllers Presenters
  • infrasctructure External Interfaces


domain


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

image

src/domain/user.go
package domaintype User struct {    ID   int    `json:"id" gorm:"primary_key"`    Name string `json:"name"`}


Создаем struct User с id и именем и устанавливаем идентификатор в качестве первичного ключа.

Немного о json:id gorm:primary_key:
В json:id json отвечает за маппинг. В gorm:primary_key идет пометка на модели с помощью gorm.
Конечно, помимо primary_key вы можете использовать not null, unique, default и т.д.,

Полезная ссылка объявление моделей на GORM

infrastructure


Самый крайний, внешний слой. Здесь описывается часть, в которой приложение связано с внешним миром. В нашем случае, в этом слое объявляется связь с БД и Router.

image

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

src/infrastucture/sqlhandler.go
package infrastructureimport (    "gorm.io/driver/mysql"    "gorm.io/gorm"    "echoSample/src/interfaces/database")type SqlHandler struct {    db *gorm.DB}func NewSqlHandler() database.SqlHandler {    dsn := "root:password@tcp(127.0.0.1:3306)/go_sample?charset=utf8mb4&parseTime=True&loc=Local"    db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})    if err != nil {        panic(err.Error)    }    sqlHandler := new(SqlHandler)    sqlHandler.db = db    return sqlHandler}func (handler *SqlHandler) Create(obj interface{}) {    handler.db.Create(obj)}func (handler *SqlHandler) FindAll(obj interface{}) {    handler.db.Find(obj)}func (handler *SqlHandler) DeleteById(obj interface{}, id string) {    handler.db.Delete(obj, id)}


По поводу баз данных я оставлю ссылки на документацию: gorm.io

Далее идет маршрутизация.
В этом приложении я использую веб-фреймворк Echo. Фреймворк определяет Method и Path API
Ссылка на документацию: https://echo.labstack.com/

src/infrastructure/router.go
package infrastructureimport (    controllers "echoSample/src/interfaces/api"    "net/http"    "github.com/labstack/echo")func Init() {    // Echo instance    e := echo.New()    userController := controllers.NewUserController(NewSqlHandler())    e.GET("/users", func(c echo.Context) error {        users := userController.GetUser()         c.Bind(&users)         return c.JSON(http.StatusOK, users)    })    e.POST("/users", func(c echo.Context) error {        userController.Create(c)        return c.String(http.StatusOK, "created")    })    e.DELETE("/users/:id", func(c echo.Context) error {        id := c.Param("id")        userController.Delete(id)        return c.String(http.StatusOK, "deleted")    })    // Start server    e.Logger.Fatal(e.Start(":1323"))}


interfaces


Слой Controllers Presenters.
Вот здесь уже нужно вспомнить о зависимостях.

image

Нет проблем с вызовом со слоев domain и usecase, но нельзя вызвать слой infrastructure напрямую, поэтому объявим interface. (Получилось немного запутанно, но речь идет про интерфейс sqlHandler, определенный на слое infrastrucure)

src/interfaces/api/user_controller.go
package controllersimport (    "echoSample/src/domain"    "echoSample/src/interfaces/database"    "echoSample/src/usecase"    "github.com/labstack/echo")type UserController struct {    Interactor usecase.UserInteractor}func NewUserController(sqlHandler database.SqlHandler) *UserController {    return &UserController{        Interactor: usecase.UserInteractor{            UserRepository: &database.UserRepository{                SqlHandler: sqlHandler,            },        },    }}func (controller *UserController) Create(c echo.Context) {    u := domain.User{}    c.Bind(&u)    controller.Interactor.Add(u)    createdUsers := controller.Interactor.GetInfo()    c.JSON(201, createdUsers)    return}func (controller *UserController) GetUser() []domain.User {    res := controller.Interactor.GetInfo()    return res}func (controller *UserController) Delete(id string) {    controller.Interactor.Delete(id)}


В controller вызываем со слоёв domain и usecase, поэтому проблем нет.

src/interfaces/api/context.go
package controllerstype Context interface {    Param(string) string    Bind(interface{}) error    Status(int)    JSON(int, interface{})}


Связь с БД

src/interfaces/database/user_repository.go
package databasepackage databaseimport (    "echoSample/src/domain")type UserRepository struct {    SqlHandler}func (db *UserRepository) Store(u domain.User) {    db.Create(&u)}func (db *UserRepository) Select() []domain.User {    user := []domain.User{}    db.FindAll(&user)    return user}func (db *UserRepository) Delete(id string) {    user := []domain.User{}    db.DeleteById(&user, id)}


В repository вызывается sqlHandler, но он вызывается не напрямую со слоя infrastructure, а с помощью объявленного там же interface.
Это называется принципом инверсии зависимостей.

src/interfaces/db/sql_handler.go
package databasetype SqlHandler interface {    Create(object interface{})    FindAll(object interface{})    DeleteById(object interface{}, id string)}


Теперь вы можете вызывать процесс sql_handler.

usecase


Последний оставшийся слой, usecase.

image

src/usecase/user_interactor.go
package usecaseimport "echoSample/src/domain"type UserInteractor struct {    UserRepository UserRepository}func (interactor *UserInteractor) Add(u domain.User) {    interactor.UserRepository.Store(u)}func (interactor *UserInteractor) GetInfo() []domain.User {    return interactor.UserRepository.Select()}func (interactor *UserInteractor) Delete(id string) {    interactor.UserRepository.Delete(id)}


Опять же, нам нужно применить принцип инверсии зависимости, как и раньше. Поэтому определяем user_repository.go.

src/usecase/user_repository.go
package usecaseimport (    "echoSample/src/domain")type UserRepository interface {    Store(domain.User)    Select() []domain.User    Delete(id string)}


На этом реализация завершена.
После этого запустите mysql с docker-compose.yml, запустите сервер, и все должно работать.

docker-compose.ymlversion: "3.6"services:  db:    image: mysql:5.7    container_name: go_sample    volumes:      # настройки mysql      - ./mysql/conf:/etc/mysql/conf.d      - ./mysql/data:/var/lib/mysql    command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci    ports:      - 3306:3306    environment:      MYSQL_DATABASE: go_sample      MYSQL_ROOT_PASSWORD: password      MYSQL_USER: root      TZ: "Asia/Tokyo"
Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru