Monday, January 13, 2014

Паттерн "Observer" (Наблюдатель)

Многие из нас интересуются футболом; кто-то предпочитает посмотреть поединок по боксу; ещё кто-то – болеть за любимую хоккейную команду по телевизору или по интернету; также есть люди с предпочтением научным передачам, чтению блогов, просмотру фильмов и т.д. Во всех этих случаях мы выступаем в роли наблюдателей (observers), а объект или субъект, который стоит в поле нашего зрения, является субъектом наблюдения (subject). 
Мы приблизились к теме данной статьи: использование паттерна наблюдатель (observer). Если Вы знакомы с данным паттерном из книги банды четырех, то в этой статье я постараюсь показать, как этот паттерн при умелом его использовании может упростить жизнь в разработке программного обеспечения. Если же Вы впервые слышите об этом паттерне, то можете почерпнуть необходимые знания из книги  банды четырех, которая является классической книгой по проектированию программного обеспечения и, наверное, должна быть у каждого уважающего себя разработчика, делающего постоянные шаги к профессиональному росту. Эта книга имеет некие сложности для понимания новичкам, которые только вкратце знакомы с проектированием программного обеспечения, а то и вовсе впервые услышали этот термин; – для такого случая есть вторая бесподобная книга в своем роде: Elisabeth Freeman, Head First Design Patterns.  Если с английским совсем туго, тогда вместо оригинала можно использовать русскоязычный вариант. Цель данной статьи – преподнести базис для понимания моей следующей статьи о Reactive Extensions (Rx). Итак, рассмотрим, что это за паттерн и "с чем его едят".
Наблюдатель – это паттерн поведения объектов, определяющий зависимость типа "один ко многим" между объектами таким образом, что при изменении состояния одного объекта все зависящие от него оповещаются об этом и автоматически обновляются.
Примечание: описание, приведенное выше, взято с классической книги банды четырех, так как в этом плане считаю ее первоисточником.  
Этот паттерн в мире .NET разработчиков чаще всего известен под именем "Publisher-Subscriber (издатель-подписчик)".
Этот паттерн следует использовать в таких случаях:
  • когда при изменении одного объекта требуется изменить другие объекты, и Вы не знаете их количество;
  • когда нужно уведомлять объекты об изменениях, при этом нам не важно, как эти объекты будут эти изменения использовать.
Рассмотрим пример, когда мы подписываемся на рассылку каких-либо новостей, например, новостей в мире разработки ПО или о новинках кинематографии: 
public interface IObserver
{
    void Update(string news);
}
public interface ISubject
{
    void AttachObserver(IObserver observer);
    void DetachObserver(IObserver observer);
    void Notify();
}
Создадим канал новостей, на который можно будет подписаться для получения новостей.   
public class ChannelNews : ISubject
{
    private List<IObserver> _observers;
    private Queue<string> _news;

    public ChannelNews()
    {
        _observers = new List<IObserver>();
        _news = new Queue<string>();
    }

    #region [ implemented methods ]
    public void AttachObserver(IObserver observer)
    {
        _observers.Add(observer);
    }

    public void DetachObserver(IObserver observer)
    {
        _observers.Remove(observer);
    }

    public void Notify()
    {
           
        while (_news.Count > 0)
        {
            var news = _news.Dequeue();
            foreach (var observer in _observers)
            {
                observer.Update(news);
            }
        }
    }
    #endregion

    //Добавление новости
    public void AddNews(string news)
    {
        _news.Enqueue(news);
    }
}
Вот как будут выглядеть наши подписчики новостей:
public class SubscriberOne : IObserver
{
    public void Update(string news)
    {
        Console.WriteLine("SubscriberOne read news {0}", news);
    }
}
public class SubscriberTwo : IObserver
{
    public void Update(string news)
    {
        Console.WriteLine("SubscriberTwo read news {0}", news);
    }
}
Посмотрим,  как будет выглядеть использование всего этого "добра":
var channel = new ChannelNews();
var subscriber1 = new SubscriberOne();
var subscriber2 = new SubscriberTwo();

channel.AttachObserver(subscriber1);
channel.AttachObserver(subscriber2);
channel.AddNews("ТОП-5 спортсменов, погубивших карьеру наркотиками");
channel.AddNews("В какой стране самое дешевое гражданство 'на продажу'");
channel.Notify();

channel.DetachObserver(subscriber1);
channel.AddNews("Для души. Украинцы делают из автохлама «конфетки»");
channel.Notify();

channel.DetachObserver(subscriber2);
Console.ReadLine();
После запуска Вы увидите, как работает паттерн наблюдатель. Если Вы хотите посмотреть, как не нужно использовать данный паттерн, добро пожаловать в книгу Андрій Будай. "Дизайн-патерни – просто, як двері". В этом источнике можно найти реализацию данного паттерна, чтобы понять, как его не нужно использовать. Рассмотрим, как автор использует объявления интерфейса IObserver:
interface IObserver
{
    void Update(ISubject subject);
}
В этом подходе автор реализует имплементацию данного интерфейса, можно посмотреть в книге. Я покажу, почему так нельзя делать.
Автор в явном виде использует приведение интерфейса к нужному классу. Это одна из дыр в написании программного обеспечения. Мало того, что данный подход нарушает принцип SOLID, причем сразу по трем пунктам, но проблема в том, что благодаря такому подходу мы можем из подписчиков изменять исходный объект. Неужели этого добивался автор? К сожалению, такие ляпы у данного автора встречаются в книге в нескольких местах. Если цель данной книги – научить пользователя и донести ему, как ни в коем случае не нужно делать, то автор точно достиг цели.
Есть несколько способов исправить то решение, которое привел Андрей Будай в своей книге. Один из самых простых способов  это добавить в интерфейс IObserver необходимый параметр, как это сделано в примере, приведенном в данной статье. Её автор нарушил один из пунктов, который чётко описан в книге банды четырехВ реализациях паттерна наблюдатель субъект довольно часто транслирует всем подписчикам дополнительную информацию о параметрах изменения. Она передается в виде аргумента операции Update, и ее объем меняется в широких диапазонах.

На одном полюсе находится так называемая модель проталкивания (push model), когда субъект посылает наблюдателям детальную информацию об изменении независимо от того, нужно ли им это. На другом  модель вытягивания (pull model), когда субъект не посылает ничего, кроме минимального уведомления, а наблюдатели запрашивают детали позднее.

Одна из интересных новостей в плане использования паттерна наблюдатель в том, что компания Microsoft уже добавила поддержку данного паттерна в mscorlib IObserver<T>. А теперь плохая новость. Компания Microsoft разделять паттерны Observer и Publisher-Subscriber. Например, второй паттерн они рассматривают как реализация на event.
class Program
{
    public event EventHandler<CustomEventArgs> CustomEvent;

    static void Main(string[] args)
    {
        var pub = new Publisher();
        var sub1 = new Subscriber("sub1", pub);
        var sub2 = new Subscriber("sub2", pub);

        // Call the method that raises the event.
        pub.DoSomething();

        // Keep the console window open
        Console.WriteLine("Press Enter to close this window.");
        Console.ReadKey();
    }
}

// Class that publishes an event
class Publisher
{
    // Declare the event using EventHandler<T>
    public event EventHandler<CustomEventArgs> RaiseCustomEvent;

    public void DoSomething()
    {
        // Write some code that does something useful here
        // then raise the event. You can also raise an event
        // before you execute a block of code.
        OnRaiseCustomEvent(new CustomEventArgs("Did something"));

    }

    // Wrap event invocations inside a protected virtual method
    // to allow derived classes to override the event invocation behavior
    protected virtual void OnRaiseCustomEvent(CustomEventArgs e)
    {
        // Make a temporary copy of the event to avoid possibility of
        // a race condition if the last subscriber unsubscribes
        // immediately after the null check and before the event is raised.
        EventHandler<CustomEventArgs> handler = RaiseCustomEvent;

        // Event will be null if there are no subscribers
        if (handler != null)
        {
            // Format the string to send inside the CustomEventArgs parameter
            e.Message += String.Format(" at {0}", DateTime.Now.ToString());

            // Use the () operator to raise the event.
            handler(this, e);
        }
    }
}

//Class that subscribes to an event
class Subscriber
{
    private string id;
    public Subscriber(string ID, Publisher pub)
    {
        id = ID;
        // Subscribe to the event using C# 2.0 syntax
        pub.RaiseCustomEvent += HandleCustomEvent;
    }

    // Define what actions to take when the event is raised.
    void HandleCustomEvent(object sender, CustomEventArgs e)
    {
        Console.WriteLine(id + " received this message: {0}", e.Message);
    }
}

// Define a class to hold custom event info
public class CustomEventArgs : EventArgs
{
    public CustomEventArgs(string s)
    {
        message = s;
    }
    private string message;

    public string Message
    {
        get { return message; }
        set { message = value; }
    }
}
Данный подход имеет проблему в том, что на одно событие класса Publisher подписываются разные подписчики. Если один из подписчиков сгенерирует exception, то все остальные подписчики, которые находятся после него, не получат уведомление, которое они ожидали. Решение данной проблемы основано на использовании метода Delegate.GetInvocationList, который возвращает массив делегатов, представляющих список вызовов текущего делегата. Поскольку событие (event) – это обертка над MulticastDelegate, благодаря ключевому слову event, у события имеются некоторые дополнительные возможности. Посмотрите, как будет выглядеть исправление.
class Program
{
    public event EventHandler<CustomEventArgs> CustomEvent;

    static void Main(string[] args)
    {
        var pub = new Publisher();
        var sub1 = new Subscriber("sub1", pub);
        var sub2 = new Subscriber("sub2", pub);
        var sub3 = new Subscriber("sub3", pub);
        var sub4 = new Subscriber("sub4", pub);

        try
        {
            // Call the method that raises the event.
            pub.DoSomething();
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex);
        }
       

        // Keep the console window open
        Console.WriteLine("Press Enter to close this window.");
        Console.ReadKey();
    }
}

// Class that publishes an event
class Publisher
{
    // Declare the event using EventHandler<T>
    public event EventHandler<CustomEventArgs> RaiseCustomEvent = delegate {  };

    public void DoSomething()
    {
        // Write some code that does something useful here
        // then raise the event. You can also raise an event
        // before you execute a block of code.
        OnRaiseCustomEvent(new CustomEventArgs("Did something"));

    }

    // Wrap event invocations inside a protected virtual method
    // to allow derived classes to override the event invocation behavior
    protected virtual void OnRaiseCustomEvent(CustomEventArgs e)
    {
        // Format the string to send inside the CustomEventArgs parameter
        e.Message += String.Format(" at {0}", DateTime.Now.ToString());

        var exceptions = new List<Exception>();
        foreach (var handler in RaiseCustomEvent.GetInvocationList())
        {
            try
            {
                handler.DynamicInvoke(this, e);
            }
            catch (Exception ex)
            {
                exceptions.Add(ex);
            }
        }
        if (exceptions.Any())
        {
            throw new AggregateException(exceptions);
        }
    }
}

//Class that subscribes to an event
class Subscriber
{
    private string id;
    public Subscriber(string ID, Publisher pub)
    {
        id = ID;
        // Subscribe to the event using C# 2.0 syntax
        pub.RaiseCustomEvent += HandleCustomEvent;
    }

    // Define what actions to take when the event is raised.
    void HandleCustomEvent(object sender, CustomEventArgs e)
    {
        if (id == "sub2")
            throw new ArgumentException("Throw publisher exception");
        Console.WriteLine(id + " received this message: {0}", e.Message);
    }
}

// Define a class to hold custom event info
public class CustomEventArgs : EventArgs
{
    public CustomEventArgs(string s)
    {
        message = s;
    }
    private string message;

    public string Message
    {
        get { return message; }
        set { message = value; }
    }
}
Мне очень не нравится это решение, но для событий другого решения нет. Но тут на помощь приходит паттерн наблюдатель, который мы рассматриваем в данной статье. Представим себе ситуацию, что один из подписчиков в примере, приведённом в начале главы, пробросил исключение. Что тогда нужно делать? Решение с использованием данного паттерна намного элегантнее того кода, который нужно написать при использовании механизма событий. 
В первую очередь добавим бросание ошибки для второго подписчика:
public class SubscriberTwo : IObserver
{
    public void Update(string news)
    {
        throw new ArgumentException("Throw publisher2 exception");
    }
}
А у нашего издателя, которым выступает новостной канал, лишь незначительно изменилась реализация интерфейса Notify:
public void Notify()
{
           
    while (_news.Count > 0)
    {
        var news = _news.Dequeue();
        foreach (var observer in _observers)
        {
            try
            {
                observer.Update(news);
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex);
            }
        }
    }
}
Так мы себя обезопасили от зловредного пользователя.
После того, как мы рассмотрели использование классического паттерна и проблемы, решаемые с его помощью, посмотрим реализацию с использованием интерфейса IObserver<T>, которое нам предлагает компания Майкрософт.
public class ChannelNews : IObservable<string>
{
    public ChannelNews()
    {
        observers = new List<IObserver<string>>();
    }

    private List<IObserver<string>> observers;

    public IDisposable Subscribe(IObserver<string> observer)
    {
        if (!observers.Contains(observer))
            observers.Add(observer);
        return new Unsubscriber(observers, observer);
    }

    private class Unsubscriber : IDisposable
    {
        private readonly List<IObserver<string>> _observers;
        private readonly IObserver<string> _observer;

        public Unsubscriber(List<IObserver<string>> observers, IObserver<string> observer)
        {
            _observers = observers;
            _observer = observer;
        }

        public void Dispose()
        {
            if (_observer != null && _observers.Contains(_observer))
                _observers.Remove(_observer);
        }
    }

    public void AddNews(string news)
    {
        foreach (var observer in observers)
        {
            if(!string.IsNullOrEmpty(news))
                observer.OnNext(news);
            else
                observer.OnError(new ArgumentNullException("News is null"));
        }
    }

    public void EndNews()
    {
        foreach (var observer in observers.ToArray())
            if (observers.Contains(observer))
                observer.OnCompleted();

        observers.Clear();
    }
}

public class Subscriber : IObserver<string>
{
    public string Name { get; private set; }

    public Subscriber(string name)
    {
        Name = name;
    }

    public void OnNext(string value)
    {
        Console.WriteLine("Subscriber {0} read news {1}", Name, value);
    }

    public void OnError(Exception error)
    {
        Console.WriteLine("{0}: News error {1}.", Name, error.Message);
    }

    public void OnCompleted()
    {
        Console.WriteLine("The news has completed {0}.", Name);
    }
}
Интерфейс для реализации данного паттерна, предложенный компанией Microsoft, немного отличается от привычного нам классического паттерна наблюдатель. Но отличие незначительное и, как по мне, очень неплохая реализация для разделения логики.  

Итоги
В данной статье рассмотрены примеры использования паттерна наблюдатель, а также приведён пример, как он может упростить нам жизнь при умелом его использовании. Также в данной статье я позволил себе немного раскритиковать решение, предложенное автором книги "Дизайн-патерни – просто, як двері", так как данное решение нарушает принципы SOLID, как и здравой логики, и использования такого подхода, как предлагал автор данной книги, лучше по возможности избегать. Эта статья является вводной для одной из моих следующих статей по Reactive Extensions (Rx), так как Rx полностью основывается на работе данного паттерна. Надеюсь, данная статья поможет внедрять использование данного паттерна в Ваши проекты, потому что в некоторых случаях решение на данном паттерне намного удобнее, чем аналогичное решение которое возникает у большинства разработчиков .NET и основывается на механизме событий.   

Источники:

No comments:

Post a Comment