Серия «Программирование и Туториалы»

15

Основы архитектуры для джунов: построение масштабируемых и чистых приложений на python (Туториал)

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

Немного теории чистого кода

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

  • В чем преимущества указания типов в python?

  • Каковы причины разделения приложения на слои?

  • Каковы преимущества использования ООП?

  • Каковы недостатки использования глобальных переменных или синглтонов?

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

Всегда указывайте типы

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

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

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

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

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

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

  6. Преимущества типизированных классов данных перед простыми структурами данных: Типизированные классы данных улучшают читаемость, работу со структурированными данными и безопасность типов по сравнению с массивами и кортежами. Они используют атрибуты вместо строковых ключей, что минимизирует ошибки из-за опечаток и улучшает автодополнение кода. Датаклассы также обеспечивают четкое определение структур данных, поддерживают значения по умолчанию, упрощают сопровождение и отладку кода.

Почему нам нужно разделить приложение на слои

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

Разделение забот

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

Возможность повторного использования

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

Масштабируемость

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

Удобство обслуживания

  • Упрощает обслуживание за счет локализации общих функций в отдельных слоях.

Улучшенная совместная работа

  • Команды могут работать над разными слоями независимо друг от друга.

Гибкость и адаптируемость

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

Тестируемость

  • Каждый слой можно тестировать независимо, что упрощает модульное тестирование и отладку.

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

Глобальные константы против инжектируемых параметров

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

Глобальные константы

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

  2. Ограниченный объем тестирования: Тестирование становится сложным при использовании глобальных констант, поскольку их нелегко переопределить. Разработчикам может потребоваться изменять глобальное состояние или использовать сложные обходные пути, чтобы приспособиться к различным сценариям тестирования, что повышает риск ошибок.

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

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

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

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

  7. Дублирование состояния на уровне модуля: В Python код на уровне модуля может выполняться несколько раз, если импорт происходит по разным путям (например, абсолютный и относительный). Это может привести к дублированию глобальных экземпляров и трудноотслеживаемым ошибкам в обслуживании.

Инжектируемые параметры

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

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

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

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

  5. Явное декларирование зависимостей: В DI компоненты явно объявляют о своих зависимостях, обычно через параметры конструктора или сеттеры. Такая ясность облегчает понимание, поддержку и расширение системы.

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

Процедурное программирование против ООП

Использование объектно-ориентированного программирования (ООП) и инъекции зависимостей (DI) может значительно повысить качество и сопровождаемость кода по сравнению с процедурным подходом с глобальными переменными и функциями. Вот простое сравнение, демонстрирующее эти преимущества:

Процедурный подход: Глобальные переменные и функции

Основы архитектуры для джунов: построение масштабируемых и чистых приложений на python (Туториал) Программирование, Python, Чистый код, Архитектура по, Разработка, Рефакторинг, Совершенный код, Туториал, Гайд, Длиннопост
  • Дублирование кода: database_config должен передаваться или обращаться глобально в нескольких функциях.

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

  • Высокая связанность: Функции напрямую зависят от глобального состояния и конкретных реализаций.

ООП + DI-подход

Основы архитектуры для джунов: построение масштабируемых и чистых приложений на python (Туториал) Программирование, Python, Чистый код, Архитектура по, Разработка, Рефакторинг, Совершенный код, Туториал, Гайд, Длиннопост
  • Уменьшено дублирование кода: Конфигурация базы данных инкапсулируется в объекте подключения.

  • Возможности DI: Легко заменить MySQLConnection на другой класс подключения к базе данных, например PostgresConnection, не изменяя код UserService.

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

  • Удобство моков и тестирования: UserService можно легко протестировать, внедрив заглушку DatabaseConnection.

  • Управление временем жизни объекта: Жизненным циклом соединений с базой данных можно управлять более детально (например, с помощью менеджеров контекста).

  • Использование принципов ООП: Демонстрирует наследование (абстрактный базовый класс), полиморфизм (реализация абстрактных методов) и протоколы (интерфейсы, определенные DatabaseConnection).

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

Создание программы

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

Начало нового проекта

Небольшой чек-лист:

1. Управление проектами и зависимостями с помощью Poetry

poetry new python-app-architecture-demo

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

2. Контроль версий с помощью Git

Инициализируйте гит:

git init

Добавьте файл .gitignore для исключения ненужных файлов из вашего репозитория. Используйте стандартный .gitignore, предоставленный GitHub, и добавьте остальные исключения, такие как .DS_Store для macOS и папки редакторов (.idea, .vscode, .zed, etc):

wget -O .gitignore https://raw.githubusercontent.com/github/gitignore/main/Pyth...

echo .DS_Store >> .gitignore

3. Управление зависимостями

Установите зависимости вашего проекта с помощью poetry:

poetry add fastapi pytest aiogram

Вы можете установить все зависимости позже, используя:

poetry install

Обратитесь к официальной документации каждой библиотеки, если вам нужны более конкретные инструкции.

4. Файлы конфигурации

Создайте файл config.py для централизации настроек приложения - это распространенный и эффективный подход.

Установите переменные окружения для секретов и настроек:

touch .env example.env

.env содержит конфиденциальные данные и должен быть git-ignored, в то время как example.env содержит placeholder или значения по умолчанию и хранится в репозитории.

5. Точка входа приложения

Определите точку входа вашего приложения в main.py:

python_app_architecture/main.py:

Основы архитектуры для джунов: построение масштабируемых и чистых приложений на python (Туториал) Программирование, Python, Чистый код, Архитектура по, Разработка, Рефакторинг, Совершенный код, Туториал, Гайд, Длиннопост

Сделайте свой проект пригодным для использования в качестве библиотеки и разрешите программный доступ, импортировав функцию run в __init__.py:

python_app_architecture/init.py

Основы архитектуры для джунов: построение масштабируемых и чистых приложений на python (Туториал) Программирование, Python, Чистый код, Архитектура по, Разработка, Рефакторинг, Совершенный код, Туториал, Гайд, Длиннопост

Включите прямое выполнение проекта с помощью Poetry, добавив ярлык в __main__.py. Это позволит вам использовать команду poetry run python python_app_architecture вместо более длинной poetry run python python_app_architecture/main.py.

python_app_architecture/main.py:

Основы архитектуры для джунов: построение масштабируемых и чистых приложений на python (Туториал) Программирование, Python, Чистый код, Архитектура по, Разработка, Рефакторинг, Совершенный код, Туториал, Гайд, Длиннопост

Определение каталогов и слоев

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

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

Как правило, имеет смысл версионировать API (например, создавая подкаталоги типа api/v1), но мы пока будем действовать проще и опустим этот шаг.

Основы архитектуры для джунов: построение масштабируемых и чистых приложений на python (Туториал) Программирование, Python, Чистый код, Архитектура по, Разработка, Рефакторинг, Совершенный код, Туториал, Гайд, Длиннопост
  • app

    • entities - структуры данных всего приложения. Чисто носители данных без логики.

    • general - чемодан с инструментами. Папка для общих утилит, помощников и оберток библиотек.

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

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

    • repositores - библиотекари. Хранители доступа к данным, абстрагирующие сложности взаимодействия с бд.

      • models - определения локальных структур базы данных, не путать с сущностями entities.

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

      • api_service - управляет внешними коммуникациями по http/s, структурированными вокруг фреймворка FastAPI.

        • dependencies - основные инструменты и помощники, необходимые для различных частей вашего API, интегрированные с помощью системы DI FastAPI

        • endpoints - конечные точки http интерфейса

        • schemas - определения структур данных для запросов и ответов апи

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

  • tests - директория предназначена исключительно для тестирования и содержит весь тестовый код, сохраняя четкое разделение с логикой приложения.

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

Основы архитектуры для джунов: построение масштабируемых и чистых приложений на python (Туториал) Программирование, Python, Чистый код, Архитектура по, Разработка, Рефакторинг, Совершенный код, Туториал, Гайд, Длиннопост

Обратите внимание, что entities - не активные компоненты, а лишь структуры данных, которые передаются между слоями:

Основы архитектуры для джунов: построение масштабируемых и чистых приложений на python (Туториал) Программирование, Python, Чистый код, Архитектура по, Разработка, Рефакторинг, Совершенный код, Туториал, Гайд, Длиннопост

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

Основы архитектуры для джунов: построение масштабируемых и чистых приложений на python (Туториал) Программирование, Python, Чистый код, Архитектура по, Разработка, Рефакторинг, Совершенный код, Туториал, Гайд, Длиннопост

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

