Хорошо спроектированная программа состоит из объектов, отвечающих принципу единственной обязанности (каждый объект делает только одну вещь, и делает это хорошо). Такие объекты постоянно “общаются” друг с другом. Для такого общения, один объект должен что-то знать о другом объекте, а это вызывает зависимость между ними. И зависимость между объектами неизбежна. Но сильная зависимость приводит к большим проблемам. Изменение одного объекта приводит к изменению другого объекта, а затем и к изменению третьего объекта… Получается спагетти код.
Пример классов с сильной зависимостью:
class Customer: def __init__(self, name, email, balance): self.name = name self.email = email self.balance = balance def charge(self, amount): self.balance -= amount mailer = MailService( 'mail.mailservice.com', 'secret_username', 'secret_password') mailer.send(self.email, 'admin@example.com', 'Payment', f'Charging {amount}') class MailService: def __init__(self, host, username, password): self.host = host self.username = username self.password = password def send(self, to, from_, subject, text): print(f'Connecting to "{self.host}" ' f'with "{self.username}:{self.password}"') print(f'Sending "{subject}: {text}"' f'to "{to}" from "{from_}"') wayne = Customer('Bruce Wayne', 'brucy@mail.com', 1000) wayne.charge(50)
У объекта есть зависимость когда он знает:
- имя другого класса. Класс
Customer
знает имя классаMailService
- требуемые аргументы для сообщения и их порядок. Класс
Customer
знает, что для того, чтобы создать объект классаMailService
, ему нужно передать 3 параметра в определенном порядке - имя сообщения, которое он собирается отправить еще кому-то кроме себя самого (кроме
self
). КлассCustomer
знает, чтоMailService
отзывается на сообщениеsend
Как я сказал выше, полностью устранить зависимости между объектами нельзя, но свести их к минимуму можно. Об этом и пойдет речь ниже.
Внедрение зависимости (dependency injection)
Почему плохо не внедрять
У объекта есть зависимость когда он знает имя другого класса.
В примере выше, класс Customer
в методе charge
ссылается на класс MailService
. Такая сильная связь между Customer
и MailService
– безосновательна. Ведь все, что нужно классу Customer
– это объект, который будет отзываться на сообщение send
(отправить email). Этим объектом может быть экземпляр любого другого класса, в котором определен метод send
. Но при такой реализации класса Customer
, внедрить такой объект будет невозможно.
Как внедрять
Зависимость между Customer
и MailService
легко устранить с помощью паттерна “Dependency Injection”. Вместо того, чтобы создавать объект внутри метода Customer.charge
, мы внедряем его в класс извне. Внедрить зависимость в класс можно двумя способами:
- Через конструктор класса:
class Customer: def __init__(self, mailer, ...): self.mailer = mailer ... mail_service = MailService( 'mail.mailservice.com', 'secret_username', 'secret_password') customer = Customer(mail_service, ...)
- Через параметры метода:
class Customer: def charge(self, mailer, ...): ... mail_service = MailService( 'mail.mailservice.com', 'secret_username', 'secret_password') customer = Customer(...) customer.charge(mail_service, ...)
Какой из методов внедрения лучше? Зависит от ситуации. В данном примере разумнее использовать внедрение через параметр, так как при каждом вызове метода charge
, мы можем захотеть использовать разные объекты для отправки сообщения на почту. То есть все те объекты, которые отзываются на сообщение send
.
Плюсы внедрения
Код легко тестировать
Представьте, что метод send
класса MailService
отправляет настоящее сообщение на почту. Ваша задача – написать тесты к методу charge
класса Customer
. Как это сделать?
Так как метод send
вызывается при каждом вызове метода charge
, то ваши тесты заспамят чью-то почту. Решение – создать фейковый объект класса MailService
, который при вызове метода send
не будет отправлять настоящее сообщение на почту. Такие фейковые объекты называют mock-объектами.
Как создать mock-объект это тема отдельной статьи. Сейчас важно то, что при тестировании, в методе charge
должен использоваться mock-объект, вместо настоящего объекта класса MailService
. Это легко сделать когда класс Customer
принимает зависимости извне. Все что для этого нужно – это создать mock-объект в тестах и передать его затем в класс. При таком раскладе вы имеете полный контроль над зависимостями класса. Но если объект класса MailService
создается внутри метода charge
, то заменить этот объект на mock-объект уже сложнее.
Код гибче
Вы можете легко изменить поведение класса без изменения кода самого класса. Все, что для этого нужно – внедрить объект с другим поведением в класс.
Для примера, вместо внедрения объекта класса MailService
, вы можете внедрить объект класса AnotherMailService
, у которого есть метод send
, но с иной реализацией.
Не получается внедрить
В крайнем случае, если на изменение кода накладываются ограничения и из-за этого нет возможности внедрить зависимость, то изолируйте зависимость внутри класса. Для этого есть два способа:
- Изолировать внутри конструктора класса:
class Customer: def __init__(self, name, email, balance): ... self.mailer = MailService( 'mail.mailservice.com', 'secret_username', 'secret_password')
- Изолировать внутри свойства/метода:
class Customer: ... @property def mailer(self): if not hasattr(self, '_mailer'): self._mailer = MailService( 'mail.mailservice.com', 'secret_username', 'secret_password') return self._mailer
Раньше зависимость была спрятана глубоко в методе charge
, а теперь сразу видно, что Customer
зависит от MailService
. Такой код проще изменять.
Использование именованных параметров
У объекта есть зависимость когда он знает требуемые аргументы для сообщения и их порядок.
При отправлении сообщения объекту, вы должны знать аргументы, которые требуются для этого сообщения. Эта зависимость неизбежна. Но есть и другая зависимость, которая связана с аргументами сообщения: зависимость от порядка следования аргументов.
Такая зависимость приводит к тому, что при каждом изменении порядка аргументов в методе, придется вносить изменения там, где этот метод вызывается.
От этой зависимости легко избавиться с помощью именованных параметров:
# без именованных параметров wayne = Customer('Bruce Wayne', 'brucy@mail.com', 1000) # с именованными параметрами wayne = Customer( name='Bruce Wayne', email='brucy@mail.com', balance=1000)
Кроме того, что именованные параметры устраняют зависимость между порядком следования аргументов, они также делают код читабельнее. До использования именованных параметров не было ясно, что значит число 1000. Чтобы это выяснить нужно смотреть исходный код класса Customer
. После использования, сразу видно, что это баланс клиента.
Когда использовать
Используйте именованные параметры когда метод принимает много аргументов, и при чем есть значительная вероятность изменения порядка этих аргументов.
Изоляция внешних сообщений
У объекта есть зависимость когда он знает имя сообщения, которое он собирается отправить еще кому-то кроме себя самого (кроме self).
В каких-то ситуацией имеет смысл устранить зависимость и от внешних сообщений, отправляемых другому объекту. В методе charge
класса Customer
известно, что объект mailer
должен откликнуться на сообщение send
:
class Customer: ... def charge(self, amount): ... self.mailer.send( to=self.email, from_='admin@example.com', subject='Payment', text=f'Charging {amount}')
В нашем примере, метод send
объекта mailer
используется только в методе charge
. Если метод send
вызывался бы в нескольких местах, то при каждом изменении имени метода send
или его аргументов, пришлось бы делать изменения везде, где метод send
вызывается. Эту зависимость можно частично устранить, если вынести вызов метода send
в собственный метод класса Customer
:
class Customer: ... def send_email(self, subject, text): self.mailer.send( to=self.email, from_='admin@example.com', subject=subject, text=text) def charge(self, amount): ... self.send_email('Payment', f'Charging {amount}')
Заключение
Зависимость между объектами неизбежна. Но сильная зависимость приводит к большим проблемах. Однако есть способы сведения зависимости к минимуму.
Внедрение зависимости – легкий способ избавиться от зависимости от конкретного класса. Этот паттерн делает код гибче и легко тестируемым. Зависимость внедряется через конструктор класса, либо через параметр метода. Если по каким-то причинам не удается внедрить зависимость, то изолируйте ее в конструкторе класса, либо в свойстве/методе.
Для устранения зависимости от порядка следования аргументов используйте именованные параметры. Бонус использования – код становится читабельнее.
В каких-то ситуацией есть смысл устранить зависимость и от внешних сообщений, отправляемых другому объекту. Для этого вынесете вызов сообщения в отдельный метод.
Отличные статьи! Все перечитал. Спасибо большое! Жду новых.