Управление зависимостями, события и паттерны Observer и Mediator

от автора

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

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

Начнём с утверждения: если класс A зависит от класса B, то, не меняя поведения, можно переписать код так, что класс B будет зависеть от класса A или ввести ещё один класс C таким образом, что классы A и B будут независимы, а класс C будет связывать и зависеть от классов A и B.

Один класс зависит от другого, если он обращается к его полям или методам. Поля легко переносятся из одного класса в другой, поэтому остановимся подробнее на методах. Допустим, класс B содержит метод Print, который выводит текущую дату в консоль, а класс А вызывает этот метод.

class A {     private readonly B _b;      public A(B b)     {         _b = b;     }      public void Run()     {         _b.Print();     } }  class B {     public void Print()     {         Console.WriteLine(DateTime.Now.ToString());     } }  public void Test() {     var b = new B();     var a = new A(b);     a.Run(); }

Таким образом класс A зависит от класса B. Чтобы вывернуть эту зависимость, вместо вызова метода Print напрямую, сгенерируем событие. Теперь класс А ничего не знает о классе B, а класс B может подписаться на событие класса A. Т.е. класс B будет зависеть от класса A.

 class A {     public event EventHandler PrintRequested;      public void Run()     {         PrintRequested.Invoke(this, EventArgs.Empty);     } }  class B {     private readonly A _a;      public B(A a)     {         _a = a;         _a.PrintRequested += (s, e) => Print();     }      public void Print()     {         Console.WriteLine(DateTime.Now.ToString());     } }  public void Test() {     var a = new A();     var b = new B(a);     a.Run(); }

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

По сути, это и есть реализация паттерна Observer в C#. Класс A — это обозреваемый объект (Observable), а класс B это обозреватель (Observer). Класс A является независимым классом, который генерирует уведомления (события). Другие классы, которые в этом заинтересованы, могут подписываться на эти события и выполнять свою логику. Система становится более динамичной за счёт того, что теперь классу A не нужно знать о других реализациях. Мы можем добавлять новые реализации, которые будут подписываться на события, при этом класс A будет оставаться неизменным.

Можно пойти дальше и убрать зависимость класса B от A, добавив внешний код, который будет связывать эти два класса, т.е. подписывать один объект на события другого.

 class A {     public event EventHandler PrintRequested;      public void Run()     {         PrintRequested.Invoke(this, EventArgs.Empty);     } }  class B {     public void Print()     {         Console.WriteLine(DateTime.Now.ToString());     } }  class C {     public void Test()     {         var a = new A();         var b = new B();         a.PrintRequested += (s, e) => b.Print();         a.Run();     } }

Теперь классы A и B полностью независимы, каждый выполняет свою задачу и ничего не знает о других классах. Логика по взаимодействию между объектами уходит в новый класс. Только класс C знает в ответ на какие события и при каких условиях должны вызываться методы класса B. Таким образом, класс C становится медиатором.

Итоги: Борьба со сложностью системы

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

Паттерн Observer уменьшает связанность за счёт обращения зависимостей. Он хорошо применим, когда есть несколько источников событий и много слушателей, которые добавляются динамически. Другим хорошим примером использования этого паттерна является реактивное программирование, когда изменение состояния одного объекта приводит к изменению состояния всех зависимых от него объектов и так далее.

Паттерн Mediator уменьшает связанность системы за счёт того, что все зависимости уходят в один класс медиатор, а все остальные классы становятся независимы и отвечают только за логику, которую они выполняют. Таким образом, добавление новых классов становится проще, но с каждым новым классом логика медиатора сильно усложняется. С течением времени, если медиатор продолжает бесконтрольно разрастаться, то его становится очень тяжело поддерживать.

Опасным подводным камнем при использовании паттернов Observer и Mediator является наличие циклических ссылок, когда события из одного класса, проходя по цепочки объектов, приводят к генерированию этого же события ещё раз. Эта проблема тяжело разрешима и заметно усложняет использование паттернов.

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


ссылка на оригинал статьи https://habr.com/post/420783/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *