Паттерны проектирования в .NET за 5 минут — Observer(Наблюдатель)

Привет. Это очередной урок из курса "Паттерны за 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

Приятного программирования.

Комментарии (1) -

Александр 14.02.2018 7:50:48

Здравствуйте! Спасибо за доходчивое объяснение!

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