Принципы SOLID в инженерии данных. Часть 2

Предисловие

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

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

Построим на этих принципах проектирования с Python реальный конвейер данных.

Архитектура конвейера

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

Исходный код  —  в репозитории GitHub.

Разбор конвейера

Традиционный подход инженерии данных

Согласно традиционному подходу инженерии данных, в архитектуре конвейера различают такие части:

Средство извлечения > преобразователь > загрузчик

…об этом чаще говорят как об…

извлечении > преобразовании > загрузке

Вот роль каждого объекта в ETL-конвейере при обработке данных:

  • Объектом Extractor выполняется извлечение (E).
  • Transformer  —  преобразование (T).
  • Loader  —  загрузка (L).

Что не так с этим подходом?

Хотя в этой версии футбольные данные для обработки и извлекаются, принципы SOLID во многом нарушаются:

  • Программа открыта для модификации, но закрыта для расширения, то есть трудно обновить или доработать код дополнительно, если потребуется.
  • Выполнение ботом множества задач, например логирования, конфигурирования и веб-скрейпинга, чревато сильной связанностью кода, когда изменения одной части программы сказываются непредвиденными последствиями для других частей кода.
  • Функциональность программы зависит от прямых конкретных реализаций таких библиотек, как boto3, logging и pandas, то есть изменения в одном блоке кода чреваты «эффектом ряби» в других частях кода. Поэтому модификации приходится применять в нескольких местах.

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

Подход программной инженерии

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

Для соблюдения принципов SOLID конвейер разбивается на классы:

  • для выполнения только одной задачи  —  единственная ответственность;
  • с добавлением к нему со временем функциональности, но никогда, ни в какое время не удалением ее из него  —  принцип открытости/закрытости;
  • заменяемые на любые дочерние классы, созданные из них без какого-либо нарушения кода  —  подстановка Лисков;
  • в которых имеются только функции, применяемые всеми дочерними классами  —  разделение интерфейса;
  • зависимые только от абстракций  —  инверсия зависимостей.

Что сделал я?

Придумал классы:

  • ILogger для записи диагностических сообщений в файл и консоль;
  • Config для управления переменными среды;
  • IWebPageLoader для загрузки URL-адреса футбольного ресурса в браузере;
  • IPopUpHandler для закрытия окошка куков, отображаемого при первом входе на сайт;
  • IDataExtractor для извлечения футбольных данных из HTML-элементов сайта;
  • IDataTransformer для преобразования извлеченных данных во фрейм данных Pandas;
  • IFileUploader для загрузки преобразованных данных в облако или локальный компьютер.

Реализация

1. Logger

Исходный код GitHub для объекта Logger отслеживается здесь.

Что это за объект?

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

В моей кодовой базе этот объект  —  абстрактный класс ILogger, в котором содержится пять абстрактных методов для записи диагностических сообщений различных уровней критичности: debug, info, warning, critical и error.

Вот создаваемые дочерние классы 1-го уровня:

  • FileLogger для записи сообщений в лог-файл;
  • ConsoleLogger для потоковой передачи сообщений на консоль.

В ConsoleLogger содержатся собственные дочерние классы 2-го уровня:

  • ColouredConsoleLogger для потоковой передачи сообщений на консоль в цветном формате;
  • NonColouredConsoleLogger для того же без цветового форматирования.

Как им соблюдаются принципы SOLID?

  • Принцип единственной ответственности: у каждого родительского и дочернего классов имеется одна конкретная задача логирования.
  • Принцип открытости/закрытости: классы не модифицируются, но при необходимости в них добавляется функциональность.
  • Принцип подстановки Лисков:
  1. Классы FileLogger и ConsoleLogger заменяются родительским классом ILogger.
  2. Классы ColouredConsoleLogger и NonColouredConsoleLogger заменяются родительским классом ConsoleLogger.
  • Принцип разделения интерфейса: всеми дочерними классами на каждом уровне применяются одни и те же методы их родительского класса без реализации ненужных им методов.
  • Принцип инверсии зависимостей:
  1. Конкретный класс FileLogger и класс ConsoleLogger зависят от абстрактного класса ILogger.
  2. Классы ColouredConsoleLogger и NonColouredConsoleLogger зависят от абстрактного класса ConsoleLogger.
  3. Никакой класс не зависит ни от каких конкретных классов в объекте Logger.

2. Config

Исходный код GitHub для объекта Config отслеживается здесь.

Что это за объект?

Класс Config  —  это простой объект для хранения закрытых учетных данных для доступа к ключевым средам вроде основной корзины S3 на AWS и используемого локального компьютера.

В нем же флагом WRITE_FILES_TO_CLOUD указывается, сохранять ли обрабатываемые в конвейере файлы в облаке: True  —  сохранять, False  —  нет. Задавая False, вы сохраняете файлы локально в формате, указанном в загрузчике данных Data Loader.

Как им соблюдаются принципы SOLID?

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

3. Webpage Loader

Исходный код GitHub для объекта Webpage Loader отслеживается здесь.

Что это за объект?

Для загрузки указанного в объекте Webpage Loader URL-адреса применяется веб-драйвер Selenium. В моем коде этот объект  —  абстрактный интерфейс IWebPageLoader, от которого зависят другие загрузчики сайтов.

Создается дочерний класс 1-го уровня WebPageLoader, которым наследуется готовый к реализации в его дочернем классе абстрактный метод load_page.

Из WebPageLoader создается дочерний класс 2-го уровня PremLeagueTableWebPageLoader. На этом уровне, чтобы загрузить сайт с таблицей Премьер-лиги, реализуется метод load_page.

Как им соблюдаются принципы SOLID?

  • Принцип единственной ответственности: в объекте Webpage Loader у каждого родительского и дочернего классов имеется одна простая задача.
  • Принцип открытости/закрытости: чтобы добавить загрузчики сайтов для других европейских лиг, просто добавляем интерфейсы без изменения имеющегося кода.
  • Принцип подстановки Лисков:
  1. Класс WebPageLoader заменяется родительским классом IWebPageLoader.
  2. Класс PremLeagueTableWebPageLoader заменяется родительским классом WebPageLoader.
  3. Принцип разделения интерфейса: каждым дочерним классом применяется один и тот же метод load_page его родительского класса.
  • Принцип инверсии зависимостей:
  1. Абстрактный класс WebPageLoader зависит от абстрактного класса IWebPageLoader.
  2. Класс PremLeagueTableWebPageLoader зависит от абстрактного класса WebPageLoader.
  3. Никакой класс не зависит ни от каких конкретных классов в объекте Webpage Loader.

4. Popup Handler

Исходный код GitHub для объекта Popup Handler отслеживается здесь.

Что это за объект?

Объектом Popup Handler в браузере закрываются нежелательные всплывающие окна куков, рекламы и сервисов подписки. У меня этот объект  —  абстрактный класс IPopUpHandler, в котором содержится абстрактный метод close_popup.

Подобно иерархии классов Webpage Loader, от класса IPopUpHandler наследуется дочерний класс 1-го уровня PopUpHandler вместе с нереализованным абстрактным методом close_popup, которым закрываются всплывающие окна в браузере.

Метод close_popup реализуется дочерним классом 2-го уровня PremLeagueTablePopUpHandler.

Как им соблюдаются принципы SOLID?

  • Принцип единственной ответственности: в объекте Popup Handler на каждый родительский и дочерний классы приходится только по одной единице работы.
  • Принцип открытости/закрытости: благодаря разделению подклассов на интерфейсы поменьше, в платформу добавляется новое поведение без модификации имеющегося кода.
  • Принцип подстановки Лисков:
  1. Класс PopUpHandler заменяется родительским классом IPopUpHandler.
  2. Класс PremLeagueTablePopUpHandler заменяется своим родительским классом PopUpHandler.
  • Принцип разделения интерфейса: каждым дочерним классом применяется один и тот же метод close_popup его родительского класса.
  • Принцип инверсии зависимостей:
  1. Абстрактный класс PopUpHandler зависит от абстрактного класса IPopUpHandler.
  2. Конкретный класс PremLeagueTablePopUpHandler зависит от абстрактного класса PopUpHandler.
  3. Никакой класс не зависит ни от каких конкретных классов в объекте Popup Handler.

5. Data Extractor

Исходный код GitHub для объекта Data Extractor отслеживается здесь.

Что это за объект?

Data Extractor  —  это первый объект в программе, который занимается фактическими данными для обработки. Футбольные данные извлекаются им из интернета с помощью интерфейса IDataExtractor, абстрактного класса, в котором содержится абстрактный метод scrape_data.

У этого класса имеются дочерние классы:

  • На 1-м уровне абстрактный класс TableStandingsDataExtractor для таблицы с извлеченными данными о месте, занимаемом каждой футбольной командой. Он зависит от абстракции класса IDataExtractor.
  • На 2-м уровне конкретный класс PremLeagueTableStandingsDataExtractor для извлеченных данных о положении каждой команды Премьер-лиги в турнирной таблице. Именно здесь для выполнения скрейпинга реализуется метод scraped_data.

Как им соблюдаются принципы SOLID?

  • Принцип единственной ответственности: каждому родительскому и дочернему классам в объекте Data Extractor назначается одна задача.
  • Принцип открытости/закрытости: код спроектирован для добавления ожидаемого поведения без редактирования/удаления того, что в коде уже имеется.
  • Принцип подстановки Лисков:
  1. Класс TableStandingsDataExtractor заменяется родительским классом IDataExtractor.
  2. Класс PremLeagueTableStandingsDataExtractor заменяется своим родительским классом TableStandingsDataExtractor.
  • Принцип разделения интерфейса: каждым дочерним классом применяется один и тот же метод scraped_data его родительского класса, гарантируя независимость классов от ненужных им методов.
  • Принцип инверсии зависимостей:
  1. Абстрактный класс TableStandingsDataExtractor зависит от абстрактного класса IDataExtractor.
  2. Конкретный класс PremLeagueTableStandingsDataExtractor зависит от абстрактного класса TableStandingsDataExtractor.
  3. Никакой класс не зависит ни от каких конкретных классов в объекте Data Extractor.

6. Data Transformer

Исходный код GitHub для объекта Data Transformer отслеживается здесь.

Что это за объект?

Объект Data Transformer занимается очисткой данных и преобразованием их во фрейм данных. У меня этот объект  —  интерфейс IDataTransformer с одним абстрактным методом transform_data.

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

PremierLeagueTableStandingsDataTransformer  —  это конкретный класс, которым расширяется класс TableStandingsDataTransformer и реализуется метод transform_data.

Как им соблюдаются принципы SOLID?

  • Принцип единственной ответственности: у каждого класса в Data Transformer имеется только одна задача.
  • Принцип открытости/закрытости: чтобы добавить преобразователи данных для обработки других данных футбольной статистики, просто добавляем интерфейсы без изменения имеющегося кода.
  • Принцип подстановки Лисков:
  1. Класс TableStandingsDataTransformer заменяется родительским классом IDataTransformer.
  2. Класс PremierLeagueTableStandingsDataTransformer заменяется своим родительским классом TableStandingsDataTransformer.
  • Принцип разделения интерфейса: каждым дочерним классом применяется один и тот же метод transform_data.
  • Принцип инверсии зависимостей:
  1. Абстрактный класс TableStandingsDataTransformer зависит от абстрактного класса IDataTransformer.
  2. Класс PremierLeagueTableStandingsDataTransformer зависит от абстрактного класса TableStandingsDataTransformer.
  3. Никакой класс не зависит ни от каких конкретных классов в объекте Data Transformer.

7. Data Loader

Исходный код GitHub для объекта Data Loader отслеживается здесь.

Что это за объект?

Объектом Data Loader очищенные данные загружаются в целевую среду  —  облако или локальный компьютер,  —  сконфигурированную основным пользователем. Основной интерфейс IFileUploader  —  это абстрактный класс с абстрактным методом upload_file.

Вот как разделяются дочерние классы.

  • На 1-м уровне:

1. S3FileUploader для загрузки файлов в корзины S3 на Amazon, то есть расширение IFileUploader.

2. LocalFileUploader для загрузки файлов в целевые локальные папки, то есть расширение IFileUploader.

  • На 2-м уровне:

1. S3CSVFileUploader для загрузки CSV-файлов в корзины S3 на Amazon, то есть расширение S3FileUploader.

2. LocalCSVFileUploader для загрузки файлов в целевые локальные папки, то есть расширение LocalFileUploader.

  • На 3-м уровне:

1. PremierLeagueTableS3CSVUploader для загрузки CSV-файлов с данными о турнирном положении команд Премьер-лиги в корзины S3 на Amazon, то есть расширение S3CSVFileUploader.

2. PremierLeagueTableLocalCSVUploader для загрузки файлов с данными о турнирном положении команд Премьер-лиги в целевые локальные папки, то есть расширение LocalCSVFileUploader.

В интерфейсах 3-го уровня содержатся реализации upload_file.

Как им соблюдаются принципы SOLID?

  • Принцип единственной ответственности: классами 3-го уровня файлы загружаются в соответственные местоположения.
  • Принцип открытости/закрытости: включаются дополнительные форматы файлов, например JSON и текстовые, за счет добавления небольших интерфейсов без модифицирования имеющегося кода.
  • Принцип подстановки Лисков:
  1. Все классы 2-го уровня заменяются классами 1-го уровня.
  2. Все классы 3-го уровня заменяются классами 1-го и 2-го уровней.
  • Принцип разделения интерфейса: в IFileUploader содержится единственный метод upload_file, реализуемый всеми конкретными классами на 3-м уровне. Это единственный доступный необходимый метод.
  • Принцип инверсии зависимостей:
  1. Все дочерние классы 1-го уровня зависят от абстрактного базового класса IFileUploader.
  2. Все дочерние классы 2-го уровня зависят от классов 1-го уровня, все дочерние классы которого  —  абстрактные базовые классы.
  3. Все дочерние классы 3-го уровня зависят от классов 2-го уровня, все дочерние классы которого  —  абстрактные базовые классы.
  4. Никакой класс не зависит ни от каких конкретных классов в объекте Data Loader.

Использованные материалы

Заключение

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

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

В части 3 узнаем, согласуется ли функциональное программирование с принципами SOLID.

Читайте также:

Читайте нас в Telegram, VK и Дзен


Перевод статьи Stephen David-Williams: SOLID Principles in Data Engineering — Part 2

Предыдущая статьяКак организовать свою систему обработки данных: кейс mondayDB
Следующая статьяБольшой языковой модели недостаточно: пример использования Merkle Genai. Часть 2