Предисловие
Предполагается, что вы обладаете достаточным уровнем знаний в области программирования на 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?
- Принцип единственной ответственности: у каждого родительского и дочернего классов имеется одна конкретная задача логирования.
- Принцип открытости/закрытости: классы не модифицируются, но при необходимости в них добавляется функциональность.
- Принцип подстановки Лисков:
- Классы
FileLogger
иConsoleLogger
заменяются родительским классомILogger
. - Классы
ColouredConsoleLogger
иNonColouredConsoleLogger
заменяются родительским классомConsoleLogger
.
- Принцип разделения интерфейса: всеми дочерними классами на каждом уровне применяются одни и те же методы их родительского класса без реализации ненужных им методов.
- Принцип инверсии зависимостей:
- Конкретный класс
FileLogger
и классConsoleLogger
зависят от абстрактного классаILogger
. - Классы
ColouredConsoleLogger
иNonColouredConsoleLogger
зависят от абстрактного классаConsoleLogger
. - Никакой класс не зависит ни от каких конкретных классов в объекте 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 у каждого родительского и дочернего классов имеется одна простая задача.
- Принцип открытости/закрытости: чтобы добавить загрузчики сайтов для других европейских лиг, просто добавляем интерфейсы без изменения имеющегося кода.
- Принцип подстановки Лисков:
- Класс
WebPageLoader
заменяется родительским классомIWebPageLoader
. - Класс
PremLeagueTableWebPageLoader
заменяется родительским классомWebPageLoader
. - Принцип разделения интерфейса: каждым дочерним классом применяется один и тот же метод
load_page
его родительского класса.
- Принцип инверсии зависимостей:
- Абстрактный класс
WebPageLoader
зависит от абстрактного классаIWebPageLoader
. - Класс
PremLeagueTableWebPageLoader
зависит от абстрактного классаWebPageLoader
. - Никакой класс не зависит ни от каких конкретных классов в объекте 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 на каждый родительский и дочерний классы приходится только по одной единице работы.
- Принцип открытости/закрытости: благодаря разделению подклассов на интерфейсы поменьше, в платформу добавляется новое поведение без модификации имеющегося кода.
- Принцип подстановки Лисков:
- Класс
PopUpHandler
заменяется родительским классомIPopUpHandler
. - Класс
PremLeagueTablePopUpHandler
заменяется своим родительским классомPopUpHandler
.
- Принцип разделения интерфейса: каждым дочерним классом применяется один и тот же метод
close_popup
его родительского класса. - Принцип инверсии зависимостей:
- Абстрактный класс
PopUpHandler
зависит от абстрактного классаIPopUpHandler
. - Конкретный класс
PremLeagueTablePopUpHandler
зависит от абстрактного классаPopUpHandler
. - Никакой класс не зависит ни от каких конкретных классов в объекте Popup Handler.
5. Data Extractor
Исходный код GitHub для объекта Data Extractor отслеживается здесь.
Что это за объект?
Data Extractor — это первый объект в программе, который занимается фактическими данными для обработки. Футбольные данные извлекаются им из интернета с помощью интерфейса IDataExtractor
, абстрактного класса, в котором содержится абстрактный метод scrape_data
.
У этого класса имеются дочерние классы:
- На 1-м уровне абстрактный класс
TableStandingsDataExtractor
для таблицы с извлеченными данными о месте, занимаемом каждой футбольной командой. Он зависит от абстракции классаIDataExtractor
. - На 2-м уровне конкретный класс
PremLeagueTableStandingsDataExtractor
для извлеченных данных о положении каждой команды Премьер-лиги в турнирной таблице. Именно здесь для выполнения скрейпинга реализуется методscraped_data
.
Как им соблюдаются принципы SOLID?
- Принцип единственной ответственности: каждому родительскому и дочернему классам в объекте Data Extractor назначается одна задача.
- Принцип открытости/закрытости: код спроектирован для добавления ожидаемого поведения без редактирования/удаления того, что в коде уже имеется.
- Принцип подстановки Лисков:
- Класс
TableStandingsDataExtractor
заменяется родительским классомIDataExtractor
. - Класс
PremLeagueTableStandingsDataExtractor
заменяется своим родительским классомTableStandingsDataExtractor
.
- Принцип разделения интерфейса: каждым дочерним классом применяется один и тот же метод
scraped_data
его родительского класса, гарантируя независимость классов от ненужных им методов. - Принцип инверсии зависимостей:
- Абстрактный класс
TableStandingsDataExtractor
зависит от абстрактного классаIDataExtractor
. - Конкретный класс
PremLeagueTableStandingsDataExtractor
зависит от абстрактного классаTableStandingsDataExtractor
. - Никакой класс не зависит ни от каких конкретных классов в объекте Data Extractor.
6. Data Transformer
Исходный код GitHub для объекта Data Transformer отслеживается здесь.
Что это за объект?
Объект Data Transformer занимается очисткой данных и преобразованием их во фрейм данных. У меня этот объект — интерфейс IDataTransformer
с одним абстрактным методом transform_data
.
Наследуемым от этого интерфейса производным классом TableStandingsDataTransformer
, единственным его прямым дочерним классом, преобразовываются извлеченные данные о положении в турнирной таблице различных футбольных команд.
PremierLeagueTableStandingsDataTransformer
— это конкретный класс, которым расширяется класс TableStandingsDataTransformer
и реализуется метод transform_data
.
Как им соблюдаются принципы SOLID?
- Принцип единственной ответственности: у каждого класса в Data Transformer имеется только одна задача.
- Принцип открытости/закрытости: чтобы добавить преобразователи данных для обработки других данных футбольной статистики, просто добавляем интерфейсы без изменения имеющегося кода.
- Принцип подстановки Лисков:
- Класс
TableStandingsDataTransformer
заменяется родительским классомIDataTransformer
. - Класс
PremierLeagueTableStandingsDataTransformer
заменяется своим родительским классомTableStandingsDataTransformer
.
- Принцип разделения интерфейса: каждым дочерним классом применяется один и тот же метод
transform_data
. - Принцип инверсии зависимостей:
- Абстрактный класс
TableStandingsDataTransformer
зависит от абстрактного классаIDataTransformer
. - Класс
PremierLeagueTableStandingsDataTransformer
зависит от абстрактного классаTableStandingsDataTransformer
. - Никакой класс не зависит ни от каких конкретных классов в объекте 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 и текстовые, за счет добавления небольших интерфейсов без модифицирования имеющегося кода.
- Принцип подстановки Лисков:
- Все классы 2-го уровня заменяются классами 1-го уровня.
- Все классы 3-го уровня заменяются классами 1-го и 2-го уровней.
- Принцип разделения интерфейса: в
IFileUploader
содержится единственный методupload_file
, реализуемый всеми конкретными классами на 3-м уровне. Это единственный доступный необходимый метод. - Принцип инверсии зависимостей:
- Все дочерние классы 1-го уровня зависят от абстрактного базового класса
IFileUploader
. - Все дочерние классы 2-го уровня зависят от классов 1-го уровня, все дочерние классы которого — абстрактные базовые классы.
- Все дочерние классы 3-го уровня зависят от классов 2-го уровня, все дочерние классы которого — абстрактные базовые классы.
- Никакой класс не зависит ни от каких конкретных классов в объекте Data Loader.
Использованные материалы
- Старый подход инженерии данных.
- Новый подход с SOLID.
Заключение
Мы узнали, как принципы SOLID применяются в приложениях обработки данных на Python. С этими принципами проектирования необходимо тщательно рассматривать каждый компонент в рабочих нагрузках с данными, это основной принцип жизненного цикла разработки ПО.
Обеспечив проверку в кодовой базе всех пяти принципов SOLID, мы предохраним код от непреднамеренных изменений или неожиданностей.
В части 3 узнаем, согласуется ли функциональное программирование с принципами SOLID.
Читайте также:
- 4 ключевых аспекта проектирования распределенных систем
- Что на самом деле важно для качества кода?
- Как стать инженером Python в 2023 году
Читайте нас в Telegram, VK и Дзен
Перевод статьи Stephen David-Williams: SOLID Principles in Data Engineering — Part 2