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

Refactoring

Трёхпроходный алгоритм рефакторинга Front End

22.04.2021 02:17:37 | Автор: admin

Попался мне достаточно крупного размера проект написанный на React + Typescript.
Покопался в коде. Всё круто, контейнеры, компоненты, типы везде стоят, линтер настроен, styled-components, даже storybook есть и некий react-query.
Ну просто счастье, а не проект!

Сажусь делать простую таску какую-то страничку собрать из компонентов.
Пишу в коде But и IDE мне предлагает 16 Button компонентов.

Блииин

Ладно, из компонентов достал component/Button (з.ы. он там единственный)

Дальше Inp ну, вы поняли. 23 штуки этих Input по всему проекту.

Ладно, думаю, открываю storybook, решил оттуда копировать всё. А там компоненты только простенькие, типо кнопок, H1, H3. И никаких тебе компонентов с состоянием ни формочки, ни таблички со значениями.


Счастью моему нет предела.

Копаю дальше. Оказывается, в контейнерах лежат не только контейнеры (собственно, у меня всегда вопрос был к этому слову, скорее люблю название page или subpage/section), но и непосредственно компоненты, относящиеся к этой странице.
По классике жанра в reusable components вынесли не всё и 4-5 крупных компонентов просто скопированы в разные контейнеры (папки контейнеров), где используются.

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

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

И придумался мне Трёхпроходный алгоритм рефакторинга Front End-а (привет Кнут-у).

План такой:

1) Выписываем основные косяки в джиру/асану/трелло/лайниар/что угодно!

2) Вставляем по всему проекту комментарии // TODO:ID description (ID берём из тулы пункта 1)

3) В свободное время делаем то, что написано в TODO

Чем это хорошо?
Тем, что TODO-шки вы сможете расставить по всему коду за 2-3 дня, если уже знакомы с проектом (в моём случае время можно выдернуть из 1-2х спринтов спокойно).

А видя TODO вы:

Во-первых, сможете достаточно спокойно объяснить заказчику, как много у вас косяков в коде (делаем CTRL + SHIFT + F и ищем // TODO:ID, показываем ему сколько раз встречается какой-то косяк, например 23 инпута одинаковых)

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

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

А теперь подробнее.

Шаг 1.

У меня получились вот такие вот задачи в Linear (типо джира):

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

Пояснения, которые считаю нужными:
Add stories имеется в виду в storybook stories для компонента добавить.
Move components into components фактические компоненты (reusable) были в контейнерах. Имеется в виду перенести их в компоненты.

Шаг 2.

Получились такие прикольные styled-component-ы (свернул лишнее)

Аналогично добавил // TODO над компонентами, которые нужно перенести из контейнеров в компоненты (сложносочинённое предложение).

И всякие остальные комментарии, думаю сами дорисуете картинку

Как не закалупаться с копированием TODO?

Я решил сделать всё в один проход по проекту.
От папки компонентов к папке containers.

Открыл Linear. Скопировал все ID.
Выписал их в документ, добавил описание

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

А дальше просто бежал по всем компонентам и контейнерам, утилитам и стилям и вставлял TODO коммент за комментом из того самого Windows + V буфера обмена.

Да, пришлось подумать, что куда переносить.
Но пробежаться не сильно сложно и долго оказалось. В некоторых местах даже глобальной заменой пользовался (Ctrl + Shift + V).

Например, для
`const Button =`

На

`// TODO: TFR-45:Button
const Button =`

Экономия!

Шаг 3.

Внимательные читатели могли заметили разницу в записях.
TFR-44 и TFR-45 в отличие от остальных TODO имеют после своего ID двоеточие и какое-то слово. Это слово - имя папки компонентов, куда их следует перенести.

Вам, наверное, интересно, зачем я это добавил?
А всё просто. Я решил автоматизировать перенос моих 16 Button и 23 Input с помощью node.js и простой регулярки.

Принцип простой:

1. Весь кусок от // TODO ID до }; я перенесу в указанную папку в файл с именем Folder(от куда)+ComponentName.

То есть весь styled-component с его повторяющимся именем и стилями

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

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

Для облегчения жизни написал вот такой скрипт: https://github.com/DrBoria/component-mover

Конкретно у меня все повторяющиеся компоненты были styled-components.
То есть заканчивались на };.
Поэтому применяю соответствующую регулярку.

Если вам не подходит допишите свою, либо просто сделайте // TODO end коммент в конце куска, который хотите вынести и будет у вас регулярка с // TODO ID по // TODO end.
Да, чуть больше копи пасты.
Но за-то можете приступать прямо сейчас, не тратя время на подготовку.

А главное это можно сделать за 1 рабочий день.
Т.е. есть у вас свободный день в спринте? Хотите вы сделать себе жизнь проще?
Запускаете регулярку.
Рефакторите строго один компонент (кнопку например).
И вуаля!
Вы теперь знаете, как работать с кнопками в вашем проекте.
А остальные TODO-шки висят и ждут лучших времён в вашей жизни.

Подводя итоги, что мы имеем с этим алгоритмом.

1) Вы и ваш заказчик осведомлены о величине технического долга в проекте. Он абсолютно конкретно измерим.

2) Вам стало куда проще понимать, сколько времени вам понадобится на рефакторинг каждой части проекта (возможно кому-то понятнее и сколько всего нужно времени)

3) У вас есть возможность автоматизации рефакторинга (как со скриптом выше, так и с вашими самописными)

Как говорит мой отец одни плюсы, как на кладбище.

Подробнее..

Фильтры действий, или Как просто улучшить читаемость кода

25.01.2021 16:17:27 | Автор: admin

Введение


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

Роль фильтров в процессе обработки запроса


Сначала обсудим сами фильтры: для чего же они нужны? Фильтры позволяют выполнять определённые действия на различных стадиях обработки запроса в ASP.NET Core. Существуют следующие встроенные фильтры:

  • Фильтры авторизации (Authorization filters) выполняются самыми первыми и определяют, может ли пользователь выполнить текущий запрос.
  • Фильтры ресурсов (Resource filters) вызываются после фильтров авторизации и необходимы, как следует из названия, для обработки ресурсов. В частности, данный тип фильтров применяют в качестве механизма кэширования.
  • Фильтры действий (Action Filters) выполняют указанные в них операции до и после выполнения метода контроллера, обрабатывающего запрос.
  • Фильтры исключений (Exception Filters) используются для перехвата необработанных исключений, произошедших при создании контроллера, привязке модели и выполнении кода фильтров действий и методов контроллера.
  • И наконец, самыми последними вызываются фильтры результатов (Result Filters), если метод контроллера был выполнен успешно. Данный тип фильтров чаще всего используется, чтобы модифицировать конечные результаты, например, мы можем создать свой заголовок ответа, в котором добавим нужную нам информацию.


Ниже представлена схема, которая показывает, в каком порядке вызываются фильтры в процессе обработки запроса:



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

Внутреннее устройство фильтров действий


Фильтры действий в ASP.NET


Интерфейс IActionFilter, который нужно реализовать, чтобы создать фильтр действий, существовал ещё в ASP.NET MVC. Он определяет методы OnActionExecuting, который вызывается перед выполнением метода контроллера, и OnActionExecuted, который вызывается сразу после. Ниже представлен пример простейшего фильтра действий, который выводит информацию во время отладки приложения до и после выполнения метода контроллера:

public class CustomActionFilter:IActionFilter {         public void OnActionExecuting(ActionExecutingContext filterContext)         {             Debug.WriteLine("Before Action Execution");         }         public void OnActionExecuted(ActionExecutedContext filterContext)         {             Debug.WriteLine("After Action Execution");         } }


Чтобы использовать вышеуказанный фильтр, его нужно зарегистрировать. Для этого в файле FilterConfig.cs, который находится в папке App_Start, следует добавить следующую строку:

public static void RegisterGlobalFilters(GlobalFilterCollection filters) {         filters.Add(new HandleErrorAttribute());         filters.Add(new CustomActionFilter()); }


Но гораздо удобнее использовать фильтры как атрибуты. Для этих целей существует абстрактный класс ActionFilterAttribute, который унаследован от класса FilterAttribute, а также реализует интерфейсы IActionFilter и IResultFilter. Таким образом, наш класс можно переписать следующим образом:

public class CustomActionFilterAttribute:ActionFilterAttribute {         public override void OnActionExecuting(ActionExecutingContext filterContext)         {             Debug.WriteLine("Before Action Execution");         }         public override void OnActionExecuted(ActionExecutedContext filterContext)         {             Debug.WriteLine("After Action Execution");         } } 


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

public class HomeController : Controller {         [CustomActionFilter]         public ActionResult Index()         {             return View();         } }


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

Фильтры действий в ASP.NET Core


С появлением ASP.NET Core в фильтрах действий произошёл ряд изменений. Кроме интерфейса IActionFilter, теперь имеется ещё и IAsyncActionFilter, который определяет единственный метод OnActionExecutionAsync. Ниже приведён пример класса, реализующего интерфейс IAsyncActionFilter:

public class AsyncCustomActionFilterAttribute:Attribute, IAsyncActionFilter {         public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)         {             Debug.WriteLine("Before Action Execution");             await next();             Debug.WriteLine("After Action Execution");         } } 


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

Применяют такой фильтр так же, как и синхронный:

public class HomeController : Controller {         [AsyncCustomActionFilter]         public ActionResult Index()         {             return View();         } }


Также изменения затронули абстрактный класс ActionFilterAttribute: теперь он наследуется от класса Attribute и реализует синхронные и асинхронные интерфейсы для фильтров действий (IActionFilter и IAsyncActionFilter) и для фильтров результатов (IResultFilter и IAsyncResultFilter), а также интерфейс IOrderedFilter.

Фильтры действий в действии


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

public class Employee {         [Required(ErrorMessage = "First name is required")]         public string FirstName { get; set; }         [Required(ErrorMessage = "Last name is required")]         public string LastName { get; set; }         [AgeRestriction(MinAge = 18, ErrorMessage = "Date of birth is incorrect")]         public DateTime DateOfBirth { get; set; }         [StringLength(50, MinimumLength = 2)]         public string Position { get; set; }         [Range(45000, 200000)]         public int Salary { get; set; } } 


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

После того как были реализованы методы POST и PUT, мы видим, что оба метода содержат повторяющиеся части кода:

[HttpPost] public IActionResult Post([FromBody] Employee value) {             if (value == null)             {                 return BadRequest("Employee value cannot be null");             }             if (!ModelState.IsValid)             {                 return BadRequest(ModelState);             }             // Perform save actions             return Ok(); } [HttpPut] public IActionResult Put([FromBody] Employee value) {             if (value == null)             {                 return BadRequest("Employee value cannot be null");             }             if (!ModelState.IsValid)             {                 return BadRequest(ModelState);             }             // Perform update actions             return Ok(); } 


И здесь нам на помощь приходят фильтры действий. Создадим новый фильтр действий и перенесём в него повторяющиеся строки следующим образом:

public class EmployeeValidationFilterAttribute : ActionFilterAttribute {         public override void OnActionExecuting(ActionExecutingContext context)         {             var employeeObject = context.ActionArguments.SingleOrDefault(p => p.Value is Employee);             if (employeeObject.Value == null)             {                 context.Result = new BadRequestObjectResult("Employee value cannot be null");                 return;             }             if (!context.ModelState.IsValid)             {                 context.Result = new BadRequestObjectResult(context.ModelState);             }         } } 


Теперь удаляем ставшие ненужными части кода и применяем созданный нами атрибут-фильтр:

public class EmployeeController : ControllerBase {         [EmployeeValidationFilter]         [HttpPost]         public IActionResult Post([FromBody] Employee value)         {             // Perform save actions             return Ok();         }         [EmployeeValidationFilter]         [HttpPut]         public IActionResult Put([FromBody] Employee value)         {             // Perform update actions             return Ok();         } } 


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

[EmployeeValidationFilter] public class EmployeeController : ControllerBase {             // Perform update actions } 


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

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

public class LoggingFilter: IActionFilter {         private readonly ILogger _logger;         public LoggingFilter(ILoggerFactory loggerFactory)         {             _logger = loggerFactory.CreateLogger<LoggingFilter>();         }         public void OnActionExecuted(ActionExecutedContext context)         {             _logger.LogInformation($"{context.ActionDescriptor.DisplayName} executed");         }         public void OnActionExecuting(ActionExecutingContext context)         {             _logger.LogInformation($"{context.ActionDescriptor.DisplayName} is executing");         } } 


Теперь мы можем применить этот фильтр либо глобально, либо к конкретной области. Сначала попробуем зарегистрировать его глобально. Для этого нам в Startup.cs следует добавить следующие строки:

services.AddControllers(options => {                 options.Filters.Add<LoggingFilter>(); }); 


Если же нам нужно применить фильтр, например, к определённому методу контроллера, то следует его использовать вместе с ServiceFilterAttribute:

[HttpPost] [ServiceFilter(typeof(LoggingFilter))] public IActionResult Post([FromBody] Employee value) 


ServiceFilterAttribute является фабрикой для других фильтров, реализующей интерфейс IFilterFactory и использующей IServiceProvider для получения нужного фильтра. Поэтому в Startup.cs нам необходимо зарегистрировать наш фильтр следующим образом:

services.AddSingleton<LoggingFilter>(); 


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

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

public class ProviderFilter : IActionFilter {         private readonly IDataProvider _dataProvider;         public ProviderFilter(IDataProvider dataProvider)         {             _dataProvider = dataProvider;         }         public void OnActionExecuted(ActionExecutedContext context)         {         }         public void OnActionExecuting(ActionExecutingContext context)         {             object idValue;             if (!context.ActionArguments.TryGetValue("id", out idValue))             {                 throw new ArgumentException("id");             }             var id = (int)idValue;             var result = _dataProvider.GetElement(id);             if (result == null)             {                 context.Result = new NotFoundResult();             }             else             {                 context.HttpContext.Items.Add("result", result);             }         } } 


Применить этот фильтр можно так же, как и фильтр из предыдущего примера, с помощью ServiceFilterAttribute.

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

public class BrowserCheckFilter : IActionFilter {         public void OnActionExecuting(ActionExecutingContext context)         {             var userAgent = context.HttpContext.Request.Headers[HeaderNames.UserAgent].ToString().ToLower();             // Detect if a user uses IE             if (userAgent.Contains("msie") || userAgent.Contains("trident"))             {                 // Do some actions              }         }         public void OnActionExecuted(ActionExecutedContext context)         {         } } 


Стоит, однако, заметить, что вышеуказанный метод имеет ещё один недостаток. Многие браузеры умеют прятать или подделывать значения, указанные в User-Agent, поэтому данный способ не является однозначно достоверным в определении типа пользовательского браузера.

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

public class LocalizationActionFilterAttribute: ActionFilterAttribute {         public override void OnActionExecuting(ActionExecutingContext filterContext)         {             var language = (string)filterContext.RouteData.Values["language"] ?? "en";             var culture = (string)filterContext.RouteData.Values["culture"] ?? "GB";             Thread.CurrentThread.CurrentCulture = CultureInfo.GetCultureInfo($"{language}-{culture}");             Thread.CurrentThread.CurrentUICulture = CultureInfo.GetCultureInfo($"{language}-{culture}");         } } 


Следующим шагом следует добавить маршрутизацию, которая будет перенаправлять URL с данными культуры на наш контроллер:

                endpoints.MapControllerRoute(name:"localizedRoute",                     pattern: "{language}-{culture}/{controller}/{action}/{id}",                     defaults: new                     {                         language = "en",                         culture = "GB",                         controller = "Date",                         action = "Index",                         id = "",                     });


Код выше создаёт маршрут с именем localizedRoute, у которого в шаблоне имеется параметр, отвечающий за локализацию. Значение по умолчанию для этого параметра en-GB.

Теперь создадим контроллер с именем DateController, который будет обрабатывать наш запрос, и представление, которое будет отображать локализованную дату. Код контроллера просто возвращает представлению текущую дату:

[LocalizationActionFilter] public class DateController : Controller {         public IActionResult Index()         {             ViewData["Date"] = DateTime.Now.ToShortDateString();             return View();         } }  


После того как пользователь перешёл по ссылке localhost:44338/Date, он увидит в браузере следующее:

image
На скриншоте выше текущая дата представлена с учётом локализации, заданной по умолчанию, т.е. с en-GB. Теперь, если пользователь перейдёт по ссылке, в которой будет явно указана культура, например, en-US, то он увидит следующее:


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

Заключение


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

Перевод Искусственный интеллект обновит устаревшее программное обеспечение за вас

02.03.2021 00:19:35 | Автор: admin
Инструменты IBM на основе искусственного интеллекта дают инженерам возможность исследовать способы применения устаревшего корпоративного ПО.

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



Последние проекты IBM под названием Mono2Micro и Application Modernization Accelerator (AMA) предоставляют архитекторам приложений инструменты для обновления устаревших приложений и повторного их применения. По словам Ника Фуллера, директора по гибридным облачным сервисам в исследовательской лаборатории IBM Research, эти инициативы позволяют приблизить момент, когда ИИ сможет автоматически перевести программу с COBOL на Java.

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

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

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

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

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


Изображение: IBM Research
Изображение интерфейса Mono2Micro

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

Эти слои внедрения представляют собой способ преобразования данных в числовые значения. Преимущество метода встраивания заключается в том, что он позволяет присвоить числовое выражение большому объему кода с множеством возможных значений. То же самое происходит, например, при переводе естественного человеческого языка в числовые значения с использованием векторного представления слов (word embeddings). Это также делается в контексте графа, так как это связано с анализом кода.

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

Фуллер отмечает: На этом этапе вы еще не можете выдохнуть, но 70% работы позади, а значит, вы стали гораздо ближе к рефакторирнгу важного приложения в архитектуру микросервисов.
Подробнее..

Кто вы, мистер архитектор?

28.05.2021 12:18:53 | Автор: admin

Привет, меня зовут Алексей, я системный архитектор e-commerce платформы Lamoda, и в этом посте мое представление о том, чем на самом деле занимается ИТ-архитектор, какие вопросы решает в ежедневной работе и за что несет ответственность.

Сцена из фильма "Начало"Сцена из фильма "Начало"

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

Чем мы вообще тут занимаемся?

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

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

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

Что важнее: доступность или согласованность

Опытные инженеры, среди которых есть и архитекторы, ежедневно работают для того, чтобы круглосуточно на отправленный запрос в нашу систему выдавался верный JSON за приемлемое время. Например, создание заказа. Для e-commerce это критичное место: нужно провалидировать соответствие всех условий, корректно зарезервировать товары и оповестить все заинтересованные стороны о новом успешном заказе. Все это должно происходить за доли секунды и отвечать ожиданиям кастомера.

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

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

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

Еще один пример. В Lamoda, как и в любой крупной e-commerce компании, существует большая система обработки заказов. За более чем 10 лет домен нашей системы вырос и многократно усложнился, как и ее ответственность за все ту же согласованность и доступность. Сама система и ее сложность появилась не просто так и не была результатом проектирования архитектора-маньяка. Ее создали люди, которые приняли сотни решений, а эти решения привели к тем результатам, которые мы видим сейчас. Нужно отдать должное этим людям, так как система выполняет возложенные на нее требования. Проблема только в одном вносить изменения стало крайне проблематично. И решение этой проблемы нельзя назвать тривиальным, но оно должно быть простым. Как и в задаче с обработкой JSON-ов.

Какую задачу решает архитектор

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

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

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

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

Как развивалось понимание архитектуры и обязанностей архитектора

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

В начале 2000-х фокус ответственности сместился: архитекторам нужно было принимать важные решения, чтобы создавать правильные модели (правильные означают, что они удовлетворяют потребности заинтересованных сторон). Если абстракция и структуры описывают то, что создает архитектор, то принятие решения относится к тому, как они создаются.

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

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

За что отвечает современный архитектор

Таким образом, обязанности архитектора заключаются в том, чтобы:

  • понимать контекст;

  • принимать решения;

  • создавать модели;

  • валидировать дизайн;

  • реализовывать и поставлять решения.

При этом одни пункты из этого списка влияют на выполнение других. Например:

  • моделирование и принятие решений без понимания контекста приводит к построению неадекватных моделей;

  • моделирование фактически подразумевает принятие решений (о декомпозиции, взаимосвязях);

  • без моделирования и решений нечего валидировать;

  • реализация и поставка не валидированных решений приводит к проблемам.

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

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

Один из ярких примеров The Waterfall Wasteland, когда архитектурная команда занимается дизайном в отрыве от проектной и не вовлекается в ежедневные активности, в результате чего растет время между проектированием и доставкой в продакшен. Совместная работа с проектной командой дает важную обратную связь, без которой легко оказаться в Башне из слоновой кости (The Ivory Tower Architect).

С другой стороны, есть пример The Agile Outback, когда в страхе перед Ivory Tower проектирование считается излишней практикой или даже контрпродуктивной. Вместо этого команда получает фидбэк от реализованных ошибок. Такой подход может быть выгодным в начале, но вскоре приводит к серьезным затруднениям.

Как я решаю свою задачу через призму этих обязанностей

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

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

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

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

Я большой фанат подходов Domain Driven Design и того, как они развиваются последние несколько лет (DDD Europe, etc). В частности bounded contexts, поскольку именно они помогают определить транзакционные границы сервиса и то, как лучше настроить оркестрацию взаимодействий. Чтобы избежать единой точки отказа, оркестраторы у нас являются частью сервиса и контекста соответственно. Т.е. исполнением саги занимается исключительно ответственный за это сервис внутри контекста, а не отдельный инфраструктурный сервис, который исполняет саги по запросу.

a) исполнение саги локальным оркестратором; b) использование отдельного сервиса оркестратора для исполнения саги; с) вариант хореографии событий;a) исполнение саги локальным оркестратором; b) использование отдельного сервиса оркестратора для исполнения саги; с) вариант хореографии событий;

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

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

Как формируем команды

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

Исходя из этого мы формируем команды, которые должны:

  • погрузиться в контекст;

  • иметь экспертизу в легаси-стеке;

  • иметь возможность адаптироваться к новому стеку;

  • принять необходимые технологии и практики;

  • получить экспертизу в домене;

  • принимать решения самостоятельно.

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

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

Выстраиваем взаимодействия через единый нарратив

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

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

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

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

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


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

Что еще почитать

Подробнее..

Flask Dependency Injector руководство по применению dependency injection

25.07.2020 06:07:48 | Автор: admin
Привет,

Я создатель Dependency Injector. Это dependency injection фреймворк для Python.

В этом руководстве хочу показать как применять Dependency Injector для разработки Flask приложений.

Руководство состоит из таких частей:

  1. Что мы будем строить?
  2. Подготовим окружение
  3. Структура проекта
  4. Hello world!
  5. Подключаем стили
  6. Подключаем Github
  7. Сервис поиска
  8. Подключаем поиск
  9. Немного рефакторинга
  10. Добавляем тесты
  11. Заключение

Завершенный проект можно найти на Github.

Для старта необходимо иметь:

  • Python 3.5+
  • Virtual environment

И желательно иметь:

  • Начальные навыки разработки с помощью Flask
  • Общее представление о принципе dependency injection

Что мы будем строить?


Мы будем строить приложение, которое помогает искать репозитории на Github. Назовем его Github Navigator.

Как работает Github Navigator?

  • Пользователь открывает веб-страницу где ему предлагают ввести поисковый запрос.
  • Пользователь вводит запрос и нажимает Enter.
  • Github Navigator ищет подходящие репозитории на Github.
  • По окончанию поиска Github Navigator показывает пользователю веб-страницу с результатами.
  • Страница результатов показывает все найденные репозитории и поисковый запрос.
  • Для каждого репозитория пользователь видит:
    • имя репозитория
    • владельца репозитория
    • последний коммит в репозиторий
  • Пользователь может нажать на любой из элементов чтобы открыть его страницу на Github.



Подготовим окружение


В первую очередь нам нужно создать папку проекта и virtual environment:

mkdir ghnav-flask-tutorialcd ghnav-flask-tutorialpython3 -m venv venv

Теперь давайте активируем virtual environment:

. venv/bin/activate

Окружение готово, теперь займемся структурой проекта.

Структура проекта


Создадим в текущей папке следующую структуру. Все файлы пока оставляем пустыми. Это пока не критично.

Начальная структура:

./ githubnavigator/    __init__.py    application.py    containers.py    views.py venv/ requirements.txt

Пришло время установить Flask и Dependency Injector.

Добавим следующие строки в файл requirements.txt:

dependency-injectorflask

Теперь давайте их установим:

pip install -r requirements.txt

И проверим что установка прошла успешно:

python -c "import dependency_injector; print(dependency_injector.__version__)"python -c "import flask; print(flask.__version__)"

Вы увидите что-то вроде:

(venv) $ python -c "import dependency_injector; print(dependency_injector.__version__)"3.22.0(venv) $ python -c "import flask; print(flask.__version__)"1.1.2

Hello world!


Давайте создадим минимальное hello world приложение.

Добавим следующие строки в файл views.py:

"""Views module."""def index():    return 'Hello, World!'

Теперь добавим контейнер зависимостей (дальше просто контейнер). Контейнер будет содержать все компоненты приложения. Добавим первые два компонента. Это Flask приложение и представление index.

Добавим следующее в файл containers.py:

"""Application containers module."""from dependency_injector import containersfrom dependency_injector.ext import flaskfrom flask import Flaskfrom . import viewsclass ApplicationContainer(containers.DeclarativeContainer):    """Application container."""    app = flask.Application(Flask, __name__)    index_view = flask.View(views.index)

Теперь нам нужно создать фабрику Flask приложения. Ее обычно называют create_app(). Она будет создавать контейнер. Контейнер будет использован для создания Flask приложения. Последним шагом настроим маршрутизацию мы назначим представление index_view из контейнера обрабатывать запросы к корню "/" нашего приложения.

Отредактируем application.py:

"""Application module."""from .containers import ApplicationContainerdef create_app():    """Create and return Flask application."""    container = ApplicationContainer()    app = container.app()    app.container = container    app.add_url_rule('/', view_func=container.index_view.as_view())    return app

Контейнер первый объект в приложении. Он используется для получения всех остальных объектов.

Теперь наше приложение готово сказать Hello, World!.

Выполните в терминале:

export FLASK_APP=githubnavigator.applicationexport FLASK_ENV=developmentflask run

Вывод должен выглядеть приблизительно так:

* Serving Flask app "githubnavigator.application" (lazy loading)* Environment: development* Debug mode: on* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)* Restarting with fsevents reloader* Debugger is active!* Debugger PIN: 473-587-859

Откройте браузер и зайдите на http://127.0.0.1:5000/.

Вы увидите Hello, World!.

Отлично. Наше минимальное приложение успешно стартует и работает.

Давайте сделаем его немного красивее.

Подключаем стили


Мы будем использовать Bootstrap 4. Используем для этого расширение Bootstrap-Flask. Оно поможет нам добавить все нужные файлы в несколько кликов.

Добавим bootstrap-flask в requirements.txt:

dependency-injectorflaskbootstrap-flask

и выполним в терминале:

pip install --upgrade -r requirements.txt

Теперь добавим расширение bootstrap-flask в контейнер.

Отредактируйте containers.py:

"""Application containers module."""from dependency_injector import containersfrom dependency_injector.ext import flaskfrom flask import Flaskfrom flask_bootstrap import Bootstrapfrom . import viewsclass ApplicationContainer(containers.DeclarativeContainer):    """Application container."""    app = flask.Application(Flask, __name__)    bootstrap = flask.Extension(Bootstrap)    index_view = flask.View(views.index)

Давайте инициализируем расширение bootstrap-flask. Нам нужно будет изменить create_app().

Отредактируйте application.py:

"""Application module."""from .containers import ApplicationContainerdef create_app():    """Create and return Flask application."""    container = ApplicationContainer()    app = container.app()    app.container = container    bootstrap = container.bootstrap()    bootstrap.init_app(app)    app.add_url_rule('/', view_func=container.index_view.as_view())    return app

Теперь нужно добавить шаблоны. Для этого нам понадобится добавить папку templates/ в пакет githubnavigator. Внутри папки с шаблонами добавим два файла:

  • base.html базовый шаблон
  • index.html шаблон основной страницы

Создаем папку templates и два пустых файла внутри base.html и index.html:

./ githubnavigator/    templates/       base.html       index.html    __init__.py    application.py    containers.py    views.py venv/ requirements.txt

Теперь давайте наполним базовый шаблон.

Добавим следующие строки в файл base.html:

<!doctype html><html lang="en">    <head>        {% block head %}        <!-- Required meta tags -->        <meta charset="utf-8">        <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">        {% block styles %}            <!-- Bootstrap CSS -->            {{ bootstrap.load_css() }}        {% endblock %}        <title>{% block title %}{% endblock %}</title>        {% endblock %}    </head>    <body>        <!-- Your page content -->        {% block content %}{% endblock %}        {% block scripts %}            <!-- Optional JavaScript -->            {{ bootstrap.load_js() }}        {% endblock %}    </body></html>

Теперь наполним шаблон основной страницы.

Добавим следующие строки в файл index.html:

{% extends "base.html" %}{% block title %}Github Navigator{% endblock %}{% block content %}<div class="container">    <h1 class="mb-4">Github Navigator</h1>    <form>        <div class="form-group form-row">            <div class="col-10">                <label for="search_query" class="col-form-label">                    Search for:                </label>                <input class="form-control" type="text" id="search_query"                       placeholder="Type something to search on the GitHub"                       name="query"                       value="{{ query if query }}">            </div>            <div class="col">                <label for="search_limit" class="col-form-label">                    Limit:                </label>                <select class="form-control" id="search_limit" name="limit">                    {% for value in [5, 10, 20] %}                    <option {% if value == limit %}selected{% endif %}>                        {{ value }}                    </option>                    {% endfor %}                </select>            </div>        </div>    </form>    <p><small>Results found: {{ repositories|length }}</small></p>    <table class="table table-striped">        <thead>            <tr>                <th>#</th>                <th>Repository</th>                <th class="text-nowrap">Repository owner</th>                <th class="text-nowrap">Last commit</th>            </tr>        </thead>        <tbody>        {% for repository in repositories %} {{n}}            <tr>              <th>{{ loop.index }}</th>              <td><a href="{{ repository.url }}">                  {{ repository.name }}</a>              </td>              <td><a href="{{ repository.owner.url }}">                  <img src="{{ repository.owner.avatar_url }}"                       alt="avatar" height="24" width="24"/></a>                  <a href="{{ repository.owner.url }}">                      {{ repository.owner.login }}</a>              </td>              <td><a href="{{ repository.latest_commit.url }}">                  {{ repository.latest_commit.sha }}</a>                  {{ repository.latest_commit.message }}                  {{ repository.latest_commit.author_name }}              </td>            </tr>        {% endfor %}        </tbody>    </table></div>{% endblock %}

Отлично, почти готово. Последним шагом изменим представление index чтобы оно использовало шаблон index.html.

Отредактируем views.py:

"""Views module."""from flask import request, render_templatedef index():    query = request.args.get('query', 'Dependency Injector')    limit = request.args.get('limit', 10, int)    repositories = []    return render_template(        'index.html',        query=query,        limit=limit,        repositories=repositories,    )

Готово.

Убедитесь что приложение работает или выполните flask run и откройте http://127.0.0.1:5000/.

Вы должны увидите:



Подключаем Github


В этом разделе интегрируем наше приложение с Github API.
Мы будем использовать библиотеку PyGithub.

Добавим её в requirements.txt:

dependency-injectorflaskbootstrap-flaskpygithub

и выполним в терминале:

pip install --upgrade -r requirements.txt

Теперь нам нужно добавить Github API клиент в контейнер. Для этого нам нужно будет воспользоваться двумя новыми провайдерами из модуля dependency_injector.providers:

  • Провайдер Factory будет создавать Github клиент.
  • Провайдер Configuration будет передавать API токен и таймаут Github клиенту.

Сделаем это.

Отредактируем containers.py:

"""Application containers module."""from dependency_injector import containers, providersfrom dependency_injector.ext import flaskfrom flask import Flaskfrom flask_bootstrap import Bootstrapfrom github import Githubfrom . import viewsclass ApplicationContainer(containers.DeclarativeContainer):    """Application container."""    app = flask.Application(Flask, __name__)    bootstrap = flask.Extension(Bootstrap)    config = providers.Configuration()    github_client = providers.Factory(        Github,        login_or_token=config.github.auth_token,        timeout=config.github.request_timeout,    )    index_view = flask.View(views.index)

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

Сначала используем, потом задаем значения.

Теперь давайте добавим файл конфигурации.
Будем использовать YAML.

Создайте пустой файл config.yml в корне проекта:

./ githubnavigator/    templates/       base.html       index.html    __init__.py    application.py    containers.py    views.py venv/ config.yml requirements.txt

И заполните его следующими строками:

github:  request_timeout: 10

Для работы с конфигурационным файлом мы будем использовать библиотеку PyYAML. Добавим ее в файл с зависимостями.

Отредактируйте requirements.txt:

dependency-injectorflaskbootstrap-flaskpygithubpyyaml

и установите зависимость:

pip install --upgrade -r requirements.txt

Для передачи API токена мы будем использовать переменную окружения GITHUB_TOKEN.

Теперь нам нужно отредактировать create_app() чтобы сделать 2 действие при старте приложения:

  • Загрузить конфигурацию из config.yml
  • Загрузить API токен из переменной окружения GITHUB_TOKEN

Отредактируйте application.py:

"""Application module."""from .containers import ApplicationContainerdef create_app():    """Create and return Flask application."""    container = ApplicationContainer()    container.config.from_yaml('config.yml')    container.config.github.auth_token.from_env('GITHUB_TOKEN')    app = container.app()    app.container = container    bootstrap = container.bootstrap()    bootstrap.init_app(app)    app.add_url_rule('/', view_func=container.index_view.as_view())    return app

Теперь нам нужно создать API токен.

Для это нужно:

  • Следовать этому руководству на Github
  • Установить токен в переменную окружения:

    export GITHUB_TOKEN=<your token>
    

Этот пункт можно временно пропустить.

Приложение будет работать без токена, но с ограниченной пропускной способностью. Ограничение для неаутентифицированных клиентов: 60 запросов в час. Токен нужен чтобы увеличить эту квоту до 5000 в час.

Готово.

Установка Github API клиента завершена.

Сервис поиска


Пришло время добавить сервис поиска SearchService. Он будет:

  • Выполнять поиск на Github
  • Получать дополнительные данные о коммитах
  • Преобразовывать формат результат

SearchService будет использовать Github API клиент.

Создайте пустой файл services.py в пакете githubnavigator:

./ githubnavigator/    templates/       base.html       index.html    __init__.py    application.py    containers.py    services.py    views.py venv/ config.yml requirements.txt

и добавьте в него следующие строки:

"""Services module."""from github import Githubfrom github.Repository import Repositoryfrom github.Commit import Commitclass SearchService:    """Search service performs search on Github."""    def __init__(self, github_client: Github):        self._github_client = github_client    def search_repositories(self, query, limit):        """Search for repositories and return formatted data."""        repositories = self._github_client.search_repositories(            query=query,            **{'in': 'name'},        )        return [            self._format_repo(repository)            for repository in repositories[:limit]        ]    def _format_repo(self, repository: Repository):        commits = repository.get_commits()        return {            'url': repository.html_url,            'name': repository.name,            'owner': {                'login': repository.owner.login,                'url': repository.owner.html_url,                'avatar_url': repository.owner.avatar_url,            },            'latest_commit': self._format_commit(commits[0]) if commits else {},        }    def _format_commit(self, commit: Commit):        return {            'sha': commit.sha,            'url': commit.html_url,            'message': commit.commit.message,            'author_name': commit.commit.author.name,        }

Теперь добавим SearchService в контейнер.

Отредактируйте containers.py:

"""Application containers module."""from dependency_injector import containers, providersfrom dependency_injector.ext import flaskfrom flask import Flaskfrom flask_bootstrap import Bootstrapfrom github import Githubfrom . import services, viewsclass ApplicationContainer(containers.DeclarativeContainer):    """Application container."""    app = flask.Application(Flask, __name__)    bootstrap = flask.Extension(Bootstrap)    config = providers.Configuration()    github_client = providers.Factory(        Github,        login_or_token=config.github.auth_token,        timeout=config.github.request_timeout,    )    search_service = providers.Factory(        services.SearchService,        github_client=github_client,    )    index_view = flask.View(views.index)

Подключаем поиск


Теперь мы готовы чтобы поиск заработал. Давайте используем SearchService в index представлении.

Отредактируйте views.py:

"""Views module."""from flask import request, render_templatefrom .services import SearchServicedef index(search_service: SearchService):    query = request.args.get('query', 'Dependency Injector')    limit = request.args.get('limit', 10, int)    repositories = search_service.search_repositories(query, limit)    return render_template(        'index.html',        query=query,        limit=limit,        repositories=repositories,    )

Теперь изменим контейнер чтобы передавать зависимость SearchService в представление index при его вызове.

Отредактируйте containers.py:

"""Application containers module."""from dependency_injector import containers, providersfrom dependency_injector.ext import flaskfrom flask import Flaskfrom flask_bootstrap import Bootstrapfrom github import Githubfrom . import services, viewsclass ApplicationContainer(containers.DeclarativeContainer):    """Application container."""    app = flask.Application(Flask, __name__)    bootstrap = flask.Extension(Bootstrap)    config = providers.Configuration()    github_client = providers.Factory(        Github,        login_or_token=config.github.auth_token,        timeout=config.github.request_timeout,    )    search_service = providers.Factory(        services.SearchService,        github_client=github_client,    )    index_view = flask.View(        views.index,        search_service=search_service,    )

Убедитесь что приложение работает или выполните flask run и откройте http://127.0.0.1:5000/.

Вы увидите:



Немного рефакторинга


Наше представление index содержит два hardcoded значения:

  • Поисковый запрос по умолчанию
  • Лимит количества результатов

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

Отредактируйте views.py:

"""Views module."""from flask import request, render_templatefrom .services import SearchServicedef index(        search_service: SearchService,        default_query: str,        default_limit: int,):    query = request.args.get('query', default_query)    limit = request.args.get('limit', default_limit, int)    repositories = search_service.search_repositories(query, limit)    return render_template(        'index.html',        query=query,        limit=limit,        repositories=repositories,    )

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

Отредактируйте containers.py:

"""Application containers module."""from dependency_injector import containers, providersfrom dependency_injector.ext import flaskfrom flask import Flaskfrom flask_bootstrap import Bootstrapfrom github import Githubfrom . import services, viewsclass ApplicationContainer(containers.DeclarativeContainer):    """Application container."""    app = flask.Application(Flask, __name__)    bootstrap = flask.Extension(Bootstrap)    config = providers.Configuration()    github_client = providers.Factory(        Github,        login_or_token=config.github.auth_token,        timeout=config.github.request_timeout,    )    search_service = providers.Factory(        services.SearchService,        github_client=github_client,    )    index_view = flask.View(        views.index,        search_service=search_service,        default_query=config.search.default_query,        default_limit=config.search.default_limit,    )

Теперь давайте обновим конфигурационный файл.

Отредактируйте config.yml:

github:  request_timeout: 10search:  default_query: "Dependency Injector"  default_limit: 10

Готово.

Рефакторинг закончен. Му сделали код чище.

Добавляем тесты


Было бы хорошо добавить немного тестов. Давайте это сделаем.

Мы будем использовать pytest и coverage.

Отредактируйте requirements.txt:

dependency-injectorflaskbootstrap-flaskpygithubpyyamlpytest-flaskpytest-cov

и установите новые пакеты:

pip install -r requirements.txt

Создайте пустой файл tests.py в пакете githubnavigator:

./ githubnavigator/    templates/       base.html       index.html    __init__.py    application.py    containers.py    services.py    tests.py    views.py venv/ config.yml requirements.txt

и добавьте в него следующие строки:

"""Tests module."""from unittest import mockimport pytestfrom github import Githubfrom flask import url_forfrom .application import create_app@pytest.fixturedef app():    return create_app()def test_index(client, app):    github_client_mock = mock.Mock(spec=Github)    github_client_mock.search_repositories.return_value = [        mock.Mock(            html_url='repo1-url',            name='repo1-name',            owner=mock.Mock(                login='owner1-login',                html_url='owner1-url',                avatar_url='owner1-avatar-url',            ),            get_commits=mock.Mock(return_value=[mock.Mock()]),        ),        mock.Mock(            html_url='repo2-url',            name='repo2-name',            owner=mock.Mock(                login='owner2-login',                html_url='owner2-url',                avatar_url='owner2-avatar-url',            ),            get_commits=mock.Mock(return_value=[mock.Mock()]),        ),    ]    with app.container.github_client.override(github_client_mock):        response = client.get(url_for('index'))    assert response.status_code == 200    assert b'Results found: 2' in response.data    assert b'repo1-url' in response.data    assert b'repo1-name' in response.data    assert b'owner1-login' in response.data    assert b'owner1-url' in response.data    assert b'owner1-avatar-url' in response.data    assert b'repo2-url' in response.data    assert b'repo2-name' in response.data    assert b'owner2-login' in response.data    assert b'owner2-url' in response.data    assert b'owner2-avatar-url' in response.datadef test_index_no_results(client, app):    github_client_mock = mock.Mock(spec=Github)    github_client_mock.search_repositories.return_value = []    with app.container.github_client.override(github_client_mock):        response = client.get(url_for('index'))    assert response.status_code == 200    assert b'Results found: 0' in response.data

Теперь давайте запустим тестирование и проверим покрытие:

py.test githubnavigator/tests.py --cov=githubnavigator

Вы увидите:

platform darwin -- Python 3.8.3, pytest-5.4.3, py-1.9.0, pluggy-0.13.1plugins: flask-1.0.0, cov-2.10.0collected 2 itemsgithubnavigator/tests.py ..                                     [100%]---------- coverage: platform darwin, python 3.8.3-final-0 -----------Name                             Stmts   Miss  Cover----------------------------------------------------githubnavigator/__init__.py          0      0   100%githubnavigator/application.py      11      0   100%githubnavigator/containers.py       13      0   100%githubnavigator/services.py         14      0   100%githubnavigator/tests.py            32      0   100%githubnavigator/views.py             7      0   100%----------------------------------------------------TOTAL                               77      0   100%

Обратите внимание как мы заменяем github_client моком с помощью метода .override(). Таким образом можно переопределить возвращаемое значения любого провайдера.

Заключение


Мы построили Flask приложения применяя принцип dependency injection. Мы использовали Dependency Injector в качестве dependency injection фреймворка.

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

"""Application containers module."""from dependency_injector import containers, providersfrom dependency_injector.ext import flaskfrom flask import Flaskfrom flask_bootstrap import Bootstrapfrom github import Githubfrom . import services, viewsclass ApplicationContainer(containers.DeclarativeContainer):    """Application container."""    app = flask.Application(Flask, __name__)    bootstrap = flask.Extension(Bootstrap)    config = providers.Configuration()    github_client = providers.Factory(        Github,        login_or_token=config.github.auth_token,        timeout=config.github.request_timeout,    )    search_service = providers.Factory(        services.SearchService,        github_client=github_client,    )    index_view = flask.View(        views.index,        search_service=search_service,        default_query=config.search.default_query,        default_limit=config.search.default_limit,    )


Контейнер как карта вашего приложения. Вы всегда знайте что от чего зависит.

Что дальше?


Подробнее..

Aiohttp Dependency Injector руководство по применению dependency injection

02.08.2020 22:16:32 | Автор: admin
Привет,

Я создатель Dependency Injector. Это dependency injection фреймворк для Python.

Продолжаю серию руководств по применению Dependency Injector для построения приложений.

В этом руководстве хочу показать как применять Dependency Injector для разработки aiohttp приложений.

Руководство состоит из таких частей:

  1. Что мы будем строить?
  2. Подготовка окружения
  3. Структура проекта
  4. Установка зависимостей
  5. Минимальное приложение
  6. Giphy API клиент
  7. Сервис поиска
  8. Подключаем поиск
  9. Немного рефакторинга
  10. Добавляем тесты
  11. Заключение

Завершенный проект можно найти на Github.

Для старта необходимо иметь:

  • Python 3.5+
  • Virtual environment

И желательно иметь:

  • Начальные навыки разработки с помощью aiohttp
  • Общее представление о принципе dependency injection


Что мы будем строить?





Мы будем строить REST API приложение, которое ищет забавные гифки на Giphy. Назовем его Giphy Navigator.

Как работает Giphy Navigator?

  • Клиент отправляет запрос указывая что искать и сколько результатов вернуть.
  • Giphy Navigator возвращает ответ в формате json.
  • Ответ включает:
    • поисковый запрос
    • количество результатов
    • список url гифок

Пример ответа:

{    "query": "Dependency Injector",    "limit": 10,    "gifs": [        {            "url": "https://giphy.com/gifs/boxes-dependent-swbf2-6Eo7KzABxgJMY"        },        {            "url": "https://giphy.com/gifs/depends-J56qCcOhk6hKE"        },        {            "url": "https://giphy.com/gifs/web-series-ccstudios-bro-dependent-1lhU8KAVwmVVu"        },        {            "url": "https://giphy.com/gifs/TheBoysTV-friends-friend-weneedeachother-XxR9qcIwcf5Jq404Sx"        },        {            "url": "https://giphy.com/gifs/netflix-a-series-of-unfortunate-events-asoue-9rgeQXbwoK53pcxn7f"        },        {            "url": "https://giphy.com/gifs/black-and-white-sad-skins-Hs4YzLs2zJuLu"        },        {            "url": "https://giphy.com/gifs/always-there-for-you-i-am-here-PlayjhCco9jHBYrd9w"        },        {            "url": "https://giphy.com/gifs/stream-famous-dollar-YT2dvOByEwXCdoYiA1"        },        {            "url": "https://giphy.com/gifs/i-love-you-there-for-am-1BhGzgpZXYWwWMAGB1"        },        {            "url": "https://giphy.com/gifs/life-like-twerk-9hlnWxjHqmH28"        }    ]}

Подготовим окружение


Начнём с подготовки окружения.

В первую очередь нам нужно создать папку проекта и virtual environment:

mkdir giphynav-aiohttp-tutorialcd giphynav-aiohttp-tutorialpython3 -m venv venv

Теперь давайте активируем virtual environment:

. venv/bin/activate

Окружение готово, теперь займемся структурой проекта.

Структура проекта


В этом разделе организуем структуру проекта.

Создадим в текущей папке следующую структуру. Все файлы пока оставляем пустыми.

Начальная структура:

./ giphynavigator/    __init__.py    application.py    containers.py    views.py venv/ requirements.txt

Установка зависимостей


Пришло время установить зависимости. Мы будем использовать такие пакеты:

  • dependency-injector dependency injection фреймворк
  • aiohttp веб фреймворк
  • aiohttp-devtools библиотека-помогатор, которая предоставляет сервер для разработки с live-перезагрузкой
  • pyyaml библиотека для парсинга YAML файлов, используется для чтения конфига
  • pytest-aiohttp библиотека-помогатор для тестирования aiohttp приложений
  • pytest-cov библиотека-помогатор для измерения покрытия кода тестами

Добавим следующие строки в файл requirements.txt:

dependency-injectoraiohttpaiohttp-devtoolspyyamlpytest-aiohttppytest-cov

И выполним в терминале:

pip install -r requirements.txt

Дополнительно установим httpie. Это HTTP клиент для командной строки. Мы будем
использовать его для ручного тестирования API.

Выполним в терминале:

pip install httpie

Зависимости установлены. Теперь построим минимальное приложение.

Минимальное приложение


В этом разделе построим минимальное приложение. У него будет эндпоинт, который будет возвращать пустой ответ.

Отредактируем views.py:

"""Views module."""from aiohttp import webasync def index(request: web.Request) -> web.Response:    query = request.query.get('query', 'Dependency Injector')    limit = int(request.query.get('limit', 10))    gifs = []    return web.json_response(        {            'query': query,            'limit': limit,            'gifs': gifs,        },    )

Теперь добавим контейнер зависимостей (дальше просто контейнер). Контейнер будет содержать все компоненты приложения. Добавим первые два компонента. Это aiohttp приложение и представление index.

Отредактируем containers.py:

"""Application containers module."""from dependency_injector import containersfrom dependency_injector.ext import aiohttpfrom aiohttp import webfrom . import viewsclass ApplicationContainer(containers.DeclarativeContainer):    """Application container."""    app = aiohttp.Application(web.Application)    index_view = aiohttp.View(views.index)

Теперь нам нужно создать фабрику aiohttp приложения. Ее обычно называют
create_app(). Она будет создавать контейнер. Контейнер будет использован для создания aiohttp приложения. Последним шагом настроим маршрутизацию мы назначим представление index_view из контейнера обрабатывать запросы к корню "/" нашего приложения.

Отредактируем application.py:

"""Application module."""from aiohttp import webfrom .containers import ApplicationContainerdef create_app():    """Create and return aiohttp application."""    container = ApplicationContainer()    app: web.Application = container.app()    app.container = container    app.add_routes([        web.get('/', container.index_view.as_view()),    ])    return app

Контейнер первый объект в приложении. Он используется для получения всех остальных объектов.

Теперь мы готовы запустить наше приложение:

Выполните команду в терминале:

adev runserver giphynavigator/application.py --livereload

Вывод должен выглядеть так:

[18:52:59] Starting aux server at http://localhost:8001 [18:52:59] Starting dev server at http://localhost:8000 

Используем httpie чтобы проверить работу сервера:

http http://127.0.0.1:8000/

Вы увидите:

HTTP/1.1 200 OKContent-Length: 844Content-Type: application/json; charset=utf-8Date: Wed, 29 Jul 2020 21:01:50 GMTServer: Python/3.8 aiohttp/3.6.2{    "gifs": [],    "limit": 10,    "query": "Dependency Injector"}

Минимальное приложение готово. Давайте подключим Giphy API.

Giphy API клиент


В этом разделе мы интегрируем наше приложение с Giphy API. Мы создадим собственный API клиент используя клиентскую часть aiohttp.

Создайте пустой файл giphy.py в пакете giphynavigator:

./ giphynavigator/    __init__.py    application.py    containers.py    giphy.py    views.py venv/ requirements.txt

и добавьте в него следующие строки:

"""Giphy client module."""from aiohttp import ClientSession, ClientTimeoutclass GiphyClient:    API_URL = 'http://api.giphy.com/v1'    def __init__(self, api_key, timeout):        self._api_key = api_key        self._timeout = ClientTimeout(timeout)    async def search(self, query, limit):        """Make search API call and return result."""        if not query:            return []        url = f'{self.API_URL}/gifs/search'        params = {            'q': query,            'api_key': self._api_key,            'limit': limit,        }        async with ClientSession(timeout=self._timeout) as session:            async with session.get(url, params=params) as response:                if response.status != 200:                    response.raise_for_status()                return await response.json()

Теперь нам нужно добавить GiphyClient в контейнер. У GiphyClient есть две зависимости, которые нужно передать при его создании: API ключ и таймаут запроса. Для этого нам нужно будет воспользоваться двумя новыми провайдерами из модуля dependency_injector.providers:

  • Провайдер Factory будет создавать GiphyClient.
  • Провайдер Configuration будет передавать API ключ и таймаут GiphyClient.

Отредактируем containers.py:

"""Application containers module."""from dependency_injector import containers, providersfrom dependency_injector.ext import aiohttpfrom aiohttp import webfrom . import giphy, viewsclass ApplicationContainer(containers.DeclarativeContainer):    """Application container."""    app = aiohttp.Application(web.Application)    config = providers.Configuration()    giphy_client = providers.Factory(        giphy.GiphyClient,        api_key=config.giphy.api_key,        timeout=config.giphy.request_timeout,    )    index_view = aiohttp.View(views.index)

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

Сначала используем, потом задаем значения.

Теперь давайте добавим файл конфигурации.
Будем использовать YAML.

Создайте пустой файл config.yml в корне проекта:

./ giphynavigator/    __init__.py    application.py    containers.py    giphy.py    views.py venv/ config.yml requirements.txt

И заполните его следующими строками:

giphy:  request_timeout: 10

Для передачи API ключа мы будем использовать переменную окружения GIPHY_API_KEY .

Теперь нам нужно отредактировать create_app() чтобы сделать 2 действие при старте приложения:

  • Загрузить конфигурацию из config.yml
  • Загрузить API ключ из переменной окружения GIPHY_API_KEY

Отредактируйте application.py:

"""Application module."""from aiohttp import webfrom .containers import ApplicationContainerdef create_app():    """Create and return aiohttp application."""    container = ApplicationContainer()    container.config.from_yaml('config.yml')    container.config.giphy.api_key.from_env('GIPHY_API_KEY')    app: web.Application = container.app()    app.container = container    app.add_routes([        web.get('/', container.index_view.as_view()),    ])    return app

Теперь нам нужно создать API ключ и установить его в переменную окружения.

Чтобы не тратить на это время сейчас используйте вот этот ключ:

export GIPHY_API_KEY=wBJ2wZG7SRqfrU9nPgPiWvORmloDyuL0

Для создания собственного ключа Giphy API следуйте этому руководству.

Создание Giphy API клиента и установка конфигурации завершена. Давайте перейдем к сервису поиска.

Сервис поиска


Пришло время добавить сервис поиска SearchService. Он будет:

  • Выполнять поиск
  • Форматировать полученный ответ

SearchService будет использовать GiphyClient.

Создайте пустой файл services.py в пакете giphynavigator:

./ giphynavigator/    __init__.py    application.py    containers.py    giphy.py    services.py    views.py venv/ requirements.txt

и добавьте в него следующие строки:

"""Services module."""from .giphy import GiphyClientclass SearchService:    def __init__(self, giphy_client: GiphyClient):        self._giphy_client = giphy_client    async def search(self, query, limit):        """Search for gifs and return formatted data."""        if not query:            return []        result = await self._giphy_client.search(query, limit)        return [{'url': gif['url']} for gif in result['data']]

При создании SearchService нужно передавать GiphyClient. Мы укажем это при добавлении SearchService в контейнер.

Отредактируем containers.py:

"""Application containers module."""from dependency_injector import containers, providersfrom dependency_injector.ext import aiohttpfrom aiohttp import webfrom . import giphy, services, viewsclass ApplicationContainer(containers.DeclarativeContainer):    """Application container."""    app = aiohttp.Application(web.Application)    config = providers.Configuration()    giphy_client = providers.Factory(        giphy.GiphyClient,        api_key=config.giphy.api_key,        timeout=config.giphy.request_timeout,    )    search_service = providers.Factory(        services.SearchService,        giphy_client=giphy_client,    )    index_view = aiohttp.View(views.index)

Создание сервиса поиска SearchService завершено. В следующем разделе мы подключим его к нашему представлению.

Подключаем поиск


Теперь мы готовы чтобы поиск заработал. Давайте используем SearchService в index представлении.

Отредактируйте views.py:

"""Views module."""from aiohttp import webfrom .services import SearchServiceasync def index(        request: web.Request,        search_service: SearchService,) -> web.Response:    query = request.query.get('query', 'Dependency Injector')    limit = int(request.query.get('limit', 10))    gifs = await search_service.search(query, limit)    return web.json_response(        {            'query': query,            'limit': limit,            'gifs': gifs,        },    )

Теперь изменим контейнер чтобы передавать зависимость SearchService в представление index при его вызове.

Отредактируйте containers.py:

"""Application containers module."""from dependency_injector import containers, providersfrom dependency_injector.ext import aiohttpfrom aiohttp import webfrom . import giphy, services, viewsclass ApplicationContainer(containers.DeclarativeContainer):    """Application container."""    app = aiohttp.Application(web.Application)    config = providers.Configuration()    giphy_client = providers.Factory(        giphy.GiphyClient,        api_key=config.giphy.api_key,        timeout=config.giphy.request_timeout,    )    search_service = providers.Factory(        services.SearchService,        giphy_client=giphy_client,    )    index_view = aiohttp.View(        views.index,        search_service=search_service,    )

Убедитесь что приложение работает или выполните:

adev runserver giphynavigator/application.py --livereload

и сделайте запрос к API в терминале:

http http://localhost:8000/ query=="wow,it works" limit==5

Вы увидите:

HTTP/1.1 200 OKContent-Length: 850Content-Type: application/json; charset=utf-8Date: Wed, 29 Jul 2020 22:22:55 GMTServer: Python/3.8 aiohttp/3.6.2{    "gifs": [        {            "url": "https://giphy.com/gifs/discoverychannel-nugget-gold-rush-rick-ness-KGGPIlnC4hr4u2s3pY"        },        {            "url": "https://giphy.com/gifs/primevideoin-ll1hyBS2IrUPLE0E71"        },        {            "url": "https://giphy.com/gifs/jackman-works-jackmanworks-l4pTgQoCrmXq8Txlu"        },        {            "url": "https://giphy.com/gifs/cat-massage-at-work-l46CzMaOlJXAFuO3u"        },        {            "url": "https://giphy.com/gifs/everwhatproductions-fun-christmas-3oxHQCI8tKXoeW4IBq"        },    ],    "limit": 10,    "query": "wow,it works"}



Поиск работает.

Немного рефакторинга


Наше представление index содержит два hardcoded значения:

  • Поисковый запрос по умолчанию
  • Лимит количества результатов

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

Отредактируйте views.py:

"""Views module."""from aiohttp import webfrom .services import SearchServiceasync def index(        request: web.Request,        search_service: SearchService,        default_query: str,        default_limit: int,) -> web.Response:    query = request.query.get('query', default_query)    limit = int(request.query.get('limit', default_limit))    gifs = await search_service.search(query, limit)    return web.json_response(        {            'query': query,            'limit': limit,            'gifs': gifs,        },    )

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

Отредактируйте containers.py:

"""Application containers module."""from dependency_injector import containers, providersfrom dependency_injector.ext import aiohttpfrom aiohttp import webfrom . import giphy, services, viewsclass ApplicationContainer(containers.DeclarativeContainer):    """Application container."""    app = aiohttp.Application(web.Application)    config = providers.Configuration()    giphy_client = providers.Factory(        giphy.GiphyClient,        api_key=config.giphy.api_key,        timeout=config.giphy.request_timeout,    )    search_service = providers.Factory(        services.SearchService,        giphy_client=giphy_client,    )    index_view = aiohttp.View(        views.index,        search_service=search_service,        default_query=config.search.default_query,        default_limit=config.search.default_limit,    )

Теперь давайте обновим конфигурационный файл.

Отредактируйте config.yml:

giphy:  request_timeout: 10search:  default_query: "Dependency Injector"  default_limit: 10

Рефакторинг закончен. Мы сделали наше приложение чище перенесли hardcoded значения в конфигурацию.

В следующем разделе мы добавим несколько тестов.

Добавляем тесты


Было бы неплохо добавить несколько тестов. Давай сделаем это. Мы будем использовать pytest и coverage.

Создайте пустой файл tests.py в пакете giphynavigator:

./ giphynavigator/    __init__.py    application.py    containers.py    giphy.py    services.py    tests.py    views.py venv/ requirements.txt

и добавьте в него следующие строки:

"""Tests module."""from unittest import mockimport pytestfrom giphynavigator.application import create_appfrom giphynavigator.giphy import GiphyClient@pytest.fixturedef app():    return create_app()@pytest.fixturedef client(app, aiohttp_client, loop):    return loop.run_until_complete(aiohttp_client(app))async def test_index(client, app):    giphy_client_mock = mock.AsyncMock(spec=GiphyClient)    giphy_client_mock.search.return_value = {        'data': [            {'url': 'https://giphy.com/gif1.gif'},            {'url': 'https://giphy.com/gif2.gif'},        ],    }    with app.container.giphy_client.override(giphy_client_mock):        response = await client.get(            '/',            params={                'query': 'test',                'limit': 10,            },        )    assert response.status == 200    data = await response.json()    assert data == {        'query': 'test',        'limit': 10,        'gifs': [            {'url': 'https://giphy.com/gif1.gif'},            {'url': 'https://giphy.com/gif2.gif'},        ],    }async def test_index_no_data(client, app):    giphy_client_mock = mock.AsyncMock(spec=GiphyClient)    giphy_client_mock.search.return_value = {        'data': [],    }    with app.container.giphy_client.override(giphy_client_mock):        response = await client.get('/')    assert response.status == 200    data = await response.json()    assert data['gifs'] == []async def test_index_default_params(client, app):    giphy_client_mock = mock.AsyncMock(spec=GiphyClient)    giphy_client_mock.search.return_value = {        'data': [],    }    with app.container.giphy_client.override(giphy_client_mock):        response = await client.get('/')    assert response.status == 200    data = await response.json()    assert data['query'] == app.container.config.search.default_query()    assert data['limit'] == app.container.config.search.default_limit()

Теперь давайте запустим тестирование и проверим покрытие:

py.test giphynavigator/tests.py --cov=giphynavigator

Вы увидите:

platform darwin -- Python 3.8.3, pytest-5.4.3, py-1.9.0, pluggy-0.13.1plugins: cov-2.10.0, aiohttp-0.3.0, asyncio-0.14.0collected 3 itemsgiphynavigator/tests.py ...                                     [100%]---------- coverage: platform darwin, python 3.8.3-final-0 -----------Name                            Stmts   Miss  Cover---------------------------------------------------giphynavigator/__init__.py          0      0   100%giphynavigator/__main__.py          5      5     0%giphynavigator/application.py      10      0   100%giphynavigator/containers.py       10      0   100%giphynavigator/giphy.py            16     11    31%giphynavigator/services.py          9      1    89%giphynavigator/tests.py            35      0   100%giphynavigator/views.py             7      0   100%---------------------------------------------------TOTAL                              92     17    82%

Обратите внимание как мы заменяем giphy_client моком с помощью метода .override(). Таким образом можно переопределить возвращаемое значения любого провайдера.

Работа закончена. Теперь давайте подведем итоги.

Заключение


Мы построили aiohttp REST API приложение применяя принцип dependency injection. Мы использовали Dependency Injector в качестве dependency injection фреймворка.

Преимущество, которое вы получаете с Dependency Injector это контейнер.

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

"""Application containers module."""from dependency_injector import containers, providersfrom dependency_injector.ext import aiohttpfrom aiohttp import webfrom . import giphy, services, viewsclass ApplicationContainer(containers.DeclarativeContainer):    """Application container."""    app = aiohttp.Application(web.Application)    config = providers.Configuration()    giphy_client = providers.Factory(        giphy.GiphyClient,        api_key=config.giphy.api_key,        timeout=config.giphy.request_timeout,    )    search_service = providers.Factory(        services.SearchService,        giphy_client=giphy_client,    )    index_view = aiohttp.View(        views.index,        search_service=search_service,        default_query=config.search.default_query,        default_limit=config.search.default_limit,    )

Что дальше?


Подробнее..

Мониторинг демон на Asyncio Dependency Injector руководство по применению dependency injection

09.08.2020 08:06:15 | Автор: admin
Привет,

Я создатель Dependency Injector. Это dependency injection фреймворк для Python.

Это еще одно руководство по построению приложений с помощью Dependency Injector.

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

Руководство состоит из таких частей:

  1. Что мы будем строить?
  2. Проверка инструментов
  3. Структура проекта
  4. Подготовка окружения
  5. Логирование и конфигурация
  6. Диспетчер
  7. Мониторинг example.com
  8. Мониторинг httpbin.org
  9. Тесты
  10. Заключение

Завершенный проект можно найти на Github.

Для старта желательно иметь:

  • Начальные знания по asyncio
  • Общее представление о принципе dependency injection

Что мы будем строить?


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

Демон будет посылать запросы к example.com и httpbin.org каждые несколько секунд. При получении ответа он будет записывать в лог такие данные:

  • Код ответа
  • Количество байт в ответе
  • Время, затраченное на выполнение запроса



Проверка инструментов


Мы будем использовать Docker и docker-compose. Давайте проверим, что они установлены:

docker --versiondocker-compose --version

Вывод должен выглядеть приблизительно так:

Docker version 19.03.12, build 48a66213fedocker-compose version 1.26.2, build eefe0d31

Если Docker или docker-compose не установлены, их нужно установить перед тем как продолжить. Следуйте этим руководствам:


Инструменты готовы. Переходим к структуре проекта.

Структура проекта


Создаем папку проекта и переходим в нее:

mkdir monitoring-daemon-tutorialcd monitoring-daemon-tutorial

Теперь нам нужно создать начальную структуру проекта. Создаем файлы и папки следуя структуре ниже. Все файлы пока будут пустыми. Мы наполним их позже.

Начальная структура проекта:

./ monitoringdaemon/    __init__.py    __main__.py    containers.py config.yml docker-compose.yml Dockerfile requirements.txt

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

Дальше нас ждет подготовка окружения.

Подготовка окружения


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

Для начала нужно определить зависимости. Мы будем использовать такие пакеты:

  • dependency-injector dependency injection фреймворк
  • aiohttp веб фреймворк (нам нужен только http клиент)
  • pyyaml библиотека для парсинга YAML файлов, используется для чтения конфига
  • pytest фреймворк для тестирования
  • pytest-asyncio библиотека-помогатор для тестирования asyncio приложений
  • pytest-cov библиотека-помогатор для измерения покрытия кода тестами

Добавим следующие строки в файл requirements.txt:

dependency-injectoraiohttppyyamlpytestpytest-asynciopytest-cov

И выполним в терминале:

pip install -r requirements.txt

Далее создаем Dockerfile. Он будет описывать процесс сборки и запуска нашего демона. Мы будем использовать python:3.8-buster в качестве базового образа.

Добавим следующие строки в файл Dockerfile:

FROM python:3.8-busterENV PYTHONUNBUFFERED=1WORKDIR /codeCOPY . /code/RUN apt-get install openssl \ && pip install --upgrade pip \ && pip install -r requirements.txt \ && rm -rf ~/.cacheCMD ["python", "-m", "monitoringdaemon"]

Последним шагом определим настройки docker-compose.

Добавим следующие строки в файл docker-compose.yml:

version: "3.7"services:  monitor:    build: ./    image: monitoring-daemon    volumes:      - "./:/code"

Все готово. Давайте запустим сборку образа и проверим что окружение настроено верно.

Выполним в терминале:

docker-compose build

Процесс сборки может занять несколько минут. В конце вы должны увидеть:

Successfully built 5b4ee5e76e35Successfully tagged monitoring-daemon:latest

После того как процесс сборки завершен запустим контейнер:

docker-compose up

Вы увидите:

Creating network "monitoring-daemon-tutorial_default" with the default driverCreating monitoring-daemon-tutorial_monitor_1 ... doneAttaching to monitoring-daemon-tutorial_monitor_1monitoring-daemon-tutorial_monitor_1 exited with code 0

Окружение готово. Контейнер запускается и завершает работу с кодом 0.

Следующим шагом мы настроим логирование и чтение файла конфигурации.

Логирование и конфигурация


В этом разделе мы настроим логирование и чтение файла конфигурации.

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

Добавим первые два компонента. Это объект конфигурации и функция настройки логирования.

Отредактируем containers.py:

"""Application containers module."""import loggingimport sysfrom dependency_injector import containers, providersclass ApplicationContainer(containers.DeclarativeContainer):    """Application container."""    config = providers.Configuration()    configure_logging = providers.Callable(        logging.basicConfig,        stream=sys.stdout,        level=config.log.level,        format=config.log.format,    )

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

Сначала используем, потом задаем значения.

Настройки логирования будут содержаться в конфигурационном файле.

Отредактируем config.yml:

log:  level: "INFO"  format: "[%(asctime)s] [%(levelname)s] [%(name)s]: %(message)s"

Теперь определим функцию, которая будет запускать наш демон. Её обычно называют main(). Она будет создавать контейнер. Контейнер будет использован для чтения конфигурационного файла и вызова функции настройки логирования.

Отредактируем __main__.py:

"""Main module."""from .containers import ApplicationContainerdef main() -> None:    """Run the application."""    container = ApplicationContainer()    container.config.from_yaml('config.yml')    container.configure_logging()if __name__ == '__main__':    main()

Контейнер первый объект в приложении. Он используется для получения всех остальных объектов.

Логирование и чтение конфигурации настроено. В следующем разделе мы создадим диспетчер мониторинговых задач.

Диспетчер


Пришло время добавить диспетчер мониторинговых задач.

Диспетчер будет содержать список мониторинговых задач и контролировать их выполнение. Он будет выполнять каждую задачу в соответствии с расписанием. Класс Monitor базовый класс для мониторинговых задач. Для создания конкретных задач нужно добавлять дочерние классы и реализовывать метод check().


Добавим диспетчер и базовый класс мониторинговой задачи.

Создадим dispatcher.py и monitors.py в пакете monitoringdaemon:

./ monitoringdaemon/    __init__.py    __main__.py    containers.py    dispatcher.py    monitors.py config.yml docker-compose.yml Dockerfile requirements.txt

Добавим следующие строки в файл monitors.py:

"""Monitors module."""import loggingclass Monitor:    def __init__(self, check_every: int) -> None:        self.check_every = check_every        self.logger = logging.getLogger(self.__class__.__name__)    async def check(self) -> None:        raise NotImplementedError()

и в файл dispatcher.py:

""""Dispatcher module."""import asyncioimport loggingimport signalimport timefrom typing import Listfrom .monitors import Monitorclass Dispatcher:    def __init__(self, monitors: List[Monitor]) -> None:        self._monitors = monitors        self._monitor_tasks: List[asyncio.Task] = []        self._logger = logging.getLogger(self.__class__.__name__)        self._stopping = False    def run(self) -> None:        asyncio.run(self.start())    async def start(self) -> None:        self._logger.info('Starting up')        for monitor in self._monitors:            self._monitor_tasks.append(                asyncio.create_task(self._run_monitor(monitor)),            )        asyncio.get_event_loop().add_signal_handler(signal.SIGTERM, self.stop)        asyncio.get_event_loop().add_signal_handler(signal.SIGINT, self.stop)        await asyncio.gather(*self._monitor_tasks, return_exceptions=True)        self.stop()    def stop(self) -> None:        if self._stopping:            return        self._stopping = True        self._logger.info('Shutting down')        for task, monitor in zip(self._monitor_tasks, self._monitors):            task.cancel()        self._logger.info('Shutdown finished successfully')    @staticmethod    async def _run_monitor(monitor: Monitor) -> None:        def _until_next(last: float) -> float:            time_took = time.time() - last            return monitor.check_every - time_took        while True:            time_start = time.time()            try:                await monitor.check()            except asyncio.CancelledError:                break            except Exception:                monitor.logger.exception('Error executing monitor check')            await asyncio.sleep(_until_next(last=time_start))

Диспетчер нужно добавить в контейнер.

Отредактируем containers.py:

"""Application containers module."""import loggingimport sysfrom dependency_injector import containers, providersfrom . import dispatcherclass ApplicationContainer(containers.DeclarativeContainer):    """Application container."""    config = providers.Configuration()    configure_logging = providers.Callable(        logging.basicConfig,        stream=sys.stdout,        level=config.log.level,        format=config.log.format,    )    dispatcher = providers.Factory(        dispatcher.Dispatcher,        monitors=providers.List(            # TODO: add monitors        ),    )

Каждый компонент добавляется в контейнер.

В завершении нам нужно обновить функцию main(). Мы получим диспетчер из контейнера и вызовем его метод run().

Отредактируем __main__.py:

"""Main module."""from .containers import ApplicationContainerdef main() -> None:    """Run the application."""    container = ApplicationContainer()    container.config.from_yaml('config.yml')    container.configure_logging()    dispatcher = container.dispatcher()    dispatcher.run()if __name__ == '__main__':    main()

Теперь запустим демон и проверим его работу.

Выполним в терминале:

docker-compose up

Вывод должен выглядеть так:

Starting monitoring-daemon-tutorial_monitor_1 ... doneAttaching to monitoring-daemon-tutorial_monitor_1monitor_1  | [2020-08-08 16:12:35,772] [INFO] [Dispatcher]: Starting upmonitor_1  | [2020-08-08 16:12:35,774] [INFO] [Dispatcher]: Shutting downmonitor_1  | [2020-08-08 16:12:35,774] [INFO] [Dispatcher]: Shutdown finished successfullymonitoring-daemon-tutorial_monitor_1 exited with code 0

Все работает верно. Диспетчер запускается и выключается так как мониторинговых задач нет.

К концу этого раздела каркас нашего демона готов. В следующем разделе мы добавим первую мониторинговую задачу.

Мониторинг example.com


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

Мы начнем с расширения нашей модели классов новым типом мониторинговой задачи HttpMonitor.

HttpMonitor это дочерний класс Monitor. Мы реализуем метод check(). Он будет отправлять HTTP запрос и логировать полученный ответ. Детали выполнения HTTP запроса будут делегированы классу HttpClient.


Сперва добавим HttpClient.

Создадим файл http.py в пакете monitoringdaemon:

./ monitoringdaemon/    __init__.py    __main__.py    containers.py    dispatcher.py    http.py    monitors.py config.yml docker-compose.yml Dockerfile requirements.txt

И добавим в него следующие строки:

"""Http client module."""from aiohttp import ClientSession, ClientTimeout, ClientResponseclass HttpClient:    async def request(self, method: str, url: str, timeout: int) -> ClientResponse:        async with ClientSession(timeout=ClientTimeout(timeout)) as session:            async with session.request(method, url) as response:                return response

Далее нужно добавить HttpClient в контейнер.

Отредактируем containers.py:

"""Application containers module."""import loggingimport sysfrom dependency_injector import containers, providersfrom . import http, dispatcherclass ApplicationContainer(containers.DeclarativeContainer):    """Application container."""    config = providers.Configuration()    configure_logging = providers.Callable(        logging.basicConfig,        stream=sys.stdout,        level=config.log.level,        format=config.log.format,    )    http_client = providers.Factory(http.HttpClient)    dispatcher = providers.Factory(        dispatcher.Dispatcher,        monitors=providers.List(            # TODO: add monitors        ),    )

Теперь мы готовы добавить HttpMonitor. Добавим его в модуль monitors.

Отредактируем monitors.py:

"""Monitors module."""import loggingimport timefrom typing import Dict, Anyfrom .http import HttpClientclass Monitor:    def __init__(self, check_every: int) -> None:        self.check_every = check_every        self.logger = logging.getLogger(self.__class__.__name__)    async def check(self) -> None:        raise NotImplementedError()class HttpMonitor(Monitor):    def __init__(            self,            http_client: HttpClient,            options: Dict[str, Any],    ) -> None:        self._client = http_client        self._method = options.pop('method')        self._url = options.pop('url')        self._timeout = options.pop('timeout')        super().__init__(check_every=options.pop('check_every'))    @property    def full_name(self) -> str:        return '{0}.{1}(url="{2}")'.format(__name__, self.__class__.__name__, self._url)    async def check(self) -> None:        time_start = time.time()        response = await self._client.request(            method=self._method,            url=self._url,            timeout=self._timeout,        )        time_end = time.time()        time_took = time_end - time_start        self.logger.info(            'Response code: %s, content length: %s, request took: %s seconds',            response.status,            response.content_length,            round(time_took, 3)        )

У нас все готово для добавления проверки http://example.com. Нам нужно сделать два изменения в контейнере:

  • Добавить фабрику example_monitor.
  • Передать example_monitor в диспетчер.

Отредактируем containers.py:

"""Application containers module."""import loggingimport sysfrom dependency_injector import containers, providersfrom . import http, monitors, dispatcherclass ApplicationContainer(containers.DeclarativeContainer):    """Application container."""    config = providers.Configuration()    configure_logging = providers.Callable(        logging.basicConfig,        stream=sys.stdout,        level=config.log.level,        format=config.log.format,    )    http_client = providers.Factory(http.HttpClient)    example_monitor = providers.Factory(        monitors.HttpMonitor,        http_client=http_client,        options=config.monitors.example,    )    dispatcher = providers.Factory(        dispatcher.Dispatcher,        monitors=providers.List(            example_monitor,        ),    )

Провайдер example_monitor имеет зависимость от значений конфигурации. Давайте добавим эти значения:

Отредактируем config.yml:

log:  level: "INFO"  format: "[%(asctime)s] [%(levelname)s] [%(name)s]: %(message)s"monitors:  example:    method: "GET"    url: "http://example.com"    timeout: 5    check_every: 5

Все готово. Запускаем демон и проверяем работу.

Выполняем в терминале:

docker-compose up

И видим подобный вывод:

Starting monitoring-daemon-tutorial_monitor_1 ... doneAttaching to monitoring-daemon-tutorial_monitor_1monitor_1  | [2020-08-08 17:06:41,965] [INFO] [Dispatcher]: Starting upmonitor_1  | [2020-08-08 17:06:42,033] [INFO] [HttpMonitor]: Checkmonitor_1  |     GET http://example.commonitor_1  |     response code: 200monitor_1  |     content length: 648monitor_1  |     request took: 0.067 secondsmonitor_1  |monitor_1  | [2020-08-08 17:06:47,040] [INFO] [HttpMonitor]: Checkmonitor_1  |     GET http://example.commonitor_1  |     response code: 200monitor_1  |     content length: 648monitor_1  |     request took: 0.073 seconds

Наш демон может следить за наличием доступа к http://example.com.

Давайте добавим мониторинг https://httpbin.org.

Мониторинг httpbin.org


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

Добавление мониторинговой задачи для https://httpbin.org будет сделать легче, так как все компоненты уже готовы. Нам просто нужно добавить новый провайдер в контейнер и обновить конфигурацию.

Отредактируем containers.py:

"""Application containers module."""import loggingimport sysfrom dependency_injector import containers, providersfrom . import http, monitors, dispatcherclass ApplicationContainer(containers.DeclarativeContainer):    """Application container."""    config = providers.Configuration()    configure_logging = providers.Callable(        logging.basicConfig,        stream=sys.stdout,        level=config.log.level,        format=config.log.format,    )    http_client = providers.Factory(http.HttpClient)    example_monitor = providers.Factory(        monitors.HttpMonitor,        http_client=http_client,        options=config.monitors.example,    )    httpbin_monitor = providers.Factory(        monitors.HttpMonitor,        http_client=http_client,        options=config.monitors.httpbin,    )    dispatcher = providers.Factory(        dispatcher.Dispatcher,        monitors=providers.List(            example_monitor,            httpbin_monitor,        ),    )

Отредактируем config.yml:

log:  level: "INFO"  format: "[%(asctime)s] [%(levelname)s] [%(name)s]: %(message)s"monitors:  example:    method: "GET"    url: "http://example.com"    timeout: 5    check_every: 5  httpbin:    method: "GET"    url: "https://httpbin.org/get"    timeout: 5    check_every: 5

Запустим демон и проверим логи.

Выполним в терминале:

docker-compose up

И видим подобный вывод:

Starting monitoring-daemon-tutorial_monitor_1 ... doneAttaching to monitoring-daemon-tutorial_monitor_1monitor_1  | [2020-08-08 18:09:08,540] [INFO] [Dispatcher]: Starting upmonitor_1  | [2020-08-08 18:09:08,618] [INFO] [HttpMonitor]: Checkmonitor_1  |     GET http://example.commonitor_1  |     response code: 200monitor_1  |     content length: 648monitor_1  |     request took: 0.077 secondsmonitor_1  |monitor_1  | [2020-08-08 18:09:08,722] [INFO] [HttpMonitor]: Checkmonitor_1  |     GET https://httpbin.org/getmonitor_1  |     response code: 200monitor_1  |     content length: 310monitor_1  |     request took: 0.18 secondsmonitor_1  |monitor_1  | [2020-08-08 18:09:13,619] [INFO] [HttpMonitor]: Checkmonitor_1  |     GET http://example.commonitor_1  |     response code: 200monitor_1  |     content length: 648monitor_1  |     request took: 0.066 secondsmonitor_1  |monitor_1  | [2020-08-08 18:09:13,681] [INFO] [HttpMonitor]: Checkmonitor_1  |     GET https://httpbin.org/getmonitor_1  |     response code: 200monitor_1  |     content length: 310monitor_1  |     request took: 0.126 seconds

Функциональная часть завершена. Демон следит за наличием доступа к http://example.com и https://httpbin.org.

В следующем разделе мы добавим несколько тестов.

Тесты


Было бы неплохо добавить несколько тестов. Давайте сделаем это.

Создаем файл tests.py в пакете monitoringdaemon:

./ monitoringdaemon/    __init__.py    __main__.py    containers.py    dispatcher.py    http.py    monitors.py    tests.py config.yml docker-compose.yml Dockerfile requirements.txt

и добавляем в него следующие строки:

"""Tests module."""import asyncioimport dataclassesfrom unittest import mockimport pytestfrom .containers import ApplicationContainer@dataclasses.dataclassclass RequestStub:    status: int    content_length: int@pytest.fixturedef container():    container = ApplicationContainer()    container.config.from_dict({        'log': {            'level': 'INFO',            'formant': '[%(asctime)s] [%(levelname)s] [%(name)s]: %(message)s',        },        'monitors': {            'example': {                'method': 'GET',                'url': 'http://fake-example.com',                'timeout': 1,                'check_every': 1,            },            'httpbin': {                'method': 'GET',                'url': 'https://fake-httpbin.org/get',                'timeout': 1,                'check_every': 1,            },        },    })    return container@pytest.mark.asyncioasync def test_example_monitor(container, caplog):    caplog.set_level('INFO')    http_client_mock = mock.AsyncMock()    http_client_mock.request.return_value = RequestStub(        status=200,        content_length=635,    )    with container.http_client.override(http_client_mock):        example_monitor = container.example_monitor()        await example_monitor.check()    assert 'http://fake-example.com' in caplog.text    assert 'response code: 200' in caplog.text    assert 'content length: 635' in caplog.text@pytest.mark.asyncioasync def test_dispatcher(container, caplog, event_loop):    caplog.set_level('INFO')    example_monitor_mock = mock.AsyncMock()    httpbin_monitor_mock = mock.AsyncMock()    with container.example_monitor.override(example_monitor_mock), \            container.httpbin_monitor.override(httpbin_monitor_mock):        dispatcher = container.dispatcher()        event_loop.create_task(dispatcher.start())        await asyncio.sleep(0.1)        dispatcher.stop()    assert example_monitor_mock.check.called    assert httpbin_monitor_mock.check.called

Для запуска тестов выполним в терминале:

docker-compose run --rm monitor py.test monitoringdaemon/tests.py --cov=monitoringdaemon

Должен получиться подобный результат:

platform linux -- Python 3.8.3, pytest-6.0.1, py-1.9.0, pluggy-0.13.1rootdir: /codeplugins: asyncio-0.14.0, cov-2.10.0collected 2 itemsmonitoringdaemon/tests.py ..                                    [100%]----------- coverage: platform linux, python 3.8.3-final-0 -----------Name                             Stmts   Miss  Cover----------------------------------------------------monitoringdaemon/__init__.py         0      0   100%monitoringdaemon/__main__.py         9      9     0%monitoringdaemon/containers.py      11      0   100%monitoringdaemon/dispatcher.py      43      5    88%monitoringdaemon/http.py             6      3    50%monitoringdaemon/monitors.py        23      1    96%monitoringdaemon/tests.py           37      0   100%----------------------------------------------------TOTAL                              129     18    86%

Обратите внимание как в тесте test_example_monitor мы подменяем HttpClient моком с помощью метода .override(). Таким образом можно переопределить возвращаемое значения любого провайдера.

Такие же действия выполняются в тесте test_dispatcher для подмены моками мониторинговых задач.


Заключение


Мы построили мониторинг демон на базе asyncio применяя принцип dependency injection. Мы использовали Dependency Injector в качестве dependency injection фреймворка.

Преимущество, которое вы получаете с Dependency Injector это контейнер.

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

"""Application containers module."""import loggingimport sysfrom dependency_injector import containers, providersfrom . import http, monitors, dispatcherclass ApplicationContainer(containers.DeclarativeContainer):    """Application container."""    config = providers.Configuration()    configure_logging = providers.Callable(        logging.basicConfig,        stream=sys.stdout,        level=config.log.level,        format=config.log.format,    )    http_client = providers.Factory(http.HttpClient)    example_monitor = providers.Factory(        monitors.HttpMonitor,        http_client=http_client,        options=config.monitors.example,    )    httpbin_monitor = providers.Factory(        monitors.HttpMonitor,        http_client=http_client,        options=config.monitors.httpbin,    )    dispatcher = providers.Factory(        dispatcher.Dispatcher,        monitors=providers.List(            example_monitor,            httpbin_monitor,        ),    )


Контейнер как карта вашего приложения. Вы всегда знайте что от чего зависит.

Что дальше?


Подробнее..

CLI приложение Dependency Injector руководство по применению dependency injection Вопросы ответы

14.08.2020 02:18:06 | Автор: admin
Привет,

Я создатель Dependency Injector. Это dependency injection фреймворк для Python.

Это завершающее руководство по построению приложений с помощью Dependency Injector. Прошлые руководства рассказывают как построить веб-приложение на Flask, REST API на Aiohttp и мониторинг демона на Asyncio применяя принцип dependency injection.

Сегодня хочу показать как можно построить консольное (CLI) приложение.

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

Руководство состоит из таких частей:

  1. Что мы будем строить?
  2. Подготовка окружения
  3. Структура проекта
  4. Установка зависимостей
  5. Фикстуры
  6. Контейнер
  7. Работа с csv
  8. Работа с sqlite
  9. Провайдер Selector
  10. Тесты
  11. Заключение
  12. PS: вопросы и ответы

Завершенный проект можно найти на Github.

Для старта необходимо иметь:

  • Python 3.5+
  • Virtual environment

И желательно иметь общее представление о принципе dependency injection.

Что мы будем строить?


Мы будем строить CLI (консольное) приложение, которое ищет фильмы. Назовем его Movie Lister.

Как работает Movie Lister?

  • У нас есть база данных фильмов
  • О каждом фильме известна такая информация:
    • Название
    • Год выпуска
    • Имя режиссёра
  • База данных распространяется в двух форматах:
    • Csv файл
    • Sqlite база данных
  • Приложение выполняет поиск по базе данных по таким критериям:
    • Имя режиссёра
    • Год выпуска
  • Другие форматы баз данных могут быть добавлены в будущем

Movie Lister это приложение-пример, которое используется в статье Мартина Фаулера о dependency injection и inversion of control.

Вот как выглядит диаграмма классов приложения Movie Lister:


Обязанности между классами распределены так:

  • MovieLister отвечает за поиск
  • MovieFinder отвечает за извлечение данных из базы
  • Movie класс сущности фильм

Подготовка окружения


Начнём с подготовки окружения.

В первую очередь нам нужно создать папку проекта и virtual environment:

mkdir movie-lister-tutorialcd movie-lister-tutorialpython3 -m venv venv

Теперь давайте активируем virtual environment:

. venv/bin/activate

Окружение готово. Теперь займемся структурой проекта.

Структура проекта


В этом разделе организуем структуру проекта.

Создадим в текущей папке следующую структуру. Все файлы пока оставляем пустыми.

Начальная структура:

./ movies/    __init__.py    __main__.py    containers.py venv/ config.yml requirements.txt

Установка зависимостей


Пришло время установить зависимости. Мы будем использовать такие пакеты:

  • dependency-injector dependency injection фреймворк
  • pyyaml библиотека для парсинга YAML файлов, используется для чтения конфига
  • pytest фреймворк для тестирования
  • pytest-cov библиотека-помогатор для измерения покрытия кода тестами

Добавим следующие строки в файл requirements.txt:

dependency-injectorpyyamlpytestpytest-cov

И выполним в терминале:

pip install -r requirements.txt

Установка зависимостей завершена. Переходим к фикстурам.

Фикстуры


В это разделе мы добавим фикстуры. Фикстурами называют тестовые данные.

Мы создадим скрипт, который создаст тестовые базы данных.

Добавляем директорию data/ в корень проекта и внутрь добавляем файл fixtures.py:

./ data/    fixtures.py movies/    __init__.py    __main__.py    containers.py venv/ config.yml requirements.txt

Далее редактируем fixtures.py:

"""Fixtures module."""import csvimport sqlite3import pathlibSAMPLE_DATA = [    ('The Hunger Games: Mockingjay - Part 2', 2015, 'Francis Lawrence'),    ('Rogue One: A Star Wars Story', 2016, 'Gareth Edwards'),    ('The Jungle Book', 2016, 'Jon Favreau'),]FILE = pathlib.Path(__file__)DIR = FILE.parentCSV_FILE = DIR / 'movies.csv'SQLITE_FILE = DIR / 'movies.db'def create_csv(movies_data, path):    with open(path, 'w') as opened_file:        writer = csv.writer(opened_file)        for row in movies_data:            writer.writerow(row)def create_sqlite(movies_data, path):    with sqlite3.connect(path) as db:        db.execute(            'CREATE TABLE IF NOT EXISTS movies '            '(title text, year int, director text)'        )        db.execute('DELETE FROM movies')        db.executemany('INSERT INTO movies VALUES (?,?,?)', movies_data)def main():    create_csv(SAMPLE_DATA, CSV_FILE)    create_sqlite(SAMPLE_DATA, SQLITE_FILE)    print('OK')if __name__ == '__main__':    main()

Теперь выполним в терминале:

python data/fixtures.py

Скрипт должен вывести OK при успешном завершении.

Проверим, что файлы movies.csv и movies.db появились в директории data/:

./ data/    fixtures.py    movies.csv    movies.db movies/    __init__.py    __main__.py    containers.py venv/ config.yml requirements.txt

Фикстуры созданы. Продолжаем.

Контейнер


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

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

Отредактируем containers.py:

"""Containers module."""from dependency_injector import containersclass ApplicationContainer(containers.DeclarativeContainer):    ...

Контейнер пока пуст. Мы добавим провайдеры в следующих секциях.

Давайте еще добавим функцию main(). Её обязанность запускать приложение. Пока она будет только создавать контейнер.

Отредактируем __main__.py:

"""Main module."""from .containers import ApplicationContainerdef main():    container = ApplicationContainer()if __name__ == '__main__':    main()

Контейнер первый объект в приложении. Он используется для получения всех остальных объектов.

Работа с csv


Теперь добавим все что нужно для работы с csv файлами.

Нам понадобится:

  • Сущность Movie
  • Базовый класс MovieFinder
  • Его реализация CsvMovieFinder
  • Класс MovieLister

После добавления каждого компонента будем добавлять его в контейнер.



Создаем файл entities.py в пакете movies:

./ data/    fixtures.py    movies.csv    movies.db movies/    __init__.py    __main__.py    containers.py    entities.py venv/ config.yml requirements.txt

и добавляем внутрь следующие строки:

"""Movie entities module."""class Movie:    def __init__(self, title: str, year: int, director: str):        self.title = str(title)        self.year = int(year)        self.director = str(director)    def __repr__(self):        return '{0}(title={1}, year={2}, director={3})'.format(            self.__class__.__name__,            repr(self.title),            repr(self.year),            repr(self.director),        )

Теперь нам нужно добавить фабрику Movie в контейнер. Для этого нам понадобиться модуль providers из dependency_injector.

Отредактируем containers.py:

"""Containers module."""from dependency_injector import containers, providersfrom . import entitiesclass ApplicationContainer(containers.DeclarativeContainer):    movie = providers.Factory(entities.Movie)

Не забудьте убрать эллипсис (...). В контейнере уже есть провайдеры и он больше не нужен.

Переходим к созданию finders.

Создаем файл finders.py в пакете movies:

./ data/    fixtures.py    movies.csv    movies.db movies/    __init__.py    __main__.py    containers.py    entities.py    finders.py venv/ config.yml requirements.txt

и добавляем внутрь следующие строки:

"""Movie finders module."""import csvfrom typing import Callable, Listfrom .entities import Movieclass MovieFinder:    def __init__(self, movie_factory: Callable[..., Movie]) -> None:        self._movie_factory = movie_factory    def find_all(self) -> List[Movie]:        raise NotImplementedError()class CsvMovieFinder(MovieFinder):    def __init__(            self,            movie_factory: Callable[..., Movie],            path: str,            delimiter: str,    ) -> None:        self._csv_file_path = path        self._delimiter = delimiter        super().__init__(movie_factory)    def find_all(self) -> List[Movie]:        with open(self._csv_file_path) as csv_file:            csv_reader = csv.reader(csv_file, delimiter=self._delimiter)            return [self._movie_factory(*row) for row in csv_reader]

Теперь добавим CsvMovieFinder в контейнер.

Отредактируем containers.py:

"""Containers module."""from dependency_injector import containers, providersfrom . import finders, entitiesclass ApplicationContainer(containers.DeclarativeContainer):    config = providers.Configuration()    movie = providers.Factory(entities.Movie)    csv_finder = providers.Singleton(        finders.CsvMovieFinder,        movie_factory=movie.provider,        path=config.finder.csv.path,        delimiter=config.finder.csv.delimiter,    )

У CsvMovieFinder есть зависимость от фабрики Movie. CsvMovieFinder нуждается в фабрике так как будет создавать объекты Movie по мере того как будет читать данные из файла. Для того чтобы передать фабрику мы используем атрибут .provider. Это называется делегирование провайдеров. Если мы укажем фабрику movie как зависимость, она будет вызвана когда csv_finder будет создавать CsvMovieFinder и в качестве инъекции будет передан объект Movie. Используя атрибут .provider в качестве инъекции будет передам сам провайдер.

У csv_finder еще есть зависимость от нескольких опций конфигурации. Мы добавили провайдер Сonfiguration чтобы передать эти зависимости.

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

Сначала используем, потом задаем значения.

Теперь давайте добавим значения конфигурации.

Отредактируем config.yml:

finder:  csv:    path: "data/movies.csv"    delimiter: ","

Значения установлены в конфигурационный файл. Обновим функцию main() чтобы указать его расположение.

Отредактируем __main__.py:

"""Main module."""from .containers import ApplicationContainerdef main():    container = ApplicationContainer()    container.config.from_yaml('config.yml')if __name__ == '__main__':    main()

Переходим к listers.

Создаем файл listers.py в пакете movies:

./ data/    fixtures.py    movies.csv    movies.db movies/    __init__.py    __main__.py    containers.py    entities.py    finders.py    listers.py venv/ config.yml requirements.txt

и добавляем внутрь следующие строки:

"""Movie listers module."""from .finders import MovieFinderclass MovieLister:    def __init__(self, movie_finder: MovieFinder):        self._movie_finder = movie_finder    def movies_directed_by(self, director):        return [            movie for movie in self._movie_finder.find_all()            if movie.director == director        ]    def movies_released_in(self, year):        return [            movie for movie in self._movie_finder.find_all()            if movie.year == year        ]

Обновляем containers.py:

"""Containers module."""from dependency_injector import containers, providersfrom . import finders, listers, entitiesclass ApplicationContainer(containers.DeclarativeContainer):    config = providers.Configuration()    movie = providers.Factory(entities.Movie)    csv_finder = providers.Singleton(        finders.CsvMovieFinder,        movie_factory=movie.provider,        path=config.finder.csv.path,        delimiter=config.finder.csv.delimiter,    )    lister = providers.Factory(        listers.MovieLister,        movie_finder=csv_finder,    )

Все компоненты созданы и добавлены в контейнер.

В завершение обновляем функцию main().

Отредактируем __main__.py:

"""Main module."""from .containers import ApplicationContainerdef main():    container = ApplicationContainer()    container.config.from_yaml('config.yml')    lister = container.lister()    print(        'Francis Lawrence movies:',        lister.movies_directed_by('Francis Lawrence'),    )    print(        '2016 movies:',        lister.movies_released_in(2016),    )if __name__ == '__main__':    main()

Все готово. Теперь запустим приложение.

Выполним в терминале:

python -m movies

Вы увидите:

Francis Lawrence movies: [Movie(title='The Hunger Games: Mockingjay - Part 2', year=2015, director='Francis Lawrence')]2016 movies: [Movie(title='Rogue One: A Star Wars Story', year=2016, director='Gareth Edwards'), Movie(title='The Jungle Book', year=2016, director='Jon Favreau')]

Наше приложение работает с базой данных фильмов в формате csv. Нам нужно еще добавить поддержку формата sqlite. Разберемся с этим в следующем разделе.

Работа с sqlite


В это разделе мы добавим другой тип MovieFinder SqliteMovieFinder.

Отредактируем finders.py:

"""Movie finders module."""import csvimport sqlite3from typing import Callable, Listfrom .entities import Movieclass MovieFinder:    def __init__(self, movie_factory: Callable[..., Movie]) -> None:        self._movie_factory = movie_factory    def find_all(self) -> List[Movie]:        raise NotImplementedError()class CsvMovieFinder(MovieFinder):    def __init__(            self,            movie_factory: Callable[..., Movie],            path: str,            delimiter: str,    ) -> None:        self._csv_file_path = path        self._delimiter = delimiter        super().__init__(movie_factory)    def find_all(self) -> List[Movie]:        with open(self._csv_file_path) as csv_file:            csv_reader = csv.reader(csv_file, delimiter=self._delimiter)            return [self._movie_factory(*row) for row in csv_reader]class SqliteMovieFinder(MovieFinder):    def __init__(            self,            movie_factory: Callable[..., Movie],            path: str,    ) -> None:        self._database = sqlite3.connect(path)        super().__init__(movie_factory)    def find_all(self) -> List[Movie]:        with self._database as db:            rows = db.execute('SELECT title, year, director FROM movies')            return [self._movie_factory(*row) for row in rows]

Добавляем провайдер sqlite_finder в контейнер и указываем его в качестве зависимости для провайдера lister.

Отредактируем containers.py:

"""Containers module."""from dependency_injector import containers, providersfrom . import finders, listers, entitiesclass ApplicationContainer(containers.DeclarativeContainer):    config = providers.Configuration()    movie = providers.Factory(entities.Movie)    csv_finder = providers.Singleton(        finders.CsvMovieFinder,        movie_factory=movie.provider,        path=config.finder.csv.path,        delimiter=config.finder.csv.delimiter,    )    sqlite_finder = providers.Singleton(        finders.SqliteMovieFinder,        movie_factory=movie.provider,        path=config.finder.sqlite.path,    )    lister = providers.Factory(        listers.MovieLister,        movie_finder=sqlite_finder,    )

У провайдера sqlite_finder есть зависимость от опций конфигурации, которые мы еще не определили. Обновим файл конфигурации:

Отредактируем config.yml:

finder:  csv:    path: "data/movies.csv"    delimiter: ","  sqlite:    path: "data/movies.db"

Готово. Давайте проверим.

Выполняем в терминале:

python -m movies

Вы увидите:

Francis Lawrence movies: [Movie(title='The Hunger Games: Mockingjay - Part 2', year=2015, director='Francis Lawrence')]2016 movies: [Movie(title='Rogue One: A Star Wars Story', year=2016, director='Gareth Edwards'), Movie(title='The Jungle Book', year=2016, director='Jon Favreau')]

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

Провайдер Selector


В этом разделе мы сделаем наше приложение более гибким.

Больше не нужно будет делать изменения в коде для переключения между csv и sqlite форматами. Мы реализуем переключатель на базе переменной окружения MOVIE_FINDER_TYPE:

  • Когда MOVIE_FINDER_TYPE=csv приложения использует формат csv.
  • Когда MOVIE_FINDER_TYPE=sqlite приложения использует формат sqlite.

В этом нам поможет провайдер Selector. Он выбирает провайдер на основе опции конфигурации (документация).

Отредактрируем containers.py:

"""Containers module."""from dependency_injector import containers, providersfrom . import finders, listers, entitiesclass ApplicationContainer(containers.DeclarativeContainer):    config = providers.Configuration()    movie = providers.Factory(entities.Movie)    csv_finder = providers.Singleton(        finders.CsvMovieFinder,        movie_factory=movie.provider,        path=config.finder.csv.path,        delimiter=config.finder.csv.delimiter,    )    sqlite_finder = providers.Singleton(        finders.SqliteMovieFinder,        movie_factory=movie.provider,        path=config.finder.sqlite.path,    )    finder = providers.Selector(        config.finder.type,        csv=csv_finder,        sqlite=sqlite_finder,    )    lister = providers.Factory(        listers.MovieLister,        movie_finder=finder,    )

Мы создали провайдер finder и указали его в качестве зависимости для провайдера lister. Провайдер finder выбирает между провайдерами csv_finder и sqlite_finder во время выполнения. Выбор зависит от значения переключателя.

Переключателем является опция конфигурации config.finder.type. Когда ее значение csv используется провайдер из ключа csv. Аналогично для sqlite.

Теперь нам нужно считать значение config.finder.type из переменной окружения MOVIE_FINDER_TYPE.

Отредактируем __main__.py:

"""Main module."""from .containers import ApplicationContainerdef main():    container = ApplicationContainer()    container.config.from_yaml('config.yml')    container.config.finder.type.from_env('MOVIE_FINDER_TYPE')    lister = container.lister()    print(        'Francis Lawrence movies:',        lister.movies_directed_by('Francis Lawrence'),    )    print(        '2016 movies:',        lister.movies_released_in(2016),    )if __name__ == '__main__':    main()

Готово.

Выполним в терминале следующие команды:

MOVIE_FINDER_TYPE=csv python -m moviesMOVIE_FINDER_TYPE=sqlite python -m movies

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

Francis Lawrence movies: [Movie(title='The Hunger Games: Mockingjay - Part 2', year=2015, director='Francis Lawrence')]2016 movies: [Movie(title='Rogue One: A Star Wars Story', year=2016, director='Gareth Edwards'), Movie(title='The Jungle Book', year=2016, director='Jon Favreau')]

В этом разделе познакомились с провайдером Selector. C помощью этого провайдера можно делать приложение более гибким. Значение переключателя может быть задано из любого источника: конфигурационного файла, словаря, другого провайдера.

Подсказка:
Переопределение значения конфигурации из другого провайдера позволяет реализовать перегрузку конфигурации в приложении без перезапуска, на горячую.
Для этого нужно использовать делегирование провайдеров и метод .override().

В следующем разделе добавим несколько тестов.

Тесты


В завершение добавим несколько тестов.

Создаём файл tests.py в пакете movies:

./ data/    fixtures.py    movies.csv    movies.db movies/    __init__.py    __main__.py    containers.py    entities.py    finders.py    listers.py    tests.py venv/ config.yml requirements.txt

и добавляем в него следующие строки:

"""Tests module."""from unittest import mockimport pytestfrom .containers import ApplicationContainer@pytest.fixturedef container():    container = ApplicationContainer()    container.config.from_dict({        'finder': {            'type': 'csv',            'csv': {                'path': '/fake-movies.csv',                'delimiter': ',',            },            'sqlite': {                'path': '/fake-movies.db',            },        },    })    return containerdef test_movies_directed_by(container):    finder_mock = mock.Mock()    finder_mock.find_all.return_value = [        container.movie('The 33', 2015, 'Patricia Riggen'),        container.movie('The Jungle Book', 2016, 'Jon Favreau'),    ]    with container.finder.override(finder_mock):        lister = container.lister()        movies = lister.movies_directed_by('Jon Favreau')    assert len(movies) == 1    assert movies[0].title == 'The Jungle Book'def test_movies_released_in(container):    finder_mock = mock.Mock()    finder_mock.find_all.return_value = [        container.movie('The 33', 2015, 'Patricia Riggen'),        container.movie('The Jungle Book', 2016, 'Jon Favreau'),    ]    with container.finder.override(finder_mock):        lister = container.lister()        movies = lister.movies_released_in(2015)    assert len(movies) == 1    assert movies[0].title == 'The 33'

Теперь запустим тестирование и проверим покрытие:

pytest movies/tests.py --cov=movies

Вы увидите:

platform darwin -- Python 3.8.3, pytest-5.4.3, py-1.9.0, pluggy-0.13.1plugins: cov-2.10.0collected 2 itemsmovies/tests.py ..                                              [100%]---------- coverage: platform darwin, python 3.8.3-final-0 -----------Name                   Stmts   Miss  Cover------------------------------------------movies/__init__.py         0      0   100%movies/__main__.py        10     10     0%movies/containers.py       9      0   100%movies/entities.py         7      1    86%movies/finders.py         26     13    50%movies/listers.py          8      0   100%movies/tests.py           24      0   100%------------------------------------------TOTAL                     84     24    71%

Мы использовали метод .override() провайдера finder. Провайдер переопределяется моком. При обращении к провайдеру finder теперь будет возвращен переопределяющий мок.

Работа закончена. Теперь давайте подведем итоги.

Заключение


Мы построили консольное (CLI) приложение применяя принцип dependency injection. Мы использовали Dependency Injector в качестве dependency injection фреймворка.

Преимущество, которое вы получаете с Dependency Injector это контейнер.

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

"""Containers module."""from dependency_injector import containers, providersfrom . import finders, listers, entitiesclass ApplicationContainer(containers.DeclarativeContainer):    config = providers.Configuration()    movie = providers.Factory(entities.Movie)    csv_finder = providers.Singleton(        finders.CsvMovieFinder,        movie_factory=movie.provider,        path=config.finder.csv.path,        delimiter=config.finder.csv.delimiter,    )    sqlite_finder = providers.Singleton(        finders.SqliteMovieFinder,        movie_factory=movie.provider,        path=config.finder.sqlite.path,    )    finder = providers.Selector(        config.finder.type,        csv=csv_finder,        sqlite=sqlite_finder,    )    lister = providers.Factory(        listers.MovieLister,        movie_finder=finder,    )


Контейнер как карта вашего приложения. Вы всегда знайте что от чего зависит.

PS: вопросы и ответы


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

Я подготовил ответы:

Что такое dependency injection?

  • это принцип который уменьшает связывание (coupling) и увеличивает сцепление (cohesion)

Зачем мне применять dependency injection?

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

Как мне начать применять dependency injection?

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

Зачем мне для этого фреймворк?

  • тебе нужен фреймворк для того чтобы не создавать свой. Код создания объектов будет дублироваться и его тяжело будет менять. Для того чтобы этого не было тебе нужен контейнер.
  • фреймворк дает тебе контейнер и провайдеры
  • провайдеры управляют временем жизни объектов. Тебе понадобятся фабрики, синглтоны и объекты конфигурации
  • контейнер служит коллекцией провайдеров

Какаю цену я плачу?

  • тебе нужно явно указывать зависимости в контейнере
  • это дополнительная работа
  • это начнет приносить дивиденды когда проект начнет расти
  • или через 2 недели после его завершения (когда ты забудешь какие решения принимал и какова структура проекта)

Концепция Dependency Injector


В дополнение опишу концепцию Dependency Injector как фреймворка.

Dependency Injector основан на двух принципах:

  • Явное лучше неявного (PEP20).
  • Не делать никакой магии с вашим кодом.

Чем Dependency Injector отличается от другим фреймворков?

  • Нет автоматического связывания. Фреймворк не делает автоматического связывания зависимостей. Не используется интроспекция, связывание по именам аргументов и / или типам. Потому что явное лучше неявного (PEP20).
  • Не загрязняет код вашего приложения. Ваше приложение не знает о наличии Dependency Injector и не зависит от него. Никаких @inject декораторов, аннотаций, патчинга или других волшебных трюков.

Dependency Injector предлагает простой контракт:

  • Вы показываете фреймворку как собирать объекты
  • Фреймворк их собирает

Сила Dependency Injector в его простоте и прямолинейности. Это простой инструмент для реализации мощного принципа.

Что дальше?


Если вы заинтересовались, но сомневайтесь, моя рекомендация такая:

Попробуйте применить этот подход на протяжении 2-х месяцев. Он неинтуитивный. Нужно время чтобы привыкнуть и прочувствовать. Польза стает ощутимой, когда проект вырастает до 30+ компонентов в контейнере. Если не понравится много не потеряйте. Если понравится получите существенное преимущество.


Буду рад фидбеку и отвечу на вопросы в комментариях.
Подробнее..

CQRS что делать с кодом, который нужно использовать сразу в нескольких обработчиках?

18.03.2021 16:05:16 | Автор: admin


При использовании архитектуры в стиле вертикальных слайсов, рано или поздно встает вопрос а что делать, если появляется код, который нужно использовать сразу в нескольких хендлерах?


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

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


Рефакторинг


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


  1. Извлечь метод
  2. Извлечь класс

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


public IEnumerable<SomeDto> Handle(SomeQuery q){    // 100 строчка кода,    // которые потребуются в нескольких обработчиках    // 50 строчек кода, которые специфичны именно    // для этого обработчика    return result;}

В реальности, бывает и так, что первые 100 и вторые 50 строчек перемешаны. В этом случае, сначала придется их размотать. Чтобы код не запутывался, заведите привычку жамкать на ctrl+shift+r -> extract method прямо по ходу разработки. Длинные методы это фу.

Итак, извлечем два метода, чтобы получилось что-то вроде:


public IEnumerable<SomeDto> Handle(SomeQuery q){    var shared = GetShared(q);    var result = GetResult(shared);    return result;}

Композиция или наследование?


Что же выбрать дальше: композицию или наследование? Композицию. Дело в том, что по мере разрастания логики код может приобрести следующую форму:


public IEnumerable<SomeDto> Handle(SomeQuery q){    var shared1 = GetShared1(q);    var shared2 = GetShared2(q);    var shared3 = GetShared3(q);    var shared4 = GetShared4(q);    var result = GetResult(shared1,shared2, shared3, shared4);    return result;}

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

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


public class ConcreteQueryHandler:     IQueryHandler<SomeQuery, IEnumerable<SomeDto>>{    ??? _sharedHandler;    public ConcreteQueryHandler(??? sharedHandler)    {        _sharedHandler = sharedHandler;    }}

Тип промежуточных хендлеров



В слоеной/луковой/чистой/порты-адаптершной архитектурах такая логика обычно находится в слое сервисов предметной области (Domain Services).


У нас вместо слоев будут соответствующие вертикальные разрезы и специализированный интерфейс IDomainHandler<TIn, TOut>, наследуемый от IHandler<TIn, TOut>.


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


public class ConcreteQueryHandler2:    IQueryHandler<SomeQuery,  IEnumerable<SomeDto>>{    IDomainHandler<???, ???> _sharedHandler;    public ConcreteQueryHandlerI(IDomainHandler<???, ???> sharedHandler)    {        _sharedHandler = sharedHandler;    }}public class ConcreteQueryHandler2:    IQueryHandler<SomeQuery,  IEnumerable<SomeDto>>{    IDomainHandler<???, ???> _sharedHandler;    public ConcreteQueryHandlerI(IDomainHandler<???, ???> sharedHandler)    {        _sharedHandler = sharedHandler;    }}

Зачем нужны специализированные маркерные интерфейсы?


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



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


Тип промежуточных хендлеров


Осталось чуть-чуть: решить, какой тип будет у IDomainHandler<???, ???>. Этот вопрос можно разделить на два:


  1. Стоит ли мне передавать ICommand/IQuery в качестве входного параметра?
  2. Стоит ли мне использоватьIQueryable<T> в качестве возвращаемого значения?

Стоит ли мне передавать ICommand/IQuery в качестве входного параметра?


Не стоит, если ваши интерфейсы определены как:


public interface ICommand<TResult>{}public interface IQuery<TResult>{}

В зависимости от типа возвращаемого значения IDomainHandler вам может потребоваться добавлять дополнительные интерфейсы на Command/Query, что не улучшает читабельность и увеличивает связность кода.


Стоит ли мне использоватьIQueryable<T> в качестве возвращаемого значения?


Не стоит, если у вас нет ORM:) А вот, если он есть Не смотря на явные проблемы LINQ с LSP я думаю, что ответ на этот вопрос зависит. Бывают случаи, когда условия получения данных настолько запутаны и сложны, что одними спецификациями выразить их не получается. В этом случае передача IQueryable во внутренних слоях приложения меньшее из зол.


Итого


  1. Выделяем метод
  2. Выделяем класс
  3. Используем специализированные интерфейсы
  4. Внедряем зависимость слоя предметной области в качестве аргументов конструктора

public class ConcreteQueryHandler:    IQueryHandler<SomeQuery,  IEnumerable<SomeDto>>{    IDomainHandler<        SomeValueObjectAsParam,        IQueryable<SomeDto>>_sharedHandler;    public ConcreteQueryHandler(        IDomainHandler<            SomeValueObjectAsParam,            IQueryable<SomeDto>>)    {        _sharedHandler = sharedHandler;    }    public IEnumerable<SomeDto> Handle(SomeQuery q)    {        var prm = new SomeValueObjectAsParam(q.Param1, q.Param2);        var shared = _sharedHandler.Handle(prm);        var result = shared          .Where(x => x.IsRightForThisUseCase)          .ProjectToType<SomeDto>()          .ToList();        return result;    }}
Подробнее..

Раздел Refactor в IDEA

28.11.2020 16:23:21 | Автор: admin

Эту статью можно рассматривать как краткий обзор c gif-ками по рефакторингам Java-файлов в IDEA для начинающих.

Осторожно, много тяжелых gif-картинок.

"Any fool can write code that a computer can understand. Good programmers write code that humans can understand. M. Fowler (1999)

Содержание

Введение

Раздел Refaсtor

- Refactor This

- Rename

- Rename File

- Change Signature

- Edit Property Value (без примера)

- Type Migration

- Make Static

- Convert To Instance Method

- Move Classes

- Copy Classes

- Safe Delete

- Extract/Introduce

- - Variable

- - Constant

- - Field

- - Parameter

- - Functional Parameter

- - Functional Variable

- - Parameter Object

- - Method

- - Type Parameter (без примера)

- - Interface

- - Superclass

- - Subquery as CTE (без примера)

- - RSpec 'let' (без примера)

- Inline

- Find and Replace Code Duplicate

- Pull Member Up

- Pull Member Down

- Push ITds In

- Use Interface Where Possible

- Replace Inheritance with Delegation

- Remove Middleman

- Wrap Method Return Value

- Encapsulate Field

- Replace Temp with Query

- Replace Constructor with Factory Method

- Replace Constructor with Builder

- Generify

- Migrate

- Lombok и Delombok (без примера)

- Internationalize Список источников

Введение

Цель данной статьи - показать доступные способы рефакторинга для Java-файлов (многие способы будут работать и для других языков). Как использовать эти приемы в реальной жизни показано в замечательном видео Тагира Валеева (ссылка в списке источников).

Думаю, каждый, кто работает в IDEA, знает, что в ней куча способов для рефакторинга кода. И почти уверен, что каждый второй смотрит анонсы новой версии, где красиво показаны новые способы рефакторинга и заглядывал в раздел Refaсtor:

 Рис. 1. Раздел Refactoring IDEA Рис. 1. Раздел Refactoring IDEA

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

В статье представлены фрагменты кода, порядок действий и анимации почти для каждого пункта. Также постарался добавить, где возможно, ссылку на замечательную книгу Refactoring: Improving the Design of Existing Code (Martin Fowler). Чтобы не сильно раздувать трафик пришлось довольно сильно обрезать много gif-картинок, поэтому обязательно смотрите использованный код под катом. Горячие клавиши приведены для Windows/LInux по умолчанию.

Раздел Refaсtor

Пойдем сверху вниз по порядку.

Пункт Refactor This (Ctrl+Alt+Shift+T)

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

Рис. 2. Refactor This для имени функцииРис. 2. Refactor This для имени функцииРис. 3. Refactor This для аргументов функцииРис. 3. Refactor This для аргументов функции

Пункт Rename (Shift+F6)

Позволяет переименовать практически любой идентификатор в коде, будь то переменная или названия класса. Изменения распространяются по всему проекту, в некоторых случаях, включая и комментарии. (У Фаулера переименованию посвящено 2 главы - Rename Field и Rename Variable)

Рис. 4. Переименование методаРис. 4. Переименование методаИспользованный код

До

public class Main {   public static void main(String[] args) {       System.out.print("Hello");       invo<caret/>ke(", World");   }   private static void invoke(String text) {       //text       System.out.println(text);   }}

После

public class Main {   public static void main(String[] args) {       System.out.print("Hello");       newFunctionName(", World");   }   private static void newFunctionName(String text) {       //text       System.out.println(text);   }}
  • Переименование переменной

 Рис. 5. Переименование переменной Рис. 5. Переименование переменнойИспользованный код

До

public class Main {   public static void main(String[] args) {       System.out.print("Hello");       invoke(", World");   }   private static void invoke(String te<caret>xt) {       //text       System.out.println(text);   }}

После

public class Main {   public static void main(String[] args) {       System.out.print("Hello");       invoke(", World");   }   private static void invoke(String newText) {       //newText       System.out.println(newText);   }}
  • Переименование вложенного класса

 Рис. 6. Переименование вложенного класса Рис. 6. Переименование вложенного классаИспользованный код

До

public class Main {   public static void main(String[] args) {       System.out.print("Hello");       invoke(", World");   }   private static void invoke(String text) {       //text       System.out.println(text);       throw new MyExc<caret>eption();   }   public static class MyException extends RuntimeException {   }}

После

public class Main {   public static void main(String[] args) {       System.out.print("Hello");       invoke(", World");   }   private static void invoke(String text) {       //text       System.out.println(text);       throw new NewMyException ();   }   public static class NewMyException extends RuntimeException {   }}
  • Переименование класса

 Рис. 7. Переименование класса Рис. 7. Переименование классаИспользованный код

До

public class Main {   public static void main(String[] args) {       MyS<caret>ervice service = new MyService();       service.service();   }}

После

public class Main {   public static void main(String[] args) {       NewMyService myService = new NewMyService ();       myService.service();   }}
  • Переименование пакета

 Рис. 8. Переименование пакета Рис. 8. Переименование пакетаИспользованный код
package gen<caret>eral;public class Main {   public static void main(String[] args) {       NewMyService service = new NewMyService();       service.service();   }}

После

package org.test.java.src;public class Main {   public static void main(String[] args) {       NewMyService service = new NewMyService();       service.service();   }}

Пункт Rename File

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

 Рис. 9. Пример использования Rename File Рис. 9. Пример использования Rename FileИспользованный код

До

public class Main {   public static void main(String[] args) throws IOException {       Path path = Paths.get("src/general/TestFile.txt");       String read = Files.readAllLines(path).get(0);       System.out.println(read);   }}

После

public class Main {   public static void main(String[] args) throws IOException {       Path path = Paths.get("src/general/TestFile2.txt");       String read = Files.readAllLines(path).get(0);       System.out.println(read);   }}

Пункт Change Signature (Ctrl+F6)

У Фаулера этому посвящена глава Change Function Declaration. В новой версии IDEA Change Signature был немного доработан. Я знаю два пути:

  • первый путь - прямой - изменить сигнатуру метода и вызвать обновление,

  • второй - через диалоговое окно.

Пример для первого способа (через изменения сигнатуры метода)

Изменяем сигнатуру метода. В этот момент слева появляется "R" и в контекстном меню появляется пункт "Update usages to reflect signature change", который позволяет обновить все использования метода.

Рис. 10. Пример использования Update usages to reflect signature changeРис. 10. Пример использования Update usages to reflect signature changeИспользованный код

До

public class ChangeSignature {   public static void main(String[] args) {       invokeMethod("Hello");       invokeMethod("World");   }   private static void invokeMethod(String text) {       System.out.println(text);   }}

После

public class ChangeSignature {   public static void main(String[] args) {       invokeMethod("Hello", null);       invokeMethod("World", null);   }   private static void invokeMethod(String text, String newType) {       System.out.println(text);   }}

Пример для второго способа (через диалоговое окно)

В диалоговом окне можно изменить состав переменных, exception, и даже сгенерировать переопределенный метод.

Рис. 11. Пример использования Change SignatureРис. 11. Пример использования Change SignatureИспользованный код

До

public class ChangeSignature {   public static void main(String[] args) {       invokeMethod("Hello");       invokeMethod("World");   }   private static void invokeMethod(String<caret> text) {       System.out.println(text);   }}

После

public class ChangeSignature {   public static void main(String[] args) {       invokeMethod("Hello");       invokeMethod("World");   }   private static void invokeMethod(String text) {       invokeMethod(text, null);   }   private static void invokeMethod(String text, String newName) {       System.out.println(text);   }}

Пункт Edit Property Value (Alt + F6)

На данный момент (Idea 2020.2) экспериментальная функция и по умолчанию не включена. Включить можно параметром property.value.inplace.editing=true Поэтому примеры не привожу.

Пункт Type Migration (Ctrl + Shift + F6)

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

Рис. 12. Пример использования Type MigrationРис. 12. Пример использования Type MigrationИспользованный код

До

public class ChangeSignature {   public static void main(String[] args) {       Inte<caret>ger hello = 1;       print(hello);   }   private static void print(Integer text) {       System.out.println(text);   }}

После

public class ChangeSignature {   public static void main(String[] args) {       Number hello = 1;       print(hello);   }   private static void print(Number text) {       System.out.println(text);   }}

Пункт Make Static (Ctrl + Shift + F6)

Позволяет сконвертировать метод или внутренний класс в статический. (Противоположность Convert To Instance Method)

Рис. 13. Пример использования Make StaticРис. 13. Пример использования Make StaticИспользованный код

До

public class MakeStatic {   public static void main(String[] args) {       MakeStatic makeStatic = new MakeStatic();       makeStatic.sayHello();   }   public void say<caret>Hello() {       System.out.println("Hello, World");   }}

После

public class MakeStatic {   public static void main(String[] args) {       MakeStatic makeStatic = new MakeStatic();       MakeStatic.sayHello();   }   public static void sayHello() {       System.out.println("Hello, World");   }}

Пункт Convert To Instance Method

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

Рис. 14. Пример использования Convert To Instance MethodРис. 14. Пример использования Convert To Instance MethodИспользованный код

До

public class MakeStatic {   public static void main(String[] args) {       sayHello();   }   public static void sa<caret>yHello() {       System.out.println("Hello, World");   }}

После

public class MakeStatic {   public static void main(String[] args) {       new MakeStatic().sayHello();   }   public void sayHello() {       System.out.println("Hello, World");   }}

Пункт Move Classes (F6)

В принципе делает, что и написано, перемещает классы.

Рис. 15. Пример использования Move ClassesРис. 15. Пример использования Move ClassesИспользованный код

До

package org.example.test.service;public class TestService {<caret>}

После

package org.example.test;public class TestService {}

Пункт Copy Classes (F5)

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

 Рис. 16. Пример использования Copy Classes Рис. 16. Пример использования Copy Classes

Пункт Safe Delete (Alt+Delete)

По функциональности почти повторяет то, что можно получить через контекстное меню (Alt + Enter), но позволяет удалять чуть больше. Поэтому, я заметил, у многих знакомых любимый способ рефакторинга - F2(следующая ошибка) и Alt + Enter или Alt + Delete. Можно удалять классы, переменные, методы. Перед удалением IDEA выполнит поиск использования удаляемых элементов, и если IDEA найдет, что они где-то используется покажет диалоговое окно Usages Detected. Про удаление неиспользуемого кода у Фаулера есть целая глава - Remove Dead Code

 Рис. 17. Пример использования Safe Delete Рис. 17. Пример использования Safe DeleteИспользованный код

До

package org.example.test;public class MainClass {   public static void main(String[] args) {       start();   }   private static void start() {       String unUsedVariable;       System.out.println("Hello, World!");   }   private static void unUsedMethod() {   }}

После

<empty>

Пункт Extract/Introduce

Следующий блок - Extract/Introduce. Думаю, является одним из самых популярных. Позволяет извлекать разные части программы.

 Рис. 18. Список доступных способов рефакторинга Extract/Introduce Рис. 18. Список доступных способов рефакторинга Extract/Introduce

Пункт Variable (Ctrl+Alt+V)

Создает новую переменную из выделенного фрагмента. (Этому способу у Фаулера посвящена глава Extract Variable).

 Рис. 19. Пример использования Extract/Introduce->Variable Рис. 19. Пример использования Extract/Introduce->VariableИспользованный код

До

public class ExtractVariable {   public static void main(String[] args) {       sayHello();   }   private static void sayHello() {       System.out.println("He<caret>llo, World!");   }}

После

public class ExtractVariable {   public static void main(String[] args) {       sayHello();   }   private static void sayHello() {       String text = "Hello, World!";       System.out.println(text);   }}

Пункт Constant (Ctrl+Alt+C)

Создает новую константу из выделенного фрагмента.

 Рис. 20. Пример использования Extract/Introduce->Constant Рис. 20. Пример использования Extract/Introduce->ConstantИспользованный код

До

public class ExtractVariable {   public static void main(String[] args) {       sayHello();   }   private static void sayHello() {       System.out.println("He<caret>llo, World!");   }}

После

public class ExtractVariable {   public static final String HELLO_WORLD = "Hello, World!";   public static void main(String[] args) {       sayHello();   }   private static void sayHello() {       System.out.println(HELLO_WORLD);   }}

Пункт Field (Ctrl+Alt+F)

Создает новое поле класса из выделенного фрагмента.

 Рис. 21. Пример использования Extract/Introduce->Field Рис. 21. Пример использования Extract/Introduce->FieldИспользованный код

До

public class ExtractVariable {   public static void main(String[] args) {       sayHello();   }   private static void sayHello() {       System.out.println("He<caret>llo, World!");   }}

После

public class ExtractVariable {   private static String x;   public static void main(String[] args) {       sayHello();   }   private static void sayHello() {       x = "Hello, World!";       System.out.println(x);   }}

Пункт Parameter (Ctrl+Alt+P)

Создает новый параметр (функции) из выделенного фрагмента.

Рис. 22. Пример использования Extract/Introduce->ParameterРис. 22. Пример использования Extract/Introduce->ParameterИспользованный код

До

public class ExtractVariable {   public static void main(String[] args) {       sayHello();   }   private static void sayHello() {       System.out.println("He<caret>llo, World!");   }}

После

public class ExtractVariable {   public static void main(String[] args) {       sayHello("Hello, World!");   }   private static void sayHello(String x) {       System.out.println(x);   }}

Пункт Functional Parameter

Очень похож на пункт Parameter, но теперь в функцию мы передаем или java.util.function.Supplier, или javafx.util.Builder. Обратите внимание, данный рефакторинг может привести к нежелательным эффектам.

Рис. 23. Пример использования Extract/Introduce->Functional ParameterРис. 23. Пример использования Extract/Introduce->Functional ParameterИспользованный код

До

public class ExtractParameter {   public static void main(String[] args) {       System.out.println(generateText());   }   private static String generateText() {       return "Hello, Wor<caret>ld!".toUpperCase();   }}

После

public class ExtractParameter {   public static void main(String[] args) {       System.out.println(generateText(() -> "Hello, World!"));   }   private static String generateText(final Supplier<string> getText) {       return getText.get().toUpperCase();   }}

Пункт Functional Variable

Очень похож на пункт Variable, но теперь мы получаем или java.util.function.Supplier или javafx.util.Builder.

 Рис. 24. Пример использования Extract/Introduce->Functional Variable Рис. 24. Пример использования Extract/Introduce->Functional Variable Использованный код

До

public class ExtractParameter {   public static void main(String[] args) {       System.out.println(generateText());   }   private static String generateText() {       return "Hello, W<caret>orld!".toUpperCase();   }}

После

public class ExtractParameter {   public static void main(String[] args) {       System.out.println(generateText());   }   private static String generateText() {       Supplier<string> getText = () -> "Hello, World!";       return getText.get().toUpperCase();   }}
ParameterObject

Пункт Parameter Object

Удобный способ, когда в функцию передается много аргументов и вам надо обернуть их в класс. (У Фаулера это глава Introduce Parameter Object).

Рис. 25. Пример использования Extract/Introduce->Parameter Object Рис. 25. Пример использования Extract/Introduce->Parameter Object Использованный код

До

public class ExtractParameter {   public static void main(String[] args) {       System.out.println(generateText("Hello", "World!"));   }   private static String generateText(Str<caret>ing hello, String world) {       return hello.toUpperCase() + world.toUpperCase();   }}

После

public class ExtractParameter {   public static void main(String[] args) {       System.out.println(generateText(new HelloWorld("Hello", "World!")));   }   private static String generateText(HelloWorld helloWorld) {       return helloWorld.getHello().toUpperCase() + helloWorld.getWorld().toUpperCase();   }   private static class HelloWorld {       private final String hello;       private final String world;       private HelloWorld(String hello, String world) {           this.hello = hello;           this.world = world;       }       public String getHello() {           return hello;       }       public String getWorld() {           return world;       }   }}

Пункт Method (Ctrl+Alt+M)

Извлекаем метод из выделенного фрагмента. (У Фаулера есть глава про похожий способ рефакторинга - Extract Function).

 Рис. 26. Пример использования Extract/Introduce->Method Рис. 26. Пример использования Extract/Introduce->Method Использованный код

До

public class ExtractMethod {   public static void main(String[] args) {       String text = "Hello, World!";       System.out.prin<caret>tln(text);   }}

После

public class ExtractMethod {   public static void main(String[] args) {       String text = "Hello, World!";       print(text);   }   private static void print(String text) {       System.out.println(text);   }}

Пункт Type Parameter

Рефакторинг из мира Kotlin, и для Java не применим (буду рад добавить, если кто-то сделает пример).

Пункт Replace Method With Method Object

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

Рис. 27. Пример использования Extract/Introduce->Replace Method With Method Object Рис. 27. Пример использования Extract/Introduce->Replace Method With Method Object Использованный код

До

public class ExtractMethod {   public static void main(String[] args) {       String text = "Hello, World!";       print(text);   }   private static void print(String text) {       System.out.p<caret>rintln(text);   }}

После

public class ExtractMethod {   public static void main(String[] args) {       String text = "Hello, World!";       print(text);   }   private static void print(String text) {       new Printer(text).invoke();   }   private static class Printer {       private String text;       public Printer(String text) {           this.text = text;       }       public void invoke() {           System.out.println(text);       }   }}

Пункт Delegate

Позволяет извлечь методы и поля в отдельный класс.

 Рис. 28. Пример использования Extract/Introduce->Delegate Рис. 28. Пример использования Extract/Introduce->Delegate Использованный код

До

public class Delegate {   public static void main(String[] args) {       new Delegate().print();   }   private void print() {       System.ou<caret>t.println("Hello, World!");   }}

После

public class Delegate {   private final Printer printer = new Printer();   public static void main(String[] args) {       new Delegate().print();   }   private void print() {       printer.print();   }   public static class Printer {       public Printer() {       }       private void print() {           System.out.println("Hello, World!");       }   }}

Пункт Interface

Для заданного класса и его методов создает интерфейс. (Особенно удобно, когда при работе со Spring, когда кто-то забыл для компонента создать соответствующий интерфейс)

 Рис. 29. Пример использования Extract/Introduce->Interface Рис. 29. Пример использования Extract/Introduce->Interface Использованный код

До

public class ExtractImpl {   public static void main(String[] args) {       new ExtractImpl().print();   }   public void print() {       System.out.println("Hello, World!");   }}

После

public class ExtractImpl implements ExtractInterface {   public static void main(String[] args) {       new ExtractImpl().print();   }   @Override   public void print() {       System.out.println("Hello, World!");   }}public interface ExtractInterface {   void print();}

Пункт Superclass

Аналогично пункту Interface, только теперь создается класс-родитель (Superclass). Фаулер описывает этот способ рефакторинга в главе Extract Superclass.

Рис. 30. Пример использования Extract/Introduce->Superclass Рис. 30. Пример использования Extract/Introduce->Superclass Использованный код

До

public class ExtractImpl {   public static void main(String[] args) {       new ExtractImpl().print();   }   public void print() {       System.out.println("Hello, World!");   }}

После

public class ExtractImpl extends ExtractAbstr {   public static void main(String[] args) {       new ExtractImpl().print();   }}public class ExtractAbstr {   public void print() {       System.out.println("Hello, World!");   }}

Пункт Subquery as CTE

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

Пункт RSpec 'let'

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

Пункт Inline

Возможно один из самых крутых методов рефакторинга, Инлайнить можно почти все. Фаулер описывает этот способ рефакторинга в главах Inline Class, Inline Function, Inline Variable.

 Рис. 31. Пример использования пункта Inline Рис. 31. Пример использования пункта InlineИспользованный код

До

public class Inline {   public static void main(String[] args) {       print();   }   private static void print() {       new Printer().print();   }   private static class Printer {       public void print() {           String text = "Hello, World!";           System.out.println(t<caret>ext);       }   }}

После

public class Inline {   public static void main(String[] args) {           System.out.println("Hello, World!");   }}

Пункт Find and Replace code duplicate

Ищет похожие фрагменты кода и предлагает заменить их, например, вызовом метода или константой.

 Рис. 32. Пример использования пункта Find and Replace code duplicate Рис. 32. Пример использования пункта Find and Replace code duplicateИспользованный код

До

public class Replace {   public static void main(String[] args) {       System.out.println("Hello, World!");   }   public void print() {       System.out.println("Hello, World!");   }   public void print2() {       System.out.prin<caret>tln("Hello, World!");   }}

После

public class Replace {   public static void main(String[] args) {       print2();   }   public void print() {       print2();   }   public static void print2() {       System.out.println("Hello, World!");   }}

Пункт Invert Boolean

Позволяет инвертировать булевые переменные.

Рис. 33. Пример использования пункта Invert BooleanРис. 33. Пример использования пункта Invert BooleanИспользованный код

До

public class Invert {   public static void main(String[] args) {       boolean co<caret>ndition = true;       if (condition) {           System.out.println("Hello, World!");       }   }}

После

public class Invert {   public static void main(String[] args) {       boolean condition = false;       if (!condition) {           System.out.println("Hello, World!");       }   }}

Пункт Pull Member Up

Позволяет перемещать методы или поля по иерархии вверх. Зачем это нужно написано у Фаулера в главах Pull Up Field и Pull Up Method. Выполняет обратную задачу пункта Pull Member Down.

Рис. 34. Пример использования пункта Pull Member UpРис. 34. Пример использования пункта Pull Member UpИспользованный код

До

public class PullMethod {   public static void main(String[] args) {       new InnerClass().print();   }   private static class InnerClass extends AbstClass {       public void print() {           System.out.pri<caret>ntln("Hello, World");       }   }   private static abstract class AbstClass {   }}

После

public class PullMethod {    public static void main(String[] args) {        new InnerClass().print();    }    private static class InnerClass extends AbstClass {    }    private static abstract class AbstClass {        public void print() {            System.out.println("Hello, World");        }    }}

Пункт Pull Member Down

Выполняет обратную задачу пункта Pull Member Up. Позволяет перемещать методы или поля по иерархии вниз. (У Фаулера - глава Push Down Method)

Рис. 35. Пример использования пункта Pull Member DownРис. 35. Пример использования пункта Pull Member DownИспользованный код

До

public class PullMethod {   public static void main(String[] args) {       new InnerClass().print();   }   private static class InnerClass extends AbstClass {   }   private static abstract class AbstClass {       public void print() {           System.out.prin<caret>tln("Hello, World");       }   }}

После

public class PullMethod {   public static void main(String[] args) {       new InnerClass().print();   }   private static class InnerClass extends AbstClass {       @Override       public void print() {           System.out.println("Hello, World");       }   }   private static abstract class AbstClass {       public abstract void print();   }}

Пункт Push ITds In

Используется при работе с AsperctJ.

 Рис. 36. Пример использования пункта Push ITds In Рис. 36. Пример использования пункта Push ITds InИспользованный код

До

aspect myAspect {   boolean Account.closed = <caret>false;   void Account.close() {       closed = true;   }}class Account {}

После

aspect myAspect {   boolean Account.closed = false;}class Account {   void close() {       closed = true;   }}

Пункт Use Interface Where Possible

IDEA старается заменить, где это возможно, указания классов на указание интерфейсов.

Рис. 37. Пример использования пункта Use Interface Where PossibleРис. 37. Пример использования пункта Use Interface Where PossibleИспользованный код

До

public class ExtractInterface {   public static void main(String[] args) {       InnerClass innerClass = new InnerClass();       print(innerClass);   }   private static void print(InnerClass innerClass) {       innerClass.print();   }   private static class InnerClass implements InnerInterface{       @Override       public void print() {           System.out.println("Hello, World!");       }   }   private static interface InnerInterface{       void print();   }}

После

public class ExtractInterface {   public static void main(String[] args) {       InnerInterface innerClass = new InnerClass();       print(innerClass);   }   private static void print(InnerInterface innerClass) {       innerClass.print();   }   private static class InnerClass implements InnerInterface{       @Override       public void print() {           System.out.println("Hello, World!");       }   }   private static interface InnerInterface{       void print();   }}

Пункт Replace Inheritance with Delegation

Заменяет наследование делегированием. У Фаулера про это главы Replace Subclass with Delegate и Replace Superclass with Delegate.

Рис. 38. Пример использования пункта Replace Inheritance with DelegationРис. 38. Пример использования пункта Replace Inheritance with DelegationИспользованный код

До

public class InheritanceDelegation {   public static void main(String[] args) {       InnerClass innerClass = new InnerClass();       print(innerClass);   }   private static void print(InnerClass innerClass) {       innerClass.print();   }   private static class In<caret>nerClass extends AbstractClass {   }   private static class AbstractClass {       public void print() {           System.out.println("Hello, World!");       }   }}

После

public class InheritanceDelegation {    public static void main(String[] args) {        InnerClass innerClass = new InnerClass();        print(innerClass);    }    private static void print(InnerClass innerClass) {        innerClass.print();    }    private static class InnerClass {        private final AbstractClass abstractClass = new AbstractClass();        public void print() {            abstractClass.print();        }    }    private static class AbstractClass {        public void print() {            System.out.println("Hello, World!");        }    }}

Пункт Remove Middleman

Заменяет все делегированные вызовы на прямые. (У Фаулера - глава Remove Middle Man).

 Рис. 39. Пример использования пункта Remove Middleman Рис. 39. Пример использования пункта Remove MiddlemanИспользованный код

До

public class Middleman {   public static void main(String[] args) {       InnerClass innerClass = new InnerClass();       innerClass.print();   }   private static class InnerClass {       private final NextClass next<caret>Class = new NextClass();       public void print() {           nextClass.print();       }   }   private static class NextClass {       public void print() {           System.out.println("Hello, World!");       }   }}

После

public class Middleman {   public static void main(String[] args) {       InnerClass innerClass = new InnerClass();       innerClass.getNextClass().print();   }   private static class InnerClass {       private final NextClass nextClass = new NextClass();       public NextClass getNextClass() {           return nextClass;       }   }   private static class NextClass {       public void print() {           System.out.println("Hello, World!");       }   }}

Пункт Wrap Method Return Value

Оборачивает возвращаемое значение в объект-обертку. Удобно, когда нужно возвращать несколько связанных значений.

 Рис. 40. Пример использования пункта Wrap Method Return Value Рис. 40. Пример использования пункта Wrap Method Return ValueИспользованный код

До

public class WrapMethodReturnValue {   public static void main(String[] args) {       System.out.println(new MessageFolder().get());   }   private static class MessageFolder {       public String get() {           ret<caret>urn "Hello, World!";       }   }}

После

public class WrapMethodReturnValue {   public static void main(String[] args) {       System.out.println(new MessageFolder().get().getValue());   }   private static class MessageFolder {       public Message get() {           return new Message("Hello, World!");       }       public class Message {           private final String value;           public Message(String value) {               this.value = value;           }           public String getValue() {               return value;           }       }   }}

Пункт Encapsulate Field

Скрывает поле за getter, setter.

 Рис. 41. Пример использования пункта Encapsulate Field Рис. 41. Пример использования пункта Encapsulate FieldИспользованный код

До

public class EncapsulateField {   public static void main(String[] args) {       System.out.println(new InnerClass().message);   }   private static class InnerClass {       public String m<caret>essage = "Hello, World!";   }}

После

public class EncapsulateField {   public static void main(String[] args) {       System.out.println(new InnerClass().getMessage());   }   private static class InnerClass {       private String message = "Hello, World!";       public String getMessage() {           return message;       }       public void setMessage(String message) {           this.message = message;       }   }}

Пункт Replace Temp with Query

Пусть у вас есть

int size = getActualSize()

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

 Рис. 42. Пример использования пункта Replace Temp with Query Рис. 42. Пример использования пункта Replace Temp with QueryИспользованный код

До

public class ReplaceTemp {   public static void main(String[] args) {       String hello = "Hello";       String mes<caret>sage = hello + ", World!";       System.out.println(message);   }}

После

public class ReplaceTemp {   public static void main(String[] args) {       String hello = "Hello";       System.out.println(message(hello));   }   private static String message(String hello) {       return hello + ", World!";   }}

Пункт Replace Constructor with Factory Method

Генерирует фабричный метод для указанного конструктора. Идеально, если у вас нет Lombok. (У Фаулера этому посвящена глава Replace Constructor with Factory Function).

Рис. 43. Пример использования пункта Replace constructor with factory methodРис. 43. Пример использования пункта Replace constructor with factory methodИспользованный код

До

public class ReplaceConstructor {   public static void main(String[] args) {       new InnerClass("Hello", "World").print();   }   private static class InnerClass {       private String message;       public Inner<caret>Class(String hello, String world) {           message = hello + ", " + world;       }       public void print() {           System.out.println(message);       }   }}

После

public class ReplaceConstructor {   public static void main(String[] args) {       InnerClass.createInnerClass("Hello", "World").print();   }   private static class InnerClass {       private String message;       private InnerClass(String hello, String world) {           message = hello + ", " + world;       }       public static InnerClass createInnerClass(String hello, String world) {           return new InnerClass(hello, world);       }       public void print() {           System.out.println(message);       }   }}

Пункт Replace Constructor with Builder

Генерирует builder для указанного конструктора. Идеально, если у вас нет Lombok.

Рис. 44. Пример использования пункта Replace Constructor with BuilderРис. 44. Пример использования пункта Replace Constructor with BuilderИспользованный код

До

public class ReplaceConstructor {   public static void main(String[] args) {       new InnerClass("Hello", "World").print();   }   private static class InnerClass {       private String message;       public InnerC<caret>lass(String hello, String world) {           message = hello + ", " + world;       }       public void print() {           System.out.println(message);       }   }}

После

public class ReplaceConstructor {   public static void main(String[] args) {       new InnerClassBuilder().setHello("Hello").setWorld("World").createInnerClass().print();   }   static class InnerClass {       private String message;       public InnerClass(String hello, String world) {           message = hello + ", " + world;       }       public void print() {           System.out.println(message);       }   }}public class InnerClassBuilder {   private String hello;   private String world;   public InnerClassBuilder setHello(String hello) {       this.hello = hello;       return this;   }   public InnerClassBuilder setWorld(String world) {       this.world = world;       return this;   }   public ReplaceConstructor.InnerClass createInnerClass() {       return new ReplaceConstructor.InnerClass(hello, world);   }}

Пункт Generify

Пытается код с raw-типами превратить в код с Generic-типами. Актуален при миграции с java версий ранее 1.5 на современные версии.

Рис. 45. Пример использования пункта GenerifyРис. 45. Пример использования пункта GenerifyИспользованный код

До

public class Generify {   public static void main(String[] args) {       List list = getList();       Object message = list.get(0);       System.out.println(message);   }   private static List getList() {       ArrayList arrayList = new ArrayList();       arrayList.add("Hello, World!");       return arrayList;   }}

После

public class Generify {   public static void main(String[] args) {       List<string> list = getList();       String message = list.get(0);       System.out.println(message);   }   private static List<string> getList() {       ArrayList<string> arrayList = new ArrayList&lt;>();       arrayList.add("Hello, World!");       return arrayList;   }}

Пункт Migrate

Предоставляет готовые миграции для следующего списка:

Рис. 46. Список доступных миграций пункта MigrateРис. 46. Список доступных миграций пункта Migrate

А также предоставляет возможность делать свои. Вот, например, правила миграции для JUnit(4.x -> 5.0):

Рис. 47. Правила миграции для JUnit(4.x -> 5.0)Рис. 47. Правила миграции для JUnit(4.x -> 5.0)

Вот здесь есть подробное видео про миграцию для JUnit(4.x -> 5.0).

Пункт Lombok и Delombok

Предоставляются плагином Lombok. Недавно было объявлено, что теперь он будет входить в стандартную поставку IDEA. Используется при работе с библиотекой кодогенерации Lombok.

Пункт Internationalize

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

Список источников

Подробнее..
Категории: Java , Idea , Refactoring

Категории

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

  • Имя: Макс
    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