DevOps в разработке: автоматизация написания кода веб-приложений

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.
Доброго времени суток, уважаемые Хабражители!

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

Мне бы хотелось исправить это маленькое недоразумение. DevOps в разработку может прийти через формализацию кодовой базы, например, при написании GUI для REST API.

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

Надеюсь данный материал будет вам интересен и полезен.

Ну что ж, начнем!

Предыстория


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

Процесс добавление новой фичи на старой архитектуре

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

  1. На back-end’e: создать модель для новой сущности (хуки), описать поля данной модели, описать всю логику действий (actions), которые данные модель может выполнять и т. д.
  2. На front-end’e: создать класс представления, соответствующий новой модели в API, вручную описать все поля, которые у данной модели есть, добавить все типы action’ов, которые данное представление может запустить и т. д.

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

Допустим, нам нужно будет поменять тип поля “name” cо “string” на “textarea”. Для этого нам нужно будет внести данную правку в код модели на сервере, а затем сделать аналогичные изменения в коде представления на клиенте.

Не слишком ли всё сложно?

Ранее мы мирились с данным фактом, поскольку многие приложения были не очень большими и подход с “дублированием” кода на сервере и на клиенте имел место быть. Но в тот самый летний день, перед началом внедрения новой фичи, внутри нас что-то щелкнуло, и мы поняли, что дальше так работать нельзя. Текущий подход являлся весьма неразумным и требовал больших временных и трудовых затрат. К тому же, “дублирование” кода на back-end’е и на front-end’e могло в будущем привести к неожиданным багам: разработчики могли бы внести изменения на сервере и забыть внести аналогичные изменения на клиенте, и тогда все пошло бы не по плану.

Как избежать дублирования кода? Поиск решения


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

Мы задали сами себе вопрос: «Можем ли мы прямо сейчас избежать дублирования изменений в представлении модели на front-end’e, после любого изменения в ее структуре на back-end’e?»

Мы подумали и ответили: «Нет, не можем».

Тогда мы задали себе еще один вопрос: «Окей, в чем тогда заключается причина подобного дублирования кода?»

И тут нас осенило: проблема, по сути, в том, что наш front-end не получает данных о текущей структуре API. Front-end ничего не знает о моделях, существующих в API, до тех пор, пока мы сами ему об этом не сообщим.

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

  • Front-end получал из API не только данные моделей, но и структуру этих моделей;
  • Front-end динамически формировал представления на основе структуры моделей;
  • Любое изменение в структуре API автоматически отображалось на front-end’e.

Внедрение новой фичи будет занимать гораздо меньше времени, поскольку будет требовать внесения изменений только на стороне back-end’a, а front-end автоматически все подхватит и представит пользователю должным образом.

Универсальность новой архитектуры


И тогда, мы решили подумать еще несколько шире: является ли новая архитектура пригодной только для нашего текущего приложения, или мы можем использовать ее где-то еще?

Общие для многих веб-приложений фичи

Ведь, так или иначе, почти все приложения имеют часть схожего функционала:

  • почти по всех приложениях есть пользователи, и в связи с этим необходимо иметь функционал связанный с регистрацией и авторизацией пользователя;
  • почти во всех приложениях есть несколько типов представлений: есть представление для просмотра списка объектов какой-то модели, есть представление для просмотра детальной записи одного, отдельного взятого, объекта модели;
  • почти у всех моделей есть схожие по типу атрибуты: строковые данные, числа и т. д., и в связи с этим, нужно уметь работать с ними как на back-end’е, так и на front-end’е.

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

Таким образом, в ходе долгих рассуждений у нас появилась идея о создании VSTUtils – фреймворка, который бы:

  1. Содержал в себе базовый, максимально схожий для большинства приложений, функционал;
  2. Позволял бы генерировать front-end на лету, основываясь на структуре API.

Как подружить back-end и front-end?


Ну что ж, надо делать, подумали мы. Некий back-end у нас уже был, некий front-end тоже, но ни на сервере, ни на клиенте не было инструмента, который мог бы сообщить или получить данные о структуре API.

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

И мы подумали, что, по идее, при инициализации приложения на клиенте front-end может получать от API данный JSON и на его основе строить все необходимые представления. Остается только научить наш front-end все это делать.

И спустя некоторое время мы его таки научили.

Версия 1.0 – что по итогу вышло


Архитектура фрейворка VSTUtils первых версий состояла из 3 условных частей и выглядела примерно так:

  1. Back-end:
    • Django и Python – вся логика связанная с моделями. На основе базовой модели Django Model мы создали несколько классов основных моделей VSTUtils. Все actions, которые могут выполнять данные модели мы реализовали с помощью Python;
    • Django REST Framework – генерация REST API. На основе описания моделей формируется REST API, благодаря которому происходит общение сервера и клиента;
  2. Прослойка между back-end’ом и front-end’ом:
    • OpenAPI – генерация JSON’а с описанием структуры API. После того, как на back-end’е были описаны все модели, для них создаются views. Добавление каждой из views вносит необходимую информацию в итоговый JSON:
      Пример JSON'a – схема OpenAPI
      {
          // объект, хранящий в себе пары (ключ, значение),
          // где ключ - имя модели,
          // значение - объект с описанием полей модели.
          definitions: {
              // описание структуры модели Hook.
              Hook: {
                  // объект, хранящий в себе пары (ключ, зачение),
                  // где ключ - имя поля модели,
                  // значение - объект с описанием свойств данного поля (заголовок, тип поля и т.д.).
                  properties: {
                      id: {
                          title: "Id",
                              type: "integer",
                              readOnly: true,
                      },
                      name: {
                          title: "Name",
                              type: "string",
                              minLength:1,
                              maxLength: 512,
                      },
                      type: {
                          title: "Type",
                              type: "string",
                      enum: ["HTTP","SCRIPT"],
                      },
                      when: {
                          title: "When",
                              type: "string",
                      enum: ["on_object_add","on_object_upd","on_object_del"],
                      },
                      enable: {
                          title:"Enable",
                              type:"boolean",
                      },
                      recipients: {
                          title: "Recipients",
                              type: "string",
                              minLength: 1,
                      }
                  },
                  // массив, хранящий в себе имена полей, являющихся обязательными для заполнения.
                  required: ["type","recipients"],
              }
          },
          // объект, хранящий в себе пары (ключ, значение),
          // где ключ - путь предсталения (шаблонный URL),
          // значение - объект с описанием свойств представления.
          paths: {
              // описание структуры представлений по пути '/hook/'.
              '/hook/': {
                  // схема представления для get запроса по пути /hook/.
                  // схема представления, соответствующей странице просмотра списка объектов модели Hook.
                  get: {
                      operationId: "hook_list",
                          description: "Return all hooks.",
                          // массив, хранящий в себе объекты со свойствами фильтров, доступных для данного списка объектов.
                          parameters: [
                          {
                              name: "id",
                              in: "query",
                              description: "A unique integer value (or comma separated list) identifying this instance.",
                              required: false,
                              type: "string",
                          },
                          {
                              name: "name",
                              in: "query",
                              description: "A name string value (or comma separated list) of instance.",
                              required: false,
                              type: "string",
                          },
                          {
                              name: "type",
                              in: "query",
                              description: "Instance type.",
                              required: false,
                              type: "string",
                          },
                      ],
                          // объект, хранящий в себе пары (ключ, значение),
                          // где ключ - код ответа сервера;
                          // значение - схема ответа сервера.
                          responses: {
                          200: {
                              description: "Action accepted.",
                                  schema: {
                                  properties: {
                                      results: {
                                          type: "array",
                                              items: {
                                              // ссылка на модель, данные которой пришли в ответе от сервера.
                                              $ref: "#/definitions/Hook",
                                          },
                                      },
                                  },
                              },
                          },
                          400: {
                              description: "Validation error or some data error.",
                                  schema: {
                                  $ref: "#/definitions/Error",
                              },
                          },
                          401: {
                              // ...
                          },
                          403: {
                              // ...
                          },
                          404: {
                              // ...
                          },
                      },
                      tags: ["hook"],
                  },
                  // схема представления для post запроса по пути /hook/.
                  // схема представления, соответствующей странице создания нового объекта модели Hook.
                  post: {
                      operationId: "hook_add",
                          description: "Create a new hook.",
                          parameters: [
                          {
                              name: "data",
                              in: "body",
                              required: true,
                              schema: {
                                  $ref: "#/definitions/Hook",
                              },
                          },
                      ],
                          responses: {
                          201: {
                              description: "Action accepted.",
                                  schema: {
                                  $ref: "#/definitions/Hook",
                              },
                          },
                          400: {
                              description: "Validation error or some data error.",
                                  schema: {
                                  $ref: "#/definitions/Error",
                              },
                          },
                          401: {
                              // ...
                          },
                          403: {
                              // ...
                          },
                          404: {
                              // ...
                          },
                      },
                      tags: ["hook"],
                  },
              }
          }
      }
  3. Front-end:
    • JavaScript – механизм, парсящий схему OpenAPI и генерирующий представления. Данный механизм запускается один раз, при инициализации приложения на клиенте. Отправив запрос к API, он получает в ответ запрашиваемый JSON с описанием структуры API и, анализируя его, создает все необходимые JS объекты, содержащие параметры представлений моделей. Данный запрос к API довольно увесистый, поэтому мы его кэшируем и запрашиваем повторно только при обновлении версии приложения;
    • JavaScript SPA libs – рендеринг представлений и маршрутизация между ними. Данные библиотеки были написаны одним из наших front-end разработчиков. При обращении пользователя к той или иной странице, механизм рендеринга производит отрисовку страницы, на основе параметров сохраненных ранее в JS объектах представлений.

Таким образом, что мы имеем: у нас есть back-end, на котором описана вся логика, связанная с моделями. Затем в игру вступает OpenAPI, который на основе описания моделей формирует JSON с описанием структуры API. Далее эстафетная палочка передается клиенту, который анализируя сформированный OpenAPI JSON автоматически генерирует веб-интерфейс.

Внедрение фичи в приложение на новой архитектуре – как это работает


Помните задачу про добавление пользовательских хуков? Вот как бы мы ее реализовали в приложении на базе VSTUtils:

Процесс добавление новой фичи на новой архитектуре

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

  1. На back-end’e: берем и наследуемся от самого подходящего класса в VSTUtils, добавляем новый функционал, характерный для новой модели;
  2. На front-end’e:
    • если представление для данной модели ничем не отличается от базового представления VSTUtils, то ничего не делаем, все автоматически отображается должным образом;
    • если нужно как-то изменить поведение представления, с помощью механизма сигналов декларативно расширяем либо полностью изменяем базовое поведение представления.

В итоге, у нас получилось довольно неплохое решение, мы добились своей цели, наш front-end стал автогенерируемым. Процесс внедрения новых фич в существующие проекты заметно ускорился: релизы стали выходить раз в 2 недели, тогда как ранее мы выпускали релизы раз в 2-3 месяца с гораздо меньшим количеством новых фич. Хотелось бы заметить, что команда разработчиков осталась прежней, такие плоды нам дала именно новая архитектура приложения.

Версия 1.0 – перемен требуют наши сердца


Но, как известно, нет предела совершенству, и VSTUtils не стал исключением.

Не смотря на то, что нам удалось автоматизировать формирование front-end’а, получилось не прям то решение, которое мы изначально хотели.

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

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

И поскольку в нашей компании мы придерживаемся DevOps подхода и стараемся наш код максимально стандартизировать и формализовать, то в феврале этого года мы решили провести глобальный рефакторинг front-end’a фреймворка VSTUtils. У нас было несколько задач:

  • формировать на front-end’е не только классы представлений, но и классы моделей – мы поняли, что было бы правильней отделять данные (и их структуру) от их представления. К тому же, наличие нескольких абстракций в виде представления и модели значительно бы облегчило добавление перегрузок базового функционала в проектах на базе VSTUtils;
  • использовать для рендеринга и маршрутизации протестированный фреймворк, с большим сообществом (Angular, React, Vue) – это позволит нам отдать всю головную боль с поддержкой кода, связанного с рендерингом и маршрутизацией внутри нашего приложения.

Рефакторинг – выбор JS фреймворка


Среди наиболее популярных JS фреймворков: Angular, React, Vue, наш выбор пал на Vue поскольку:

  • Кодовая база Vue весит меньше, чем у React и Angular;

    Сравнительная таблица размеров фреймворков Gzipped версии
    Фреймворк Размер, kb
    Angular 2 111
    Angular 2 + RX 143
    Angular 1.4.5 51
    React 0.14.5 + React DOM 40
    React 0.14.5 + React DOM + Redux 42
    React 15.3.0 + React DOM 43
    Vue 2.4.2 21
  • процесс рендеринга страницы у Vue занимает меньше времени, чем у React и Angular;
    сравнение скорости рендеринга страниц разными javascript фреймворками относительно чистого javascript
  • Порог входа в Vue гораздо ниже, чем в React и Angular;
  • Нативно понятный синтаксис шаблонов;
  • Шикарная, подробнейшая документация, доступная на нескольких языках, в том числе и на русском;
  • Развитая экосистема, предоставляющая, помимо базовой библиотеки Vue, библиотеки для маршрутизации и для создания реактивного хранилища данных.

Версия 2.0 – результат рефакторинга front-end’а


Процесс глобального рефакторинга front-end’а VSTUtils занял около 4 месяцев и вот что у нас в итоге вышло:

новая архитектура fron-end VSTUtils

Front-end фреймворка VSTUtils по-прежнему состоит из двух больших блоков: первый занимается парсингом схемы OpenAPI, второй – рендерингом представлений и маршрутизацией между ними, но оба этих блоков перенесли ряд существенных изменений.

Был полностью переписан механизм, парсящий схему OpenAPI. Изменился подход к парсингу этой схемы. Мы постарались сделать архитектуру front-end’а максимально похожей на архитектуру back-end’a. Теперь на стороне клиента у нас есть не просто единая абстракция в виде представлений, теперь у нас есть еще абстракции в виде моделей и QuerySet’ов:

  • объекты класса Model и его потомков – объекты, соответствующие абстракции Django Models на стороне сервера. Объекты данного типа содержат в себе данные о структуре модели (имя модели, поля модели и т. д.);
  • объекты класса QuerySet и его потомков – объекты, соответствующие абстракции Django QuerySets на стороне сервера. Объекты данного типа, содержат в себе методы, позволяющие выполнять запросы к API (добавление, изменение, получение, удаление данных объектов моделей);
  • объекты класса View – объекты, хранящие в себе данные о том, каким образом нужно представить модель на той или иной странице, какой шаблон использовать для «рендеринга» страницы, на какие другие представления моделей может ссылаться данная страница и т. п.

Блок, отвечающий за рендеринг и маршрутизацию, тоже заметно преобразился. Мы отказались от самописных JS SPA библиотек в пользу фреймворка Vue.js. Мы разработали собственные Vue компоненты, из которых строятся все страницы нашего веб-приложения. Маршрутизация между представлениями осуществляется с помощью библиотеки vue-router, а в качестве реактивного хранилища состояния приложения мы используем vuex.

Хотелось бы также отметить, что на стороне front-end’а реализация классов Model, QuerySet и View не зависит от средств реализации рендеринга и маршрутизации, то есть если мы вдруг захотим перейти от Vue к какому-то другому фреймворку, например на React или на что-то новое, то все что нам нужно будет сделать, это переписать компоненты Vue на компоненты нового фреймворка, переписать роутер, хранилище, и все – фреймворк VSTUtils снова будет работоспособен. Реализация классов Model, QuerySet и View останется прежней, поскольку никак не зависит от Vue.js. Мы считаем, что это является весьма неплохим подспорьем для возможных будущих изменений.

Подведем итоги


Таким образом, нежелание писать “дублирующий” код вылилось в задачу по автоматизации формирования front-end’a веб-приложния, которая была решена с помощью создания фреймворка VSTUtils. Нам удалось построить архитектуру веб-приложения так, что back-end и front-end гармонично дополняют друг друга и любое изменение в структуре API автоматически подхватывается и отображается должным образом на клиенте.

Преимущества, которые мы получили от формализации архитектуры веб-приложения:

  • Релизы приложений, работающих на базе VSTUtils стали выходить в 2 раза чаще. Это связанно с тем, что теперь для внедрения новой фичи, зачастую, нам необходимо добавить код только на back-end’e, front-end автоматически сформируется – это значительно экономит время;
  • Упростили обновление базового функционала. Так как теперь весь базовый функционал собран в одном фреймворке, то для того, чтобы обновить какие-то важные зависимости или внести улучшение в базовый функционал, нам необходимо внести правки только в одном месте – в кодовой базе VSTUtils. При обновлении версии VSTUtils в дочерних проектах все нововведения автоматически подхватятся;
  • Поиск новых сотрудников стал легче. Согласитесь, гораздо проще найти разработчика под формализованный стек технологий (Django, Vue), чем искать человека, который согласится работать с неизвестным самописом. Результаты поиска разработчиков, упомянувших в своем резюме Django или Vue на HeadHunter’е (по всем регионам):
    • Django – найдено 3 454 резюме у 3 136 соискателей;
    • Vue – найдено 4 092 резюме у 3 747соискателей.

К недостаткам подобной формализации архитектуры веб-приложения можно отнести следующее:

  • За счет парсинга схемы OpenAPI инициализация приложения на клиенте занимает чуть больше времени, чем ранее (примерно на 20-30 миллисекунд дольше);
  • Неважная поисковая индексация. Дело в том, что в данный момент мы никак не задействуем серверный рендеринг в рамках VSTUtils, и весь контент приложения формируется в итоговом виде уже на клиенте. Но нашим проектам, зачастую высокая поисковая выдача не нужна и для нас это не так критично.

На этом мой рассказ подходит к концу, спасибо за внимание!

Полезные ссылки


  • Спецификация OpenAPI;
  • Репозиторий VSTUtils;
  • Пример проекта, созданного на базе VSTUtils.
Источник: https://habr.com/ru/post/456146/


Интересные статьи

Интересные статьи

Привет, Хабр! Представляю вашему вниманию перевод статьи «Dark code-style academy: line breaks, spacing, and indentation» автора zhikin2207 Привет, народ! Позвольте мне продол...
Есть неожиданно много людей, считающих, что писать код можно как угодно, а все эти правила и рекомендации просто занудство. Что же, у каждого каждого человека есть право на своё мнение. Одн...
«Чрезмерная автоматизация была ошибкой.  Если быть точным — моей ошибкой.  Люди недооценены.» Илон Маск Эта статья может выглядеть, как выступление пчёл против мёда. Действительно, странно:...
Изначально и в работе, и в курсах мы делали упор на «как» и избегали «зачем». Мы рассказывали про настройку Kubernetes и молчали про то, где нужен K8s. Если клиент хотел кластер, мы делали ему кл...
Снова здравствуйте. Перевод следующего материала подготовлен специально для студентов курса «Разработчик C++», занятия по которому стартуют уже 27 июня. Библиотека Ranges была принята в C+...