Паттерны в чистом виде встречаются довольно редко и при изучении паттернов, особенно на ранних этапах, важны не столько сами паттерны, сколько понимание механизмов (тактик), с помощью которых они реализуются. В этой статье я хотел бы описать один из таких механизмов (управление зависимостями), который используется в паттернах Observer и Mediator, но который часто упускается из внимания. Если ты только начинаешь изучать паттерны, то добро пожаловать под кат.
Начнём с утверждения: если класс A зависит от класса B, то, не меняя поведения, можно переписать код так, что класс B будет зависеть от класса A или ввести ещё один класс C таким образом, что классы A и B будут независимы, а класс C будет связывать и зависеть от классов A и B.
Один класс зависит от другого, если он обращается к его полям или методам. Поля легко переносятся из одного класса в другой, поэтому остановимся подробнее на методах. Допустим, класс B содержит метод Print, который выводит текущую дату в консоль, а класс А вызывает этот метод.
Таким образом класс A зависит от класса B. Чтобы вывернуть эту зависимость, вместо вызова метода Print напрямую, сгенерируем событие. Теперь класс А ничего не знает о классе B, а класс B может подписаться на событие класса A. Т.е. класс B будет зависеть от класса A.
Поведение кода не меняется, а в вызывающем методе меняется только порядок инициализации объектов и передача зависимостей в конструктор.
По сути, это и есть реализация паттерна Observer в C#. Класс A — это обозреваемый объект (Observable), а класс B это обозреватель (Observer). Класс A является независимым классом, который генерирует уведомления (события). Другие классы, которые в этом заинтересованы, могут подписываться на эти события и выполнять свою логику. Система становится более динамичной за счёт того, что теперь классу A не нужно знать о других реализациях. Мы можем добавлять новые реализации, которые будут подписываться на события, при этом класс A будет оставаться неизменным.
Можно пойти дальше и убрать зависимость класса B от A, добавив внешний код, который будет связывать эти два класса, т.е. подписывать один объект на события другого.
Теперь классы A и B полностью независимы, каждый выполняет свою задачу и ничего не знает о других классах. Логика по взаимодействию между объектами уходит в новый класс. Только класс C знает в ответ на какие события и при каких условиях должны вызываться методы класса B. Таким образом, класс C становится медиатором.
Одной из важных проблем в программировании является наличие сложных систем из запутанных классов с большим количеством зависимостей (сильносвязанные системы). Управляя зависимостями, можно уменьшить связанность, упростить систему и достичь большей динамичности и гибкости.
Паттерн 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 является наличие циклических ссылок, когда события из одного класса, проходя по цепочки объектов, приводят к генерированию этого же события ещё раз. Эта проблема тяжело разрешима и заметно усложняет использование паттернов.
Таким образом, в разных обстоятельствах, управляя зависимостями, можно прийти к разным паттернам, иногда к их смеси, а иногда этот механизм окажется полезен без использования паттернов вовсе. Боритесь со сложностью и не плодите сущностей.