Преимущества диспетчера сообщений
У Вас большой и успешный проект, который постоянно растет и развивается. Но внешний вид и удобство использования никак не покажут из чего состоит такой нужный продукт. И только разработчики понимают всю глубину «Марианской впадины». Что я этим хочу сказать? Давайте по порядку.
Из чего состоит программа, написанная с использованием ООП? Конечно из классов. Каждый класс отвечает за свой функционал (в идеале), который он должен реализовывать. И естественно классы должны взаимодействовать друг с другом, обмениваясь данными или используя функционал другого класса. И вот это взаимодействие является архитектурой приложения.
Казалось бы, все понимают эти элементарные и базовые вещи. Но вот построить хорошую архитектуру на практике получается не просто. А идеальных архитектур вообще не бывает, по крайней мере я еще не встречал.
Как понять хорошая архитектура или нет? Все просто. Если Вы в проект добавляете новый функционал без особых проблем и Вам не приходится менять логику взаимодействия классов – архитектура хорошая. И наоборот. Чем больше Вы будете вносить изменений во взаимодействие классов, тем архитектура хуже.
Что в итоге получается? Если архитектура плохая, то разработчику приходится делать больше работы, реализуя не только новый функционал, но правя каркас приложения. Как следствие, дополнительные ошибки, отладка изменений и много других нехороших моментов.
Так как же сделать хорошую архитектуру? Универсальным инструментом для этого является опыт и знания. Первое зависит только от Вас, а вот небольшую часть второго я постараюсь рассказать ниже, основываясь на своем опыте.
У нас в компании на проекте используется диспетчер сообщений. И используя самые элементарные принципы: один класс – один функционал, один метод – одно действие, плюс диспетчер сообщений, мы можем, грубо говоря, без особых усилий добавлять новый функционал и расширять проект. Естественно бывают исключения, поскольку проект не маленький. Однако использование диспетчера сообщений на столько упрощают архитектуру, что этих моментов меньше на порядки. Вот о диспетчере сообщений я и хочу рассказать.
Что такое диспетчер сообщений?
Сейчас опишу вещи, которые должны знать большинство программистов. В принципе в самом названии уже есть ответ на данный вопрос. Это диспетчер, который распределяет сообщения. Данный компонент построен на паттерне Посредник. Задача этого посредника передать сообщение от отправителя получателю. Как на словах, так и на схеме все выглядит очень просто.
Ну и возникает резонный вопрос, как это может упростить архитектуру? Все просто. При использовании диспетчера сообщений классам не нужно знать друг о друге. Классам нужно лишь знать о самом диспетчере и о сообщении, с которым эти классы могут работать. Это позволяет упростить взаимодействие классов между собой и как следствие, упростить архитектуру приложения. Попробую объяснить подробней.
Жирный плюс диспетчера сообщений.
Основной плюс диспетчера сообщений в его назначении, т.е. в том для чего его придумали. Как писал ранее основная его задача убрать зависимости объектов друг от друга. Используя диспетчер сообщений нам не нужны ссылки на получателя в отправителе. Все что должны знать отправитель и получатель - это определенный формат сообщения.
Так что конкретно мы можем улучшить с помощью диспетчера сообщений? Самое первое что приходит на ум – это события. Допустим у нас есть такое событие:
public class A
{
public Action<object> OnDoIt;
}
И мы на него подписываемся.
public class B
{
public void Init(A a)
{
a.OnDoIt += DoIt;
}
}
И нам нужно сразу написать вот это:
public class B
{
...
public void Deinit()
{
a.OnDoIt -= DoIt
}
}
Как все Вы, надеюсь, знаете, если не сделать отписку от события, то подписанный объект не удалиться из памяти. Последствия также, подозреваю, понимаете. И если Вы думаете, что это ерунда и не проблема, то скажу так. Я много раз встречал ситуацию, когда забывали делать отписку и более того, подписку копипастили и забывали исправлять плюс на минус. Результат получался веселый.
Так чем здесь может помочь диспетчер сообщений? Первое, классу B не нужно знать о классе A. Следовательно, мы не привяжем один к другому и если класс A станет не нужен, то сборщик мусора его удалит без проблем. Все что нам нужно – это договориться о формате сообщения, которое класс A может отправить, а класс B получить и обработать.
И тут возникает вопрос, как диспетчер сообщений узнает, что класс A отправляет сообщение классу B? Ну на то он и диспетчер чтобы знать. Описывать внутренности диспетчера сообщений здесь не буду. Это тема отдельной статьи. Однако упрощенная схема работы выглядит так.
У класса A должна быть ссылка на диспетчер сообщений. А класс B должен подписаться в диспетчере на нужное ему сообщение. И когда класс A отправляет сообщение в диспетчер, то диспетчер переправляет это сообщение в класс B, который подписан на это сообщение. И тут Вы можете сказать: «Так, а в чем разница? И там, и там нужна ссылка. И там, и там нужно подписаться. И классы будут зависимы от диспетчера сообщений.» Как бы логично, но есть нюанс.
Если грамотно реализовать диспетчер сообщений, то сборщик мусора может без проблем убрать подписанный класс, когда он будет уже не нужен. Даже если мы забудем отписать класс. Следующее, классам нужна всего одна ссылка, это ссылка на диспетчер сообщений. Классам знать друг о друге не нужно.
Давайте сравним. Допустим у нас есть три класса A, B, C. Классы B и C должны реагировать на событие в классе A. А класс C реагировать на событие в классе B.
public class A
{
public Action<object> EventInA;
}
public class B
{
public Action<object> EventInB;
public void Init(A a)
{
a.EventInA += ProcessA;
}
public void Deinit()
{
a.EventInA -= ProcessA;
}
private void ProcessA(object messageA)
{
...
}
}
public class C
{
public void Init(A a, B b)
{
a.EventInA += ProcessA;
b.EventInB += ProcessB;
}
public void Deinit()
{
a.EventInA -= ProcessA;
b.EventInB -= ProcessB;
}
private void ProcessA(object messageA)
{
...
}
private void ProcessB(object messageB)
{
...
}
}
Как Вы видите, как и ранее чтобы обработать события класса нам нужна ссылка на объект этого класса. И для класса C нам нужны A и B. А если таких классов пара десятков. Думаете не реально? Вот Вам простой пример.
Программа движения автомобиля. Когда едет автомобиль, то происходят разные события: поворот налево, нажата педаль газа, включена пятая скорость, нажат тормоз и т.п. Как думаете сколько событий будет при движении автомобиля? Явно больше двух десятков. И есть класс, который реагирует на все эти события, обрабатывает и записывает их в базу. Получается он должен подписаться на все эти события. И как будет выглядеть эта подписка?
Теперь посмотрим на пример с диспетчером сообщений.
public class MessageDispatcher
{
}
public class A
{
public void Init(MessageDispatcher md)
{
}
public void ProcessA()
{
md.SendMessage(messageA);
}
}
public class B : IMessageClient
{
public void Init(MessageDispatcher md)
{
md.Subscribe(MessageA, this);
}
public void ProcessMessage(IMessage msg)
{
// Обработка сообщений
}
public void ProcessB()
{
md.SendMessage(messageB);
}
}
public class C : IMessageClient
{
public void Init(MessageDispatcher md)
{
md.Subscribe(MessageA, this);
md.Subscribe(MessageB, this);
}
public void ProcessMessage(IMessage msg)
{
// Обработка сообщений
}
}
Красота? Все что нам нужно – это ссылка на диспетчер сообщений. Мы можем подписываться на сотни сообщений от других классов не зная о них ничего.
Давайте рассмотрим еще один пример, где может быть полезен диспетчер сообщений. Есть класс A, в котором есть класс B, выполняющий обработку определенных данных. И функционал этой обработки нужно вызывать в классе C. Как это будет выглядеть в классическом варианте. Нам нужно будет либо делегировать вызов метода обработки класса B из класса A, либо вернуть ссылку на класс B. Возьмем первый вариант.
public class A
{
private B b;
public void Init()
{
b = new B();
}
public void ProcessB(MessageC messageC)
{
b.ProcessB(messageC)
}
}
public class B
{
public void ProcessB(MessageC)
{
// Обрабатываем данные
}
}
public class C
{
public void Init(A a)
{
}
public void ProcessC()
{
a.ProcessB(messageC);
}
}
Как видите нам снова нужны ссылки на классы реализующий нужный функционал. Плюс дополнительный код по делегированию. А если эти классы в разных библиотеках и они не должны знать друг о друге? Тогда нам придется создать интерфейсы. Еще дополнительный код.
А как же это будет выглядеть с диспетчером сообщений.
public class MessageDispatcher
{
}
public class A
{
private B b;
public void Init()
{
b = new B();
}
}
public class B : IMessageClient
{
public void Init(MessageDispatcher md)
{
md.Subscribe(MessageC, this);
}
public void ProcessMessage(IMessage msg)
{
// Обработка сообщения
}
}
public class C
{
public void Init(MessageDispatcher md)
{
}
public void ProcessC()
{
md.SendMessage(messageC);
}
}
И снова красота. Классы не зависят друг от друга. Они вообще не знают друг о друге.
И что же мы в итоге имеем?
Я привел маленькие примеры как можно стандартные реализации заменить паттерном Посредник, который реализует диспетчер сообщений.
Как я писал вначале, у нас в компании на проекте используется диспетчер сообщений. В проекте используется плагинная система и есть функционал перестарта плагинов. Грубо говоря, при перестарте все плагины должны удаляться и создаваться снова. И поскольку у нас классы плагинов не привязываются к другим классам событиями и сильными ссылками, то они удаляются сборщиком мусора без проблем. Что не приводит к утечкам памяти. Это было нами установлено неоднократными тестами. И вот этот момент позволяет дальнейшую разработку и поддержание проекта без оглядки на проблему потери памяти.
Если брать конкретно наш диспетчер сообщений, то он дает дополнительную гибкость в обработке сообщений. Например, мы можем обрабатывать сообщения по очереди асинхронно в отдельном потоке или синхронно в текущем потоке. Также мы можем выставить приоритеты обработчикам сообщений, т.е. указать кто из обработчиков будет обрабатывать данное сообщение первым, кто вторым и т.д. Можем после обработки сообщения прервать его дальнейшую обработку , чтобы не тратить время на вызовы остальных обработчиков, зарегистрировнных для обработки этого сообщения. Еще реализована возможность возврата результатов обработки сообщения.
Проще говоря Вы можете себе создать диспетчер сообщений с нужным Вам функционалом, который будет удобней и гибче обычных событий.
Что касается меня лично, то использование диспетчера сообщений мне приносит своего рода удовольствие. С ним многое упрощается, в том числе трудозатраты.
Минусы?
У любой медали две стороны. Во первых, чтобы использовать диспетчер сообщений, нужно настроить мозги на эту логику. Для кого-то может это показаться трудным, но когда в голове сформируется схема работы диспетчера этот минус сразу превратиться в плюс. Вы сразу осознаете простоту и легкость. Т.е. это минус, скажем так, для новичков.
Так же довольно часто возникает необходимость лишнего кода для дополнительной обработки сообщений.
Что еще? При отладке не получиться идти построчно и отследить отсылку и получение сообщения простым способом. Дебаг в лоб не получится.
Возможно есть и другие минусы, которые я забыл, но скажу так. Для меня плюсы затмевают все минусы использования диспетчера сообщений. Если у Вас другая ситуация, значит Вашему проекту не подходит этот паттерн и нужно использовать что-то другое.
Зачем все это писал?
Все просто. Хотел поделиться своим опытом от использования диспетчера сообщений. Возможно кто-то откроет для себя диспетчер сообщений или посмотрит на него с другой стороны и также получит удовольствие в разработке, применяя эту полезную штуку.
Всем добра и удачи.