Основы архитектуры для джунов: построение масштабируемых и чистых приложений на python (Туториал) Программирование, Python, Чистый код, Архитектура по, Разработка, Рефакторинг, Совершенный код, Туториал, Гайд, Длиннопост

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

Основы архитектуры для джунов: построение масштабируемых и чистых приложений на python (Туториал) Программирование, Python, Чистый код, Архитектура по, Разработка, Рефакторинг, Совершенный код, Туториал, Гайд, Длиннопост

Изучение кода

Эндпоинт

Начнем с конечной точки:

Основы архитектуры для джунов: построение масштабируемых и чистых приложений на python (Туториал) Программирование, Python, Чистый код, Архитектура по, Разработка, Рефакторинг, Совершенный код, Туториал, Гайд, Длиннопост
  1. Импортируем вспомогательную функцию инъекции зависимостей (мы рассмотрим ее через минуту)

  2. Импортируем UserProvider protocol для аннотации типа

  3. Конечная точка требует, чтобы тело запроса содержало схему UserCreate в формате json

  4. Параметр provider в функции register представляет собой экземпляр реализации UserProvider, инжектируемый FastAPI с помощью механизма Depends.

  5. В метод create_user функции UserProvider передаются распарсенные данные пользователя. Это демонстрирует четкое разделение проблем, когда уровень API делегирует бизнес-логику уровню провайдера, придерживаясь принципа, что интерфейсные уровни не должны содержать бизнес-логику.

UserProvider

Теперь давайте посмотрим на бизнес-логику:

Основы архитектуры для джунов: построение масштабируемых и чистых приложений на python (Туториал) Программирование, Python, Чистый код, Архитектура по, Разработка, Рефакторинг, Совершенный код, Туториал, Гайд, Длиннопост
  1. Определение интерфейса: UserProvider - это протокол, определяющий метод create_user, который должен реализовать любой класс, придерживающийся этого протокола. Он служит формальным контрактом для функциональности создания пользователя.

  2. Протокол наблюдателя: UserProviderOutput служит в качестве наблюдателя (или делегата), который получает уведомление о создании пользователя. Этот протокол обеспечивает свободное соединение и улучшает событийно-ориентированную архитектуру приложения.

  3. Реализация протокола: UserProviderImpl реализует логику создания пользователя, но ему не нужно явно декларировать свою приверженность UserProvider из-за динамической природы Python и использования утиной типизации.

  4. Основные зависимости: Конструктор принимает UserRepository и MailProvider - оба определены как протоколы - в качестве параметров. Полагаясь исключительно на эти протоколы, UserProviderImpl остается отделенным от конкретных реализаций, иллюстрируя принципы Dependency Injection, где провайдер не зависит от базовых деталей, взаимодействуя только через определенные контракты.

  5. Опциональный делегат вывода: Конструктор принимает необязательный экземпляр UserProviderOutput, который, если он предоставлен, будет уведомлен по завершении создания пользователя.

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

  7. Центральная бизнес-логика: Метод create_user инкапсулирует основную бизнес-логику для добавления пользователя, демонстрируя отделение от обработки API.

  8. Взаимодействие с репозиторием: Использует UserRepository для абстрагирования операций с базой данных (например, добавление пользователя), гарантируя, что провайдер не будет напрямую манипулировать базой данных.

  9. Расширенная бизнес-логика: Вовлекает отправку электронной почты через MailProvider, иллюстрируя, что обязанности провайдера могут выходить за рамки простых CRUD-операций.

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

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

Зависимости FastAPI

Хорошо, но как инстанцировать провайдер и внедрить его? Давайте посмотрим на код инъекции, реализованный с помощью DI-движка FastAPI:

Основы архитектуры для джунов: построение масштабируемых и чистых приложений на python (Туториал) Программирование, Python, Чистый код, Архитектура по, Разработка, Рефакторинг, Совершенный код, Туториал, Гайд, Длиннопост
  1. Получение сессии базы данных через систему инъекций зависимостей FastAPI, гарантируя, что каждый запрос имеет чистую сессию.

  2. Получение из состояния приложения экземпляра Coordinator, который отвечает за управление более широкими задачами на уровне приложения и выступает в качестве менеджера событий.

  3. Примечание: функция возвращает протокол, но не точную реализацию.

  4. Конструирование экземпляра UserProviderImpl путем инжекции всех необходимых зависимостей. Это демонстрирует практическое применение инъекции зависимостей для сборки сложных объектов.

  5. Инициализация UserRepository с сессией, полученной из DI-системы FastAPI. Этот репозиторий обрабатывает все операции по сохранению данных, абстрагируя взаимодействие с базой данных от провайдера.

  6. Настройка MailProvider с помощью конфигурационного токена.

  7. Инжектирование Coordinator в качестве выходного протокола. При этом предполагается, что Coordinator реализует протокол UserProviderOutput, что позволяет ему получать уведомления о создании пользователя.

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

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

Координатор

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

Основы архитектуры для джунов: построение масштабируемых и чистых приложений на python (Туториал) Программирование, Python, Чистый код, Архитектура по, Разработка, Рефакторинг, Совершенный код, Туториал, Гайд, Длиннопост
  1. Некоторые состояния могут быть общими для разных провайдеров, служб, слоев и всего приложения.

  2. Сборка реализаций и внедрение зависимостей

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

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

  5. Запустить все сервисы в отдельных потоках

  6. Уже запускается в отдельном потоке внутри сервиса

  7. Некоторая кросс-сервисная логика, просто для примера

  8. Пример управления сервисами из координатора

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

Контейнер DI

Однако в крупномасштабных приложениях ручное использование DI может привести к появлению значительного количества шаблонного кода. Именно тогда на помощь приходит DI Container. DI Containers, или Dependency Injection Containers, - это мощные инструменты, используемые при разработке программного обеспечения для управления зависимостями в приложении. Они служат в качестве центрального места, где регистрируются и управляются объекты и их зависимости. Когда объекту требуется зависимость, DI-контейнер автоматически обрабатывает инстанцирование и предоставление этих зависимостей, гарантируя, что объекты получат все необходимые компоненты для эффективного функционирования. Такой подход способствует свободному соединению, улучшает тестируемость и общую сопровождаемость кодовой базы за счет абстрагирования сложной логики управления зависимостями от бизнес-логики приложения. DI-контейнеры упрощают процесс разработки, автоматизируя и централизуя конфигурацию зависимостей компонентов.

Для python существует множество библиотек, предоставляющих различные реализации DI Container, я просмотрел почти все из них и записал лучшие IMO

  • python-dependency-injector - автоматизирован, основан на классах, имеет различные варианты жизненного цикла, такие как Singleton или Factory

  • lagom - интерфейс словаря с автоматическим разрешением

  • dishka - хороший контроль области видимости через менеджер контекста

  • that-depends - поддержка контекстных менеджеров (объекты должны быть закрыты в конце), встроенная интеграция fastapi

  • punq - более классический подход с методами register и resolve.

  • rodi - классический, простой, автоматический

main.py

В завершение обновим файл main.py:

Основы архитектуры для джунов: построение масштабируемых и чистых приложений на python (Туториал) Программирование, Python, Чистый код, Архитектура по, Разработка, Рефакторинг, Совершенный код, Туториал, Гайд, Длиннопост

Заключение

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

Этот подход универсален для различных приложений на Python. Он эффективен для бэкенд-серверов без состояния, например, построенных с помощью FastAPI, но его преимущества особенно ярко проявляются в приложениях без фреймворка и приложениях, управляющих состоянием. Сюда относятся настольные приложения (как с графическим интерфейсом, так и с командной строкой), а также системы, управляющие физическими устройствами, например IoT-устройствами, робототехникой, дронами и другими технологиями, ориентированными на аппаратное обеспечение.

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

О других интересных проектах периодически пишу в телеграм.

Показать полностью 16
13

S.T.A.R.K. — первый фреймворк для создания голосового ассистента

Привет, любители технологий! Сегодня я рад представить вам S.T.A.R.K. (Speech and Text Algorithmic Recognition Kit). Если вы когда-либо мечтали создать голосового ассистента, который будет современным, продвинутым и невероятно интуитивным, S.T.A.R.K. - лучшее решение.

Почему именно S.T.A.R.K.?

  1. Автономность и конфиденциальность: S.T.A.R.K. работает полностью на устройстве, гарантируя сохранность ваших данных.

  2. Распознавание контекста: С S.T.A.R.K. вы можете легко определять контекст и параметры для последующих запросов, а также одновременно выполнять несколько команд.

  3. Асинхронные команды: Запустите задачу и продолжайте использовать ваш голосовой ассистент. S.T.A.R.K. уведомит вас о завершении.

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

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

  6. Расширяемость с помощью ведущих языковых моделей: Улучшите когнитивные способности S.T.A.R.K., интегрируя его с ведущими языковыми моделями, такими как ChatGPT.

  7. Поддержка нескольких языков: Взаимодействуйте со своим голосовым ассистентом на нескольких языках.

  8. Полная настройка: Создавайте сложные команды, интегрируйте различные голосовые или текстовые интерфейсы и даже переопределяйте существующие классы.

  9. Поддержка сообщества: Присоединяйтесь к репозиторию STARK-PLACE и пользуйтесь библиотекой расширений от сообщества.

Присоединяйтесь к нам

Для более подробного изучения каждой функции и понимания всех возможностей S.T.A.R.K. посетите официальную документацию и репозиторий на GitHub. Если вам понравился фреймворк, не забудьте добавить звезду ⭐ репозиторию.

Показать полностью
15

Делаем нативное мобильное приложение с ИИ и бэкендом (Туториал)

Предыдущие статьи:

В этой статье мы рассмотрим все тонкости создания Proof of Concept (PoC) мобильного приложения, построенного с помощью фреймворка SwiftUI и бэкенда с использованием FastAPI. Дополнительно я продемонстрирую эффективные архитектурные паттерны для SwiftUI-приложений, в частности MVVMP в сочетании с принципами SOLID и Dependency Injection (DI). Для android код можно легко перевести на Kotlin с Jetpack Compose.

Зачем нам нужен бэкенд

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

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

  1. Безопасность: Бэкэнд помогает защитить конфиденциальные данные и токены аутентификации пользователей от атак типа MITM (Man-in-the-Middle). Он выступает в качестве защищенного шлюза между пользовательским устройством и базой данных или внешними службами, обеспечивая шифрование и аутентификацию всех данных.

  2. Контроль над использованием сервисов: Управляя API и взаимодействием с пользователями через бэкэнд, вы можете отслеживать и контролировать использование приложения. Это включает в себя дросселирование для управления нагрузкой, предотвращение злоупотреблений и обеспечение эффективного использования ресурсов.

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

  4. Модели подписки и Freemium: Реализация услуг по подписке или модели freemium требует наличия бэкенда для выставления счетов, отслеживания использования и управления уровнями пользователей. Бэкэнд может безопасно обрабатывать платежи и подписки, обеспечивая бесперебойную работу пользователей и соблюдая требования по защите данных.

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

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

Объяснение технического стека

  • SwiftUI: Лучший вариант для нативных приложений для iOS после выхода UIKit. Он декларативен и упорядочен, а XCode является незаменимым редактором благодаря эпл. Для android код можно легко перевести на Kotlin с помощью Jetpack Compose.

  • FastAPI: Выбран для бэкенда за его скорость, минимальное количество шаблонов и декларативность, редактируется с помощью превосходного Zed.dev.

  • ChatGPT API: Используется в качестве большой языковой модели (LLM); выбор может меняться в зависимости от необходимости кастомизации.

  • Ngrok: Реализует туннелирование с помощью простой команды CLI для выхода локального сервера в интернет.

Создание приложения для iOS

Теория: Архитектурные паттерны

  1. MVVMP (Model View ViewModel Presenter):

  • Model: Представляет собой структуры данных, используемые в приложении, такие как Question, Answer, Questionary и FilledQuestionary. Эти модели просты и содержат только данные, следуя принципу KISS.

  • View: Отвечают только за представление пользовательского интерфейса и делегируют все данные и логику презентерам. Они не содержат никакой бизнес-логики и спроектированы так, чтобы быть простыми и сосредоточенными на рендере UI.

  • ViewModel: В SwiftUI ViewModel представлена объектом ObservableObject, который служит моделью наблюдения за изменяемыми данными. Здесь нет методов и логики.

  • Presenter: Presenter управляет всей логикой, связанной с модулем (экраном или представлением), но не бизнес-логикой. Он взаимодействует с доменным слоем для выполнения операций бизнес-логики, таких как взаимодействие с API или управление сохранением данных.

  • Domain Layer: Этот слой содержит бизнес-логику приложения и взаимодействует с внешними ресурсами, такими как базы данных, API или другие сервисы. Он состоит из нескольких компонентов, таких как сервисы, провайдеры, менеджеры, репозитории, мапперы, фабрики и т. д.

  • На самом деле, MP в MVVMP является инициалами Марка Паркера, а полная форма — «Model View ViewModel by Mark Parker».

  1. Принципы СОЛИД:

    • Принцип единой ответственности: У каждого класса должна быть только одна причина для изменений.

    • Принцип открытость-закрытость: Компоненты должны быть открыты для расширения, но закрыты для модификации.

    • Принцип замещения Лискова: Объекты суперкласса должны быть заменяемы объектами подклассов.

    • Принцип разделения интерфейсов: Ни один клиент не должен быть вынужден зависеть от интерфейсов, которые он не использует.

    • Принцип инверсии зависимостей: Зависимость от абстракций, а не от конкретики, чему способствует DI.

  2. Инъекция зависимостей (DI): Реализация с использованием DI-контейнера для соблюдения принципа инверсии зависимостей.

Разработка бэкенда

Код бэкенда довольно прост. Эндпоинты (main.py):

Делаем нативное мобильное приложение с ИИ и бэкендом (Туториал) Программирование, Разработка, iOS, Искусственный интеллект, Туториал, Открытый код, Чистый код, Архитектура по, Frontend, Backend, Open Source, Длиннопост

"onboarding" предоставляет список вопросов анамнеза, которые необходимо заполнить при первом запуске приложения. Ответы будут сохранены на устройстве и использованы для персонализированной диагностики в будущем. "doctor" — основной эндпоинт: он генерирует вопросы на основе предыдущих ответов и карты пользователя, либо возвращает результат диагностики.

Модели:

Делаем нативное мобильное приложение с ИИ и бэкендом (Туториал) Программирование, Разработка, iOS, Искусственный интеллект, Туториал, Открытый код, Чистый код, Архитектура по, Frontend, Backend, Open Source, Длиннопост

Промпты:

Делаем нативное мобильное приложение с ИИ и бэкендом (Туториал) Программирование, Разработка, iOS, Искусственный интеллект, Туториал, Открытый код, Чистый код, Архитектура по, Frontend, Backend, Open Source, Длиннопост

Модуль промптов использует GPT-3.5 от OpenAI для генерации ответов на основе пользовательского ввода, анамнеза и заполненных анкет. Он возвращает пользователю соответствующие вопросы и советы по диагностике здоровья. Как видите, ничего сложного здесь нет. Код элементарен, а промпты - просто набор четких инструкций для LLM.

Настройте окружение и запустите сервер с помощью fastapi dev main.py.

Подробности:

Открытие доступа к локальному хосту через Интернет

  1. Зарегистрируйтесь на сайте ngrok.com и получите токен доступа.

  2. Установите ngrok с сайта ngrok.com/download.

  3. Выполните команду ngrok config add-authtoken <TOKEN>.

  4. Запустите с помощью команды ngrok http http://localhost:8080 (при необходимости измените порт).

Подробные инструкции по настройке можно найти в документации ngrok.

Кодим приложение

Я не буду показывать здесь весь исходный код, для этого есть GitHub. Найти его можно по адресу: HouseMDAI iOS App. Вместо этого я остановлюсь только на важных (IMO) моментах.

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

Первым делом нам нужны модели, и они довольно просты (принцип KISS).

Делаем нативное мобильное приложение с ИИ и бэкендом (Туториал) Программирование, Разработка, iOS, Искусственный интеллект, Туториал, Открытый код, Чистый код, Архитектура по, Frontend, Backend, Open Source, Длиннопост

Теперь давайте сделаем онбординг. Продолжаем следовать KISS и SRP (Single Responsibility Principle), никакой бизнес-логики в представлениях, только UI. В данном случае - только список вопросов с прокруткой. Все данные и логика делегированы презентеру. Единственное, что здесь интересно, это небольшой вспомогательный метод bindingForQuestion, который, вероятно, должен быть в презентере, но сейчас это не имеет значения.

Делаем нативное мобильное приложение с ИИ и бэкендом (Туториал) Программирование, Разработка, iOS, Искусственный интеллект, Туториал, Открытый код, Чистый код, Архитектура по, Frontend, Backend, Open Source, Длиннопост

Вы будете удивлены, но в презентере также нет никакой бизнес-логики!

Делаем нативное мобильное приложение с ИИ и бэкендом (Туториал) Программирование, Разработка, iOS, Искусственный интеллект, Туториал, Открытый код, Чистый код, Архитектура по, Frontend, Backend, Open Source, Длиннопост

Все по-прежнему simple, stupid и имеет только одну ответственность. Presenter должен содержать только логику своего представления. Бизнес-логика уровня приложения находится вне его юрисдикции, поэтому презентер просто делегирует ее наверх по стэку вызова.

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

При использовании паттерна Dependency Injection зависимости предоставляются извне класса, а не инстанцируются внутри него. Это способствует развязке и позволяет упростить поддержку и тестирование кода.

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

Если вы рассматриваете возможность использования протоколов в представлениях SwiftUI, необходимо помнить об одном важном моменте. Поскольку View в SwiftUI - это структура, она требует явного указания типов своих свойств. Это означает, что вам придется сделать её дженериком и пробрасывать тип через весь стек вызовов, что приведет к появлению большого количества шаблонного кода.


Однако существует альтернативный подход, предлагаемый MarkParker5/AnyObservableObject.
Эта библиотека работает аналогично родным оберткам свойств SwiftUI, но убирает проверку типа во время компиляции в пользу проверки во время рантайма. Хотя такой подход может внести некоторые риски, их легко снизить, написав элементарные xcode тесты, которые просто инициализируют представления так же, как вы делаете это во время рантайма.

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

Итак, если презентер не содержит бизнес-логику, то где же она? Это задача для доменного слоя, который обычно содержит сервисы, провайдеры и менеджеры. У них всех очень схожее применение, и разница между ними до сих пор является предметом дискуссий. Давайте создадим OnboardingProvider, который будет содержать всю бизнес-логику процесса онбординга.

Делаем нативное мобильное приложение с ИИ и бэкендом (Туториал) Программирование, Разработка, iOS, Искусственный интеллект, Туториал, Открытый код, Чистый код, Архитектура по, Frontend, Backend, Open Source, Длиннопост

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

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

Делаем нативное мобильное приложение с ИИ и бэкендом (Туториал) Программирование, Разработка, iOS, Искусственный интеллект, Туториал, Открытый код, Чистый код, Архитектура по, Frontend, Backend, Open Source, Длиннопост

Это приложение SwiftUI устанавливает свое начальное состояние с помощью оберток полей StateObject. Оно инициализирует OnboardingProvider, OnboardingPresenter и HomePresenter в своем методе init. Провайдер OnboardingProvider отвечает за управление данными, связанными с онбордингом, а OnboardingPresenter управляет логикой представления онбординга. HomePresenter управляет главным домашним представлением.

В теле сцены приложения проверяется, нужна ли регистрация на сайте. Если да, то она представляет OnboardingView с OnboardingPresenter. В противном случае она представляет TabView, содержащий HomeView с HomePresenter и, если доступно, ProfileView.

Теперь настало время для домашнего экрана. Логика проста:

  1. Получаем сообщение от пользователя

  2. Используя сообщение, запрашиваем список вопросов из бэкенда

  3. Показываем вопросы по одному, используя встроенную push-навигацию.

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

  5. Показываем финальный результат

Делаем нативное мобильное приложение с ИИ и бэкендом (Туториал) Программирование, Разработка, iOS, Искусственный интеллект, Туториал, Открытый код, Чистый код, Архитектура по, Frontend, Backend, Open Source, Длиннопост

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

Делаем нативное мобильное приложение с ИИ и бэкендом (Туториал) Программирование, Разработка, iOS, Искусственный интеллект, Туториал, Открытый код, Чистый код, Архитектура по, Frontend, Backend, Open Source, Длиннопост

Он управляет вводом сообщения пользователем и обновляет путь навигации на основе ответов от бэкенда.

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

Аналогично, когда заполняется вопросник, метод onQuestionaryFilled() отправляет заполненный вопросник на бэкенд и соответствующим образом обновляет путь навигации.

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

Модуль Questionary (View+Presenter) почти является копией Onboarding и просто делегирует логику до HomePresenter, поэтому я не вижу необходимости показывать код. Опять же, для этого есть github.

Последнее, что я хочу показать, это две реализации DoctorProvider, единственной обязанностью которых является вызов API и возврат DoctorResponse.

Делаем нативное мобильное приложение с ИИ и бэкендом (Туториал) Программирование, Разработка, iOS, Искусственный интеллект, Туториал, Открытый код, Чистый код, Архитектура по, Frontend, Backend, Open Source, Длиннопост

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

Делаем нативное мобильное приложение с ИИ и бэкендом (Туториал) Программирование, Разработка, iOS, Искусственный интеллект, Туториал, Открытый код, Чистый код, Архитектура по, Frontend, Backend, Open Source, Длиннопост

Вторая вызывает openai api напрямую (подход backendless) и является практически копией модуля подсказок из бэкенда:

Делаем нативное мобильное приложение с ИИ и бэкендом (Туториал) Программирование, Разработка, iOS, Искусственный интеллект, Туториал, Открытый код, Чистый код, Архитектура по, Frontend, Backend, Open Source, Длиннопост

Обе реализации легко взаимозаменяются благодаря инъекции зависимостей:

Делаем нативное мобильное приложение с ИИ и бэкендом (Туториал) Программирование, Разработка, iOS, Искусственный интеллект, Туториал, Открытый код, Чистый код, Архитектура по, Frontend, Backend, Open Source, Длиннопост

Другой пример

Посмотреть пример этой архитектуры в реальном приложении можно в моем проекте TwiTreads на github.com/MarkParker5/TwiTreads

Что делать дальше

  • Интегрируйте аутентификацию и базу данных пользователей в бэкенд. Можете использовать официальный шаблон FastAPI из FastAPI Project Generation.

  • Реализуйте логику аутентификации в приложении.

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

Заключение

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

О других интересных проектах периодически пишу в телеграм.

Показать полностью 14

Как локализовать все ваши iOS приложения на 20 языков за 5 минут

Исходный пост: https://markparker.me/blog/localize-ios-app-in-5-minutes

Предисловие

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

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

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

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

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

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

Подготовьте свой проект

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

  1. Создайте файл Localizable.strings и заполните его строками, которые вы хотите локализовать.

    /* optional description */ "[key]" = "[string]"; /* Example: */ "welcome text" = "Welcome to XCodelocalize";

  2. Перейдите в настройки файла проекта, добавьте желаемые языки. Apple рекомендует EFIGS (английский французский итальянский немецкий испанский) + китайский язык в качестве основы.

  3. Создайте файлы Localizable.strings для всех выбранных языков. Скорее всего, вы так же захотите локализовать файлы plist, storyboard и intentdefinition.

Установка XCodeLocalize

Неожиданно, инструмент для iOS разработки оказался написанным на питоне, так что убедитесь, что у вас есть python 3.9+

После этого вы можете установить xcodelocalize с помощью pip:

pip3 install xcodelocalize

Также доступна установка из .whl файла на странице релизов или с помощью poetry из исходников.

Запустите автоматическую локализацию

cd к корневой папке проекта и запустите

xcodelocalize [OPTIONS]

или

python3 -m xcodelocalize [OPTIONS]

Параметры

  • --base-language: код языка, с которого будут переведены все строки. [по умолчанию: 'en']

  • --override / --no-override: булево значение; указывает, будут ли переведены строки, которые уже существуют в файле. Перевести, если override; пропустить, если no-override. [по умолчанию: no-override]

  • --format-base / --no-format-base: сортировать строки base файла по ключу. [по умолчанию: no-format-base]

  • --file: Имена .strings файлов для перевода. Можно указать несколько файлов. Если не указано, все файлы будут переведены. [по умолчанию: None]
    Пример:

    xcodelocalize --file InfoPlist xcodelocalize --file InfoPlist --file MainStoryboard --file Localizable

  • --key: Ключи, которые должны быть переведены. Можно указать несколько ключей. Если не указано, все будут переведены. [по умолчанию: None]

  • --language: Код языков, которые будут переведены. Допускается множество значений. Если не указано, все файлы будут переведены. [по умолчанию: None]

  • --log-level: Один из [progress|errors|group|string]. [по умолчанию: group]

  • --help: Информация о параметрах

Автоматизация

Вы можете перейти в Target -> Build Phases -> New Run Script Phase в вашем проекте Xcode и ввести xcodelocalize. Это будет переводить необходимые строки во время сборки и ваши файлы всегда будут локализованы.

Заключение

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

Не стесняйтесь оставлять комментарии. Поставьте звезду репозиторию, если вам понравилось.

Показать полностью
Отличная работа, все прочитано!