Современные подходы к разработке программного обеспечения

В октябре прошлого года я выступал на DevFest с докладом на тему, вынесенную в заголовок статьи. Само выступление доступно на Youtube, а ниже будет его текстовая расшифровка.

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

Сначала я хотел вам рассказать доклад по плану указанному ниже:

  • Проектирование систем — архитектура
  • Дизайн сервиса — организация кода внутри приложения
  • Хранение и обработка данных сервиса — организация данных
  • Quality Assurance — shift left и пирамида тестов
  • Инфраструктура — IaaC, *aaS
  • Доставка на продакшен — CI/CD pipelines
  • Безопасность
  • Данные для отчетов — подходы Data Lake и Data Mesh

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

Код

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

Код может превратиться в спагетти от

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

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

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

Например, в Тинькофф мне досталось несколько таких систем по наследству 4 года назад и мы с моими командами их успешно переписали. Самым ярким примером были 2 системы:

  • Система персонализаций и тестов версии 1.0, которая выросла из прототипа, где ее разработчик не любил лишних вызовов функций (и функции у него были по 500 строк) и не признавал понятие цикломатической сложности и важности ее ограничения
  • Система управления контентом версии 1.0, где несколько фронтендеров писали бекенд систему с API

Теперь подведем итоги разработки в лоб:

  • Всегда ли такой подход плох? — Нет. В прототипах и RnD он может быть хорош.
  • Что делать, если мы пишем не прототип? Или пишем много прототипов? — Нам нужна более умная разработка

Надо добавить как минимум стадию проектирования.

А что же такое проектирование?

По-факту, оно разделяется на две части: что делаем и как делаем. “Что делаем” относится к требованиям — надо очень внимательно посмотреть на требования, а конкретно на функциональные и нефункциональные требования.

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

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

Как делаем” относится больше к паттернам и фреймворкам, которые можно использовать для ускорения и улучшения структуры приложений. Шаблон проектирования или паттерн — это повторяемая архитектурная конструкция, представляющая собой решение проблемы проектирования в рамках некоторого часто возникающего контекста. Фре́ймворк — каркас, определяющий структуру программной системы и облегчающий разработку и объединение разных компонентов большого программного проекта.

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

  • UI-слой
  • слой бизнес-логики
  • слой хранения данных

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

Есть разные варианты организации архитектуры слоеной системы, но мне персонально нравится подход Uncle Bob, который он назвал чистой архитектурой. У него вообще пунктик на слове “Clean”, что подтверждают книги ««Clean Code», «Clean Architecture», «Clean Agile». Но если возвращаться к Чистой Архитектуре, то в основе лежит Правило Зависимостей

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

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

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

Данные

Эволюция подходов изображена на рисунке ниже.

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

  • совместный доступ к данным
  • сложные выборки данных, когда паттерн выборки меняется

Эти моменты решаются при использовании реляционных баз данных, которые предложил доктор Эдгар Кодд в 1970, когда он работал в IBM. Для работы с реляционными БД применяют реляционные системы управления базами данных (для краткости просто базами данных). Кодд предложил 12 правил, которым должна удовлетворять база данных, чтобы считаться реляционной. Интересно, что большинство реляционных баз данных не соблюдают часть этих правил:)

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

Также обычно реляционные базы данных дают гарантии ACID

  • Atomicity — Атомарность
  • Consistency — Согласованность
  • Isolation — Изолированность
  • Durability — Стойкость

Реляционные базы данных подходили не ко всем нагрузкам. Их хорошо было использовать для OLTP нагрузок (Online Transaction Processing). А вот для OLAP нагрузок (Online Analytical Processing) нужны отдельные решения. Саму концепцию предложил тоже Кодд в 1993 году. Основная причина использования OLAP для обработки запросов — скорость. В OLAP решении важна подготовка суммарной (агрегированной) информации на основе больших массивов данных, структурированных по многомерному принципу. OLAP-структура, созданная из рабочих данных, называется OLAP-куб.

Дальнейшим развитием файловых систем стало появление объектных хранилищ, первым популярным среди которых стало Amazon S3 (Simple Storage Service), предоставляющая возможность для хранения и получения любого объёма данных, в любое время из любой точки сети, так называемый файловый хостинг. Впервые появилась в марте 2006 года в США и в ноябре 2007 года в Европе.

В начале 2000-х годов объем информации, которую нужно было обрабатывать перестал помещаться в одно хранилище и потребовалась возможность параллельной обработки на группе машин. Google представил модель распределенных вычислений MapReduce, который использовался для параллельных вычислений над очень большими, вплоть до нескольких петабайт, наборами данных в компьютерных кластерах. Apache Hadoop — это open source проект, который был разработан на основе идеи MapReduce для распределенных вычислений. Он нашел своих последователей и стал крайне популярным в своем классе решений для обработки BigData. Кстати, проект был назван в честь игрушечного слонёнка ребёнка основателя проекта.

В 2009 году появился термин NoSQL, который был придуман Эриком Эвансом, когда Джоан Оскарсон из Last.fm хотел организовать мероприятие для обсуждения распределенных баз данных с открытым исходным кодом. Сейчас он применяется к системам, в которых делается попытка решить проблемы масштабируемости и доступности за счёт полного или частичного отказа от требований атомарности и согласованности данных. В итоге, NoSQL базы уже не дают гарантии ACID, вместо этого они говорят о гарантиях BASE

  • Basically Available
  • Soft state
  • Eventual consistency

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

  • согласованность данных (англ. consistency) — во всех вычислительных узлах в один момент времени данные не противоречат друг другу
  • доступность (англ. availability) — любой запрос к распределённой системе завершается корректным откликом, однако без гарантии, что ответы всех узлов системы совпадают
  • устойчивость к разделению (англ. partition tolerance) — расщепление распределённой системы на несколько изолированных секций не приводит к некорректности отклика от каждой из секций

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

Архитектура

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

Следующим шагом развития была сервис ориентированная архитектура (SOA), определение которого звучало неплохо

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

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

Программные комплексы, разработанные в соответствии с сервис-ориентированной архитектурой, обычно реализуются как набор веб-служб, взаимодействующих по протоколу SOAP, но существуют и другие реализации (например, на базе jini, CORBA, на основе REST).

Данная архитектура стала популярной в конце 1990-х годов и начале 2000-х. Но она была достаточно сложна и из-за этого обрела достаточную популярность только в “кровавом enterprise”. В остальных местах ее использование зачастую было over engineering.

В 2010-х годах стала популярна микросервисная архитектура — вариант сервис-ориентированной архитектуры программного обеспечения, направленный на взаимодействие насколько это возможно небольших, слабо связанных и легко изменяемых модулей — микросервисов, получивший распространение в середине 2010-х годов в связи с развитием практик гибкой разработки и DevOps. Если раньше сервисы могли быть достаточно большими, то в микросервисной архитектуре есть акцент на небольших компонентах, которые общаются по сети с использованием экономичных протоколов json, protobuf, thrift.

Для правильного проектирования границ сервисов можно использовать Предметно-ориентированное проектирование (Domain Driven Design) — набор принципов и схем, направленных на создание оптимальных систем объектов. Сводится к созданию программных абстракций, которые называются моделями предметных областей. В эти модели входит бизнес-логика, устанавливающая связь между реальными условиями области применения продукта и кодом. Подробнее про DDD можно почитать в моем обзоре книги “What Is Domain-Driven Design?”

Также есть хорошая методология 12 factor app от Heroku для создания приложений, которые

Используют декларативный формат для описания процесса установки и настройки, что сводит к минимуму затраты времени и ресурсов для новых разработчиков, подключённых к проекту;

Имеют соглашение с операционной системой, предполагающее максимальную переносимость между средами выполнения;

Подходят для развёртывания на современных облачных платформах, устраняя необходимость в серверах и системном администрировании;

Сводят к минимуму расхождения между средой разработки и средой выполнения, что позволяет использовать непрерывное развёртывание (continuous deployment) для максимальной гибкости;

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

Современные микросервисные приложения реализуются в рамках cloud native подхода, который определяется так

CNCF Cloud Native Definition v1.0

Approved by TOC: 2018–06–11

Cloud native technologies empower organizations to build and run scalable applications in modern, dynamic environments such as public, private, and hybrid clouds. Containers, service meshes, microservices, immutable infrastructure, and declarative APIs exemplify this approach.

These techniques enable loosely coupled systems that are resilient, manageable, and observable. Combined with robust automation, they allow engineers to make high-impact changes frequently and predictably with minimal toil.

The Cloud Native Computing Foundation seeks to drive adoption of this paradigm by fostering and sustaining an ecosystem of open source, vendor-neutral projects. We democratize state-of-the-art patterns to make these innovations accessible for everyone.

Для некоторых нагрузок можно использовать следующий шаг — serverless architecture или FaaS (Function as a Service), когда приложение хостится третьей стороной в виде сервиса, который обрабатывает запросы клиентов или определенные события. В итоге, разработчикам не требуется думать про сервера, на которых развернуто их решение.

Кстати, в конце 2019 года я рассказывал на конференции ArchDays как эволюционировала архитектура Tinkoff.ru за 3 года. Вот sequence диаграмма распределенной системы из того выступления.

Вот мы и поняли как сделать правильное решение, а теперь настало время разобраться с процессом его поставки.

Deploy

Ниже представлена эволюция подходов доставки продакшена на прод.

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

  • Capistrano, написанный на Ruby
  • Fabric, написанный на Python

Ну и финальный подход с использованием полноценных CI/CD систем типа Gitlab CI, Bamboo, TeamCity, Jenkins. Часто для работы с CI/CD привлекаются devops-инженеры, которые помогают с настройкой CI/CD и runtime инфраструктуры.

Инфраструктура

Эволюцию инфраструктуры можно увидеть на рисунке ниже

Все начиналось с bare metal, то есть железных серверов. Потом появилась виртуализация, которая позволила нарезать большие железные машинки на большое количество виртуальных машин. В конце 1990х годов компания VMware вышла с виртуализацией x86 машинок и это открыло ящик Пандоры.

После этого AWS запустил AWS Web Services, которые изначально включали EC2 (Elastic Compute Cloud) и S3 (Simple Storage Service). Про S3 мы уже говорили — это инструмент для хранения объектов. А вот compute cloud позволял получить виртуальные машины через API. Это и стало началом подхода Инфраструктура как услуга (англ. Infrastructure-as-a-Service; IaaS). Это модель обслуживания в облаках, по которой потребителям предоставляются по подписке фундаментальные информационно-технологические ресурсы — виртуальные серверы с заданной вычислительной мощностью, операционной системой (чаще всего — предустановленной провайдером из шаблона) и доступом к сети.

Дальше микросервисный подход вошел в свою силу и стало достаточно дорого и неудобно выделять по виртуалке на каждый сервис. Сервисы стали заворачивать в контейнеры, а оркестрировать эти контейнеры стали при помощи Kubernetes, который зачастую нужен клиентам в виде managed сервиса. Так в облаках появился K8s as a Service.

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

Кстати, вот как выглядит основной инструментарий, который используем мы в Tinkoff (где стрелочкой отмечен постепенный переезд с Teamcity на Gitlab CI).

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

Качество

И для обеспечения качество в процесс добавляют тестирование. Обычно делают это так, как на рисунке ниже, а именно перед этапом деплоя.

Но, в итоге, получаем просто анти паттерн, а именно Quality Control.

И что же не так с тестированием в конце процесса — это просто долго, дорого и плохо автоматизируемо. И что делать, если мы хотим высокого качества и быстрых релизов? Нам нужно сдвинуть тестирование влево по процессу (shift left). Это значит, что qa-инженер должен начинать работать с этапа сбора и анализа требований — на этом этапе он может написать test cases, которые будут проверять работоспособность задачи. На этапе проектирования он может подсказать насколько спроектированное решение будет тестируемым. На этапе разработки он сможет автоматизировать часть проверок, описанных раньше test cases и в финале проверить то, что не стали автоматизировать руками.

Кажется, что мы закончили обсуждать процесс? Но нет … мы кое-что забыли.

Безопасность

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

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

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

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

Надежность

Собственно тут нам на помощь приходит популяризированный Google подход с их SRE (Site Reliability Engineering). Вот как они описывают его на сайте sre.google

SRE is what you get when you treat operations as if it’s a software problem. Our mission is to protect, provide for, and progress the software and systems behind all of Google’s public services — Google Search, Ads, Gmail, Android, YouTube, and App Engine, to name just a few — with an ever-watchful eye on their availability, latency, performance, and capacity.

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

DataOps

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

Итоги

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

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

Я поделился своим видением этого процесса, привел примеры из нашей практики в Tinkoff, а также ссылки на тематические плейлисты, которые я собрал для вас на обучающей платформе O’Reilly, которую я вам очень рекомендую для самообучения и дальнейшего развития в профессиональном плане

Рекомендованное чтение

  1. По проектированию и архитектуре можно изучить книги из моей статьи “Как прокачаться в проектировании программного обеспечения — список книг
  2. Плейлист из книг раздела Код
  3. Плейлист из книг раздела Данные
  4. Плейлист для раздела Devops
  5. Плейлист для раздела QA
  6. Плейлист для раздела Security
  7. Плейлист для раздела SRE
  8. Плейлист для раздела DataOps

Director of digital ecosystem development department at Tinkoff. Bachelor at applied math, Master at system analysis, Postgraduate studies at economics.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store