Привет. Это очередной урок из курса "Паттерны за 5 минут" и в данном уроке мы поговорим о паттерне "Наблюдатель".
Наблюдатель — поведенческий шаблон проектирования. Определяет зависимость типа "один ко многим" таким образом, что при изменении объекта, все зависящие от его получают сообщение об этом событии
Говоря проще, у нас есть поставщик событий, и подписчики на эти события. Когда происходит новое событие, все подписчики об этом оповещаются.
В .NET наиболее распространены 3 способа реализации данного паттерна. Через делегаты, данный способ гарантирует наличие наблюдателя и отлично подходит когда нужно реализовать отношение один поставщик один наблюдатель. Также при таком подходе вы можете получить результат в ответ от подписчика.
Вторым способом будет классическая реализация через события. При таком подходе может быть любое число подписчиков, реализация очень проста. Но в то же время наличие подписчиков НЕ обязательно и получение ответа от подписчика при такой реализации так же ожидать не стоит.
Также паттерн "Наблюдатель" можно реализовать через набор интерфейсов IObserver/IObservable. Реализация через данные интерфейсы может быть использована в реактивных расширениях, которые позволят работать с данными через LINQ синтаксис.
В качестве практической задачи рассмотрим задачу отслеживания удаления файлов в указанном каталоге. При удалении будем оповещать всех подписчиков данного события. Сперва реализуем класс, который позволит отслеживать удалённые в каталоге файлы. Добавим новый класс DirMonitoring. В нем, в конструкторе будем получать путь к отслеживаемой директории. При запуске метода StartMonitor будем получать список имеющихся файлов на данный момент. При вызове метода DeletedFiles будем искать в каталоге файлы из полученного ранее списка. Если какой-то из них отсутствует, будем добавлять его в список удалённых файлов для результата метода.
public class DirMonitoring
{
private List<string> _files;
private readonly string _directory;
public DirMonitoring(string directory)
{
_directory = directory;
}
public bool StartMonitor()
{
if (!Directory.Exists(_directory))
{
return false;
}
_files = new List<string>();
var directoryInfo = new DirectoryInfo(_directory);
foreach (var fileInfo in directoryInfo.GetFiles())
{
_files.Add(fileInfo.FullName);
}
return true;
}
public List<string> DeletedFiles()
{
var result = new List<string>();
foreach (var file in _files.ToArray())
{
if (!File.Exists(file))
{
_files.Remove(file);
result.Add(file);
}
}
return result;
}
}
Также добавим класс, в котором реализуем вывод данных подписчиков на оповещение об удалении. Добавим класс Subscriber. В конструкторе будем передавать название для подписчика. В метод ItIsSubscriber будем передавать имя удалённого файла и выводить в консоль надпись, что данный файл был удалён. Вариант ниже представляет из себя готовый вариант, который использовался в примере:
public class Subscriber
{
private readonly string _name;
public Subscriber(string name)
{
_name = name;
}
public void ItIsSubscriber(string fileName)
{
Console.WriteLine($"{_name} {fileName} was deleted!");
}
public void ItIsSubscriber(object sender, string fileName)
{
Console.WriteLine($"{_name} {fileName} was deleted!");
}
public void ItIsSecondSubscriber(object sender, string fileName)
{
Console.WriteLine("---");
Console.WriteLine($"{_name} {fileName} was deleted!");
Console.WriteLine("---");
}
}
Теперь добавим вариант реализации через делегаты. Создадим класс FileStatusDelegate. Такое название класса для того, чтобы в дальнейшем мы отличали наши реализации, понятно, что для реальных задач подобные имена использовать не стоит. В классе добавлю делегат _subscriber, который принимает на вход строку и ничего не возвращает. Добавлю таймер, который позволит вызывать проверку удаления файлов через определённые промежутки времени. А также переменную _dirMonitoring, которая будет отвечать за отслеживание за удалением. В конструктор данного класса будем передавать путь к директории и делегат с методом подписчика. Зададим в _timer вызов метода для проверки наличия удалённых файлов каждую секунду. Также проверим переданную директорию на наличие. Добавим метод CheckRemoval, в нём будем получать список удалённых файлов и передавать его нашему подписчику.
class FileStatusDelegate: IDisposable
{
private readonly Action<string> _subscriber;
private readonly Timer _timer;
private readonly DirMonitoring _dirMonitoring;
public FileStatusDelegate(string directory, Action<string> subscriber)
{
_subscriber = subscriber;
_dirMonitoring = new DirMonitoring(directory);
if (_dirMonitoring.StartMonitor())
{
_timer = new Timer(1000);
_timer.Elapsed += CheckRemoval;
_timer.Start();
}
else
{
Console.WriteLine("Specified direcory does not exist");
Dispose();
}
}
private void CheckRemoval(Object source, ElapsedEventArgs e)
{
foreach (var fileName in _dirMonitoring.DeletedFiles())
{
_subscriber(fileName);
}
}
public void Dispose()
{
_timer.Dispose();
}
Перейдём к классической реализации через события. Так как реализации через события очень похожа на реализацию через делегаты, она будет представлять из себя следующее:
class FileStatusEvent : IDisposable
{
public EventHandler<string> RemoveFiles;
private readonly Timer _timer;
private readonly DirMonitoring _dirMonitoring;
public FileStatusEvent(string directory)
{
_dirMonitoring = new DirMonitoring(directory);
if (_dirMonitoring.StartMonitor())
{
_timer = new Timer(1000);
_timer.Elapsed += CheckRemoval;
_timer.Start();
}
else
{
Console.WriteLine("Specified direcory does not exist");
Dispose();
}
}
private void CheckRemoval(Object source, ElapsedEventArgs e)
{
foreach (var fileName in _dirMonitoring.DeletedFiles())
{
var handler = RemoveFiles;
handler?.Invoke(this, fileName);
}
}
public void Dispose()
{
_timer.Dispose();
}
}
Займемся вариантом реализации через интерфейсы IOservable/IObserver
Добавим класс Observer, наследуем его от IObserver<string> данный класс будет выступать в качестве наблюдателя/подписчики на события. Имплементируем все необходимые методы, добавляем конструктор, в котором передаём IObservable<string> переменную и если она не null, то вызываем метод Subscribe для данного класса и результат сохраняем в перменную _unsubscriber. В методе OnNext будем выводить результат. Метод OnError отвечает за обработку ошибок и OnConpleted отрабатывается тогда, когда необходимо завершить подписку, так как оповещений больше не будет.
class Observer: IObserver<string>
{
private IDisposable _unsubscriber;
public virtual void Subscribe(IObservable<string> provider)
{
if (provider != null)
{
_unsubscriber = provider.Subscribe(this);
}
}
public void OnNext(string value)
{
Console.WriteLine($"{value} file was deleted!");
}
public void OnError(Exception error)
{
Console.WriteLine("An error was occured: " + error.Message);
}
public void OnCompleted()
{
Unsubscribe();
}
public virtual void Unsubscribe()
{
_unsubscriber.Dispose();
}
}
Добавим класс FileStatusEventObservable наследуем его от IObservable<string> именно данный класс будет выступать в качестве поставщика сообщений. Добавим переменную _observers типа List<IObserver<string>> в ней будем хранить подписчиков. В конструкторе инициализируем данный список. В методе CheckRemoval будем проходить по списку имеющихся подписчиков и вызывать для них метод OnNext, передавая имя удалённого файла. Также необходимо добавить метод Subscribe который принимает нового подписчика и возвращает значения типа IDisposable, это позволит корректно завершать работу с подписчиками.
class FileStatusEventObservable : IObservable<string>, IDisposable
{
private readonly List<IObserver<string>> _observers;
private readonly Timer _timer;
private readonly DirMonitoring _dirMonitoring;
public FileStatusEventObservable(string directory)
{
_observers = new List<IObserver<string>>();
_dirMonitoring = new DirMonitoring(directory);
if (_dirMonitoring.StartMonitor())
{
_timer = new Timer(1000);
_timer.Elapsed += CheckRemoval;
_timer.Start();
}
else
{
Console.WriteLine("Specified direcory does not exist");
Dispose();
}
}
private void CheckRemoval(Object source, ElapsedEventArgs e)
{
foreach (var fileName in _dirMonitoring.DeletedFiles())
{
foreach (var observer in _observers)
{
observer.OnNext(fileName);
}
}
}
public void Dispose()
{
_timer.Dispose();
}
public IDisposable Subscribe(IObserver<string> observer)
{
if (!_observers.Contains(observer))
{
_observers.Add(observer);
}
return new Unsubscriber(_observers, observer);
}
}
Подводя итоги, остаётся добавить, что в .NET можно увидеть огромное количество примеров реализации данного паттерна, ведь классы содержащие события, по сути являются паттерном наблюдатель. Столь широкое распространение данный паттерн получил из-за того, что он предоставляет легко реализуемый универсальный механизм уменьшения связанности в приложениях.
Исходный код данного урока вы можете увидеть репозитории https://github.com/flash2048/Patterns_2018/tree/master/Observer
Приятного программирования.