Comments 58
class CatServiceImpl(AnimalService):
def __init__(self, cat: Cat):
self.cat = cat
Зачем для каждого котика создавать отдельный экземпляр службы?
for animal_serv in animal_services:
animal_serv.move()
animal_serv.make_sound()
Не представляю, при решении какой задачи такое может понадобиться.
ИМХО было бы логичней, создавая экземпляр CatServiceImpl
, передать в конструктор CatRepositoryImpl
. А затем использовать этот экземпляр службы для обработки списка котиков.
Данная специфика языка нам помогает строить архитектуру на основе модулей и отказаться от использования классов во множестве случаев.
Зачем надо отказываться от использования классов для множества случаев?
Поскольку во множестве случаев они избыточны)
Классы могут быть полезны при группировке поведения и свойств в одном компоненте. Но в случае stateless приложений, коими серверные приложения во многом являются, мы можем использовать модули. Это будет проще для восприятия и удобнее для использования.
А в чем проще? Ну не будет одной строчки class SomeService:
перед функциями, на что это влияет?
Класс SomeService скорее всего stateless. В него нужно будет внедрить SomeRepository, либо-же другие сервисы, с написанием конструктора и созданием объекта это уже 4 строки.
Далее, у нас нет встроенного менеджера бинов как в том же Spring Java, поэтому мы должны самостоятельно управлять нашими объектами SomeService.
Захотел синглтон(что вполне логично), пиши костыль рода:
some_service = SomeService()
del SomeService
А это еще самый короткий и простой способ и тот будет уже антипаттерном)
Итого, плюс 5-10 строк на каждый сервис в лучшем случае.
Также учтём усложнение восприятия управлением этого всего, добавление лишних 4 пробелов каждый раз для методов и получим, что в тех же сервисах можно писать без класса.
Класс SomeService скорее всего stateless. В него нужно будет внедрить SomeRepository
Экземпляр SomeRepository это и есть state для SomeService.
поэтому мы должны самостоятельно управлять нашими объектами SomeService
https://python-dependency-injector.ets-labs.org/introduction/di_in_python.html
Я не специалист в Python, но вот тут описывают внедрение зависимостей такое же, как в других языках.
Также не очень понятно, вот вы импортировали в виде модуля конкретный SaleRepository, который обращается к базе, а как в тестах вместо него подставить мок, который к базе не обращается?
Экземпляр SomeRepository это и есть state для SomeService.
SomeRepository тоже будет stateless, наше состояние - это как правило база данных, либо просто файлы. SomeRepository нужен лишь для того, чтобы вынести логику работы с базой/файлами.
https://python-dependency-injector.ets-labs.org/introduction/di_in_python.html
Я не специалист в Python, но вот тут описывают внедрение зависимостей такое же, как в других языках.
Можно использовать классы и внедрять зависимости схожим с другими языками образом. Я лишь хотел показать способ, где можно это дело упростить средствами того же python, без лишних библиотек или еще чего)
подставить мок, который к базе не обращается?
Очень просто на самом деле. Мы спокойно можем замокать любой объект и в python модуль то же объект)
+ у нас нет проблем той же java, где только сейчас появляются способы замокать статические методы и различные другие конструкции.
SomeRepository тоже будет stateless
Нет, в SomeRepository будет подключение к БД, а в подключении конкретный логин/пароль, или идентификатор сокета, или другие параметры, которые обеспечивают соединение с БД. Но для SomeService это неважно, для него состояние это ссылка на экземпляр репозитория. Также могут быть ссылки на другие компоненты (компонент для отправки email), или конкретные значения конфигурации (email администратора).
Подключение к БД - это подключение к БД))
Не нужно смешивать конфигурацию и логику работы с базой данных. Изменение типа хранение не должно затрагивать изменение интерфейса взаимодействия с SomeService, а в вашей ситуации это скорее всего произойдет. Тажке отдельно прописывается миграция, если нужно.
Почему оно вдруг произойдет? SomeRepository это уже конкретная реализация хранилища, там могут быть запросы на конкретном SQL-диалекте. Но на интерфейс это не влияет. Нужно другое хранилище - делаем другой репозиторий с тем же интерфейсом, и для вызывающего кода ничего не меняется.
В моем примере нигде не смешивается конфигурация и логика работы с базой данных. Сервис, репозиторий и драйвер БД это 3 разных класса, и ни один из них не stateless, у каждого есть свое состояние.
Нет, в SomeRepository будет подключение к БД, а в подключении конкретный логин/пароль, или идентификатор сокета, или другие параметры, которые обеспечивают соединение с БД
Параметры у баз данных могут отличаться и им не место в репозитории, который у нас stateless и наличие sql в реализации это не показатель наличия состояния)
Я не сказал, что параметрам баз данных место в репозитории. В моем примере три класса - сервис, репозиторий, подключение к БД (драйвер БД). В сервис передается репозиторий, в репозиторий передается инстанс класса для подключения к БД (например, результат вызова psycopg2.connect()). "Параметры баз данных" находятся в классе подключения к БД, а не в репозитории. Результат connect() это состояние репозитория, и у вас оно тоже есть, только вы его вынесли в глобальное пространство имен.
Я не сказал, что наличие SQL в реализации это показатель наличия состояния. Наличие SQL в реализации это показатель наличия типа хранения, это основное назначение репозитория, поэтому изменение типа хранения подразумевает использования другого репозитория, а не изменение интерфейса взаимодействия этого репозитория с сервисом.
Драйвер базы да, он содержит состояние, вы правы.
Но это глобальное состояние и оно одно на все приложение. С помощью него мы создаём сессию и при помощи репозиториев можем ее реализовывать.
Нам его достаточно одного и мы его можем инжектить во все реализации репозиториев и опять таки класс нам необязательно создавать для этого)
И SQL - это язык запросов. Если мы в сервисах меняем какую-либо сущность, это же не значит, что у нас в том же сервисе есть состояние. Состояние есть у модели, но не у сервиса.
Чувствую, что дискусия заходит в тупик, я учту Ваши доводы и постараюсь провести более корректное сравнение подходов на примере полной реализации веб-приложения в следующе й статье ?
Но это глобальное состояние и оно одно на все приложение.
Почему вдруг одно-то?) Вполне может быть 2 базы. Или какой-то репозиторий может брать сущности всегда из кеша, куда они добавляются другим приложением. Или пользователи могут храниться в сервисе авторизации. Сегодня база одна, а завтра пришел бизнес и сказал "А выведите мне в админке вот эти данные из Оракла от бизнес-аналитиков, веб-API у них нет".
Передачей через конструктор класса как раз и пытаются избежать глобального состояния.
Ну и да, если у вас класс без глобального состояния не работает, значит он не stateless. Stateless он может стать только если вы это состояние передаете в виде аргумента при вызове каждого метода.
Состояние есть у модели, но не у сервиса.
Нет. У сервиса свое состояние, у модели свое. Все зависимости, которые пробрасываются в сервис, это его состояние, оно инициализируется в конструкторе. Состояние инстанса некоторого класса - это значения полей этого класса, неважно, из базы мы их получили или еще откуда-то.
И SQL - это язык запросов.
Вы похоже не читаете, что пишет собеседник. SQL в моем комментарии никак не связан с доказательством наличия состояния где бы то ни было. Он связан с вашим утверждением про "изменение интерфейса взаимодействия".
Подключений к бд да, скорее всего будет несколько, тот же редис для хэширования.
Состояние у сервиса, оно быть может, а может и не быть.
У нас разное понимание stateless класса. В этом основное различие)
Бизнес данные меняются в основном только в бд. Запросами я не меняю поля того же sessionManager, поэтому он и будет глобальным и это уже как правило базовый класс. Объекта репозитория мне хватит и одного на все приложение, поскольку его функции не меняют свое поведение от изменения его полей. Это я и понимаю про stateless. Если учитывать базу, то у нас и слой адаптеров не будет stateless, поскольку тот же delete, post запрос будет вести себя не как чистая функция (особенно если id генерируется).
К SQL да, возможно слишком много акцента.
В общем, я понял, что Вы имеете ввиду. Да, если мы пробрасываем репозиторий в сервис, то в сервисе содержится поле с ним и это его состояние. И для адаптеров получится тоже самое. Однако stateless это про "состояние независимость" и поэтому говориться мной про синглтон. Если Вам удобнее внедрять зависимость через классы, дело Ваше, однако моя задача была показать иной способ и да, я верю и знаю что для слоев адаптер, сервис и репозиторий в случае python практически всегда проще использовать модули без классов, об этом и статья была по-большому счету.
Тестирование очень интересная тема, хотелось бы ее затронуть в следующей статье при полной реализации веб-сервиса.
Уважаемый автор:
В Ваших двух первых примерах класс Animal не абстрактый от слова совсем! И это уже, кажется, не первая Ваша статья где вы так ошибаетесь.
В классe TypeAnimal строчка
name_type: str
лишняя. От слова совсем.
В первом примере достаточно
animals: list[AnimalInterface] = [Cat(5, type_animal), Dog(10, type_animal)]
Наследовать класс Animal в первом примере от AnimalInterface и классы CatServiceImpl и DogServiceImpl от протокольного класса AnimalService во втором примере не нужно. Читайте https://mypy.readthedocs.io/en/stable/protocols.html
Во-первых, это моя первая статья здесь и я явно могу ошибиться только первый раз))
В остальных моментах я могу с вами согласиться, здесь как раз-таки и показывается необязательность данных действий.
И на счёт четвертого пункта, да, можно не наследовать, однако в таком варианте не лучшим образом подхватывается документация в реализации и возможно другие моменты. Мне хотелось написать аналогично джава-стилю, чтобы продемонстрировать избыточность.
Во-первых, это моя первая статья здесь и я явно могу ошибиться только первый раз))
Тогда извиняюсь, Видимо, "популярная" ошибка. Недавно видел в похожей по тематике статье.
однако в таком варианте не лучшим образом подхватывается документация в реализации и возможно другие моменты
Поскольку Вы не используете отформатированные каким-нибудь "стандартным" способом docstrings, то я вас тут совсем не понимаю.
Мне хотелось написать аналогично джава-стилю, чтобы продемонстрировать избыточность.
А вот это зря. В питоне при множественном наследовании, особенно, когда один и тот же класс наследуется несколько раз, порядок инициализации может оказаться нетривиальным. А вы даже конструкторы родителей не вызываете через super().
Верно Вы все пишите, но это игрушечный пример с животными, примитивным наследованием и упрощенной логикой)
Моя цель в этом примере разобрать случай выноса логики, а не написать идеальные классы...
Насчет docstring аналогичная история. В примере их нет, так как они избыточны, но никто не говорит, что их не может быть)
Вот только, увы, не вижу я в этих действительно игрушечных примерах пользы от "выноса логики". Ну, добавьте действие - "покормить голодноле животное"?
Учту этот момент, будет вовремя добавлю малость логики в пример ?
Прислушался к Вашим замечаниям и малость переписал примеры.
TypeAnimal, name_type согласен, в контексте примера лишнее, мыслил ограничением значений на уровне базы. Поменял на enum)
А почему вы упорно делаете type_animal свойством экземпляра, а не класса? Для того, чтобы можно было что-то подобное написать:
cat = Cat(name="Барсик", type_animal=TypeAnimal.AMPHIBIA)
???
Ну, кошка иногда может себя повести как еще та змея :)
Можно сделать и свойством класса. Это лишь один из вариантов нормализации схемы.
Или:
cat = Cat(name="Барсик")
Или:
cat = Animal(name="Барсик", type_animal=TypeAnimal.CAT)
Но зачем мешать эти два примера в одном?
Учите биологию, кошки подкласс млекопитающих)
TypeAnimal его определяет.
Да, можно сделать много уровней наследования, но если сущности будут SQL ориентированы, то возникнут трудности.
С какой целью вам определять класс в type_animal, если подкласс Cat или Dog уже его определяет? Для чего вам нужно это дублирование?
Повторюсь, для нормализации схемы.
Что за CRUDService? Какое он место занимает на исходной схеме? Что по Interface Segregation Principle?
Те же вопрос к CrudRepository. Почему это дженерик класс? Почему в нем именно такие сигнатуры методов? Гарантируете ли вы что для все сущностей все методы должны быть именно такие? Например, не везде id будет int, мало где нужен fetch_all, зато много где будут разные способы фильтрации. Почему у вас протоколы репозиториев лежат где-от отдельно от интеракторов (я же правильно понял что их роль выполняют функции в последнем блоке кода типа sell)?
Рекомендую к прочтению https://www.ben-morris.com/why-the-generic-repository-is-just-a-lazy-anti-pattern/
Правильные вопросы задаёте)
Касаемо разделения интерфейсов, по-хорошему нужно для каждого метода делать свой протокол и потом с помощью композиции создавать общий.
Статья хорошая, спасибо за рекомендацию. Но я все-равно считаю, что использование общего репозитория не самая плохая практика и в том же spring-data очень часто используется.
Протоколы лежат отдельно, здесь как мне видится больше дело вкуса, хотя скорее всего было бы да, логичнее их держать вместе.
# todo: добавить транзакционность
Вот весь ваш ДДД и закончился =)
"Сервисы" создаются типо для того чтобы иметь возможность сервис перенести из монолита в отдельный сервис и общаться с ним по апи. Насколько часто это происходит? Проще оставить эти 5 строчек имплементации и не делать лишних абстракций. Тем более что скорее всего вы эти абстракции наворотите неправильно.
Мое имхо, на последнюю инстанцию не претендую
Buzzword driven development
Немного переписал ваш код, чтобы было в полтора раза короче и понятнее:
Спойлер
from abc import ABC
class TypeAnimal:
name_type: str
def __init__(self, name_type):
self.name_type = name_type
class Animal(ABC):
weight: int
move_string: str
sound: str
type_animal: TypeAnimal
def __init__(self, weight: int):
self.weight = weight
def move(self):
print(self.move_string)
def make_sound(self):
print(self.sound)
class Cat(Animal):
move_string: str = "Cat do step"
sound: str = "Meow"
type_animal: str = TypeAnimal("mammalian")
class Dog(Animal):
move_string: str = "Dog do step"
sound: str = "Woof"
type_animal: str = TypeAnimal("mammalian")
if __name__ == "__main__":
# Вместо адаптеров)
animal_services: list[Animal] = [
Cat(5),
Dog(10),
]
for animal_serv in animal_services:
animal_serv.move()
animal_serv.make_sound()
Теперь неизменяемые свойства кошек и собак содержатся в определении класса, и их не надо:
1) Каждый раз прописывать руками при инициализации экземпляра
2) Бегать искать по определениям разных классов с непонятным функционалом.
Только вы оставили ошибки в коде, которые я выше отмечал. Класс Anymal - не абстрактный. В классе ТypeAnymal у вас name_type и класс аттрибут, и инстанс атрибут. И я бы еще сделал move() и make_sound() - classmethod'ами.
P.S. И вообще TypeAnymal пока выкинуть можно. Строка и строка.
> Класс Anymal - не абстрактный
Ну да, в нем нет ни одного абстрактного метода, т.к. в вашем примере кошки и собаки ходят и говорят совершенно одинаково.
> В классе ТypeAnymal у вас name_type и класс аттрибут, и инстанс атрибут.
Согласен, надо было убрать инит и создавать экземпляры как TypeAnimal(type_animal="mammalian")
.
Это я к чему такой зануда? Это я к тому, что раз уж вы приводите примеры абстракций, то пусть эти примеры будут настоящими. Игрушечными, но настоящими. А иначе совсем непонятно, зачем эти абстракции нужны.
Можно написать так:
from abc import ABC, abstractmethod
class Animal(ABC):
@property
@abstractmethod
def sound(self) -> str:
...
class Cat(Animal):
@property
def sound(self) -> str:
return "Meow"
class Dog(Animal):
pass
Cat()
Dog() # error: Cannot instantiate abstract class "Dog" with abstract attribute "sound" [abstract]
или так:
from typing import Protocol, ClassVar
class Animal(Protocol):
sound: ClassVar[str]
class Cat(Animal):
sound = "Meow"
class Dog(Animal):
pass
Cat()
Dog() # error: Cannot instantiate abstract class "Dog" with abstract attribute "sound" [abstract]
Понравился второй вариант, переписал похожим образом, только без ClassVar)
Sound это свойство класса, а не экземпляра, на что и указывает ClassVar. А вместе с Protocol они требуют, чтобы это поле было определено в одном из потомков.
class ReadableService(Protocol, Generic[T]):
def get_by_id(self, id: int) -> T:
pass
def get_all(self) -> list[T]:
pass
class CrudService(ReadableService, Generic[T], Protocol):
...
В данном случае CrudService намеренно наследует непосредственно Protocol? Так как вижу, что и ReadableService наследует Protocol, значит и CrudService при наследовании ReadableService должен унаследовать Protocol. Сорри за косноязычность.
ООП заставляет нас больше холиварить, чем заниматься написанием структурированного кода. Делать ли CRUD отдельным слоем, или как в Django делать Модель на основе данных? Вы предлагаете Классовый подход сервисного слоя. Я бы сделал набор простых функций...
Про CatServiceImpl в комментах, наверняка, уже выразились. Все эти примеры с Животными и Фигурами для объяснения ООП довольно коварны своей очевидной простотой. В реалиях будут Message, MessageService, MessageProvider, где придётся выбирать что и когда возвращать (данные или объекты, делать их ленивыми или нет, асинхронность там...)
В итоге, мы делаем всё за ради полиморфизма:
for animal in animals:
animal.move()
animal.make_sound()
Хотя Питон, будучи динамическим языком, позволяет и так делать вызовы из объектов, если нужные методы присутствуют. Если нам так нравится java-подход, зачем вообще брать python?
Есть прекрасное видео https://www.youtube.com/watch?v=t-IUY6QrJyU - The Problem with The Problem (Screencast) - это David Beazley, который по Питону несколько книг выпустил. Он показывает, как мы можем закопаться в деталях, даже не начав решать задачу.
И спасибо за статью!
В последних примерах как раз таки показывается применение простых функций, объединенных логически в модули. Для модулей можно сделать протоколы, если необходимо иметь абстракции. В ходе статьи я и вел к тому, что можно использовать модули и в базовых слоях с ними будет работать проще.
За видео спасибо, гляну как будет время.
Не делать CRUD вообще. Этот набор букв - вырожденный случай. В реальном мире у вас не CRUD, а что угодно - начиная от только R и заканчивая десятками действий на сущность.
В чистой архитектуре не упоминается нкиакой CRUD и сервисы. В ней мы проектируем отталкиваясь от сценариев использования. Интеракторы - это конкретные шаги сценариев, они уже вызывают вспомогательные классы (клиенты апишек, хранилище, "сервисы"), интерфейсы которых проектируются исходя из реальных требований конкретного сценария.
Даже если рассматривать только R(ead) - мы имеем разные варианты поиска сущностей. Где-то чиатем по ID, где-то получаем отфильтрованный список, где-то динамические фильтры зависящие от пользователя. С U(pdate) всё ещё хуже. Продукт может быть продан, деньги переведены на другой счет, дом построен. Сгребая всё под термин update мы теряем самое главное - то ради чего писался код, бизнес value конкретной единицы.
Зависит от задач. Если мы работаем с аналитикой, может быть куча таблиц, где операции самые стандартные. Тогда удобно использовать ORM и набор базовых операций.
Если компоненты системы во-многих поведенческий моментах отличаются, лучше писать как Вы предлагаете.
Не делать CRUD вообще. Этот набор букв - вырожденный случай. В реальном мире у вас не CRUD, а что угодно - начиная от только R и заканчивая десятками действий на сущность.
Как добавление\редактирование карточки товара противоречит какой либо архитектуре?
Сгребая всё под термин update мы теряем самое главное - то ради чего писался код, бизнес value конкретной единицы.
Вот тут я вообще не понял. Поясните, плиз...
Добавление и редактирование карточки товара актуально для карточки товара. А, например, редактирование проведенной транзакции - вещь странная. Редактировать сумму счета вообще не стоит просто так. Если вы пишете конкретную логику, там будут конкретные действия вроде "отредактировать карточку товара" (редактирование даты создания сюда не входит) или "поменять статус транзакции" (изменение суммы перевода сюда не выходит). Эти действия формально все udpate, но при этом они не абстрактные, они делают конкретный вид апдейта, имеющий определенный смысл с точки зрения бизнес логики. И другие апдейты при этом могут быть запрещены
Python и чистая архитектура…