Почему шаблон Observer намного сложнее в C#, чем в Ruby?

Я прочитал в "Design Patterns in Ruby" Russ Olsen, как шаблон Observer может быть реализован в Ruby. Я заметил, что реализация этого шаблона на Ruby намного проще, чем реализация на C#, например, реализация, показанная в "Программирование.NET 3.5" Джесси Либерти и Алексом Хоровицем.

Поэтому я переписал пример шаблона Observer "Programming .NET 3.5" (стр. 251 из pdf-издания) с использованием алгоритма "Design Patterns in Ruby", исходный код для обеих реализаций можно загрузить с упомянутых веб-сайтов.

Ниже приведен переписанный пример, скажите мне, что вы думаете?
Действительно ли нам нужно использовать события и делегаты, чтобы использовать шаблон Observer в C#?


Обновление После прочтения комментариев я хотел бы задать этот вопрос:
Есть ли другая причина использовать делегаты и события, кроме того, что это делает код короче? И я не говорю о программировании GUI.

Update2 Я наконец-то понял, что делегат - это просто указатель на функцию, а событие - более безопасная версия делегата, которая допускает только две операции += и -=.

Мой переписать пример "Программирование.NET 3.5":

using System;
using System.Collections.Generic;

namespace MyObserverPattern
{
    class Program
    {
        static void Main()
        {
            DateTime now = DateTime.Now;

            // Create new flights with a departure time and add from and to destinations
            CarrierSchedule jetBlue = new CarrierSchedule("JetBlue", now);
            jetBlue.Attach(new AirTrafficControl("Boston"));
            jetBlue.Attach(new AirTrafficControl("Seattle"));

            // ATCs will be notified of delays in departure time
            jetBlue.DepartureDateTime =
                now.AddHours(1.25); // weather delay

            jetBlue.DepartureDateTime =
                now.AddHours(1.75); // weather got worse

            jetBlue.DepartureDateTime =
                now.AddHours(0.5);  // security delay

            jetBlue.DepartureDateTime =
                now.AddHours(0.75); // Seattle puts a ground stop in place

            // Wait for user
            //Console.Read();
        }
    }


    // Subject: This is the thing being watched by Air Traffic Control centers
    abstract class AirlineSchedule
    {

        // properties 
        public string Name { get; set; }
        public string DeparturnAirport { get; set; }
        public string ArrivalAirport { get; set; }
        private DateTime departureDateTime;

        private List<IATC> observers = new List<IATC>();


        public AirlineSchedule(string airline, 
                               string outAirport, 
                               string inAirport, 
                               DateTime leaves )
        {
            this.Name = airline;
            this.DeparturnAirport = outAirport;
            this.ArrivalAirport = inAirport;
            this.DepartureDateTime = leaves;
        }


        // Here is where we actually attach our observers (ATCs)
        public void Attach(IATC atc)
        {
            observers.Add(atc);
        }

        public void Detach(IATC atc)
        {
            observers.Remove(atc);
        }


        public void OnChange(AirlineSchedule asched)
        {
            if (observers.Count != 0)
            {
                foreach (IATC o in observers)
                    o.Update(asched);
            }
        }

        public DateTime DepartureDateTime
        {
            get { return departureDateTime; }

            set
            {
                departureDateTime = value;
                OnChange(this);
                Console.WriteLine("");
            }
        }
    }// class AirlineSchedule


    // A Concrete Subject
    class CarrierSchedule : AirlineSchedule
    {
        // Jesse and Alex only really ever need to fly to one place...
        public CarrierSchedule(string name, DateTime departing) :
            base(name, "Boston", "Seattle", departing)
        {
        }
    }


    // An Observer
    interface IATC
    {
        void Update(AirlineSchedule sender);
    }


    // The Concrete Observer
    class AirTrafficControl : IATC
    {
        public string Name { get; set; }

        public AirTrafficControl(string name)
        {
            this.Name = name;
        }

        public void Update(AirlineSchedule sender)
        {
            Console.WriteLine(
                "{0} Air Traffic Control Notified:\n {1}'s flight 497 from {2} " +
                "to {3} new deprture time: {4:hh:mmtt}",
                Name,
                sender.Name,
                sender.DeparturnAirport,
                sender.ArrivalAirport,
                sender.DepartureDateTime );
            Console.WriteLine("---------");
        }
    }

}

Здесь упоминается код Ruby:

module Subject
  def initialize
    @observers=[]
  end
  def add_observer(observer)
    @observers << observer
  end
  def delete_observer(observer)
    @observers.delete(observer)
  end
  def notify_observers
    @observers.each do |observer|
      observer.update(self)
    end
  end
end


class Employee
  include Subject

  attr_reader :name, :address
  attr_reader :salary

  def initialize( name, title, salary)
    super()
    @name = name
    @title = title
    @salary = salary
  end
  def salary=(new_salary)
    @salary = new_salary
    notify_observers
  end
end

class TaxMan
  def update( changed_employee )
    puts("Send #{changed_employee.name} a new tax bill!")
  end
end

fred = Employee.new('Fred', 'Crane Operator', 30000.0)
tax_man = TaxMan.new
fred.add_observer(tax_man)

Вот пример "Programming .NET 3.5", который я переписал:

using System;

namespace Observer
{
    class Program
    {

        static void Main()
        {
            DateTime now = DateTime.Now;
            // Create new flights with a departure time and add from and to destinations
            CarrierSchedule jetBlue = new CarrierSchedule("JetBlue", now);
            jetBlue.Attach(new AirTrafficControl("Boston"));
            jetBlue.Attach(new AirTrafficControl("Seattle"));

            // ATCs will be notified of delays in departure time
            jetBlue.DepartureDateTime = 
                now.AddHours(1.25); // weather delay

            jetBlue.DepartureDateTime = 
                now.AddHours(1.75); // weather got worse

            jetBlue.DepartureDateTime = 
                now.AddHours(0.5);  // security delay

            jetBlue.DepartureDateTime = 
                now.AddHours(0.75); // Seattle puts a ground stop in place

            // Wait for user
            Console.Read();
        }
    }

    // Generic delegate type for hooking up flight schedule requests
    public delegate void ChangeEventHandler<T,U>
        (T sender, U eventArgs);

    // Customize event arguments to fit the activity
    public class ChangeEventArgs : EventArgs 
    {
        public ChangeEventArgs(string name, string outAirport, string inAirport, DateTime leaves) 
        {
            this.Airline = name;
            this.DeparturnAirport = outAirport;
            this.ArrivalAirport = inAirport;
            this.DepartureDateTime = leaves;
        }

        // Our Properties
        public string Airline               { get; set; }
        public string DeparturnAirport      { get; set; }
        public string ArrivalAirport        { get; set; }
        public DateTime DepartureDateTime   { get; set; }

    }  

    // Subject: This is the thing being watched by Air Traffic Control centers
    abstract class AirlineSchedule
    {

        // properties 
        public string Name                  { get; set; }
        public string DeparturnAirport      { get; set; }
        public string ArrivalAirport        { get; set; }
        private DateTime departureDateTime;

        public AirlineSchedule(string airline, string outAirport, string inAirport, DateTime leaves)
        {
            this.Name = airline;
            this.DeparturnAirport = outAirport;
            this.ArrivalAirport = inAirport;
            this.DepartureDateTime = leaves;
        }

        // Event
        public event ChangeEventHandler<AirlineSchedule, ChangeEventArgs> Change;

        // Invoke the Change event
        public virtual void OnChange(ChangeEventArgs e) 
        {
            if (Change != null)
            {
                Change(this, e);
            }
        }

        // Here is where we actually attach our observers (ATCs)
        public void Attach(AirTrafficControl airTrafficControl)
        {
            Change += 
                new ChangeEventHandler<AirlineSchedule, ChangeEventArgs>
                    (airTrafficControl.Update);
        }

        public void Detach(AirTrafficControl airTrafficControl)
        {
            Change -= new ChangeEventHandler<AirlineSchedule, ChangeEventArgs>
                (airTrafficControl.Update);
        }


        public DateTime DepartureDateTime
        {
            get { return departureDateTime; }
            set
            {
                departureDateTime = value;
                OnChange(new ChangeEventArgs(
                    this.Name, 
                    this.DeparturnAirport,
                    this.ArrivalAirport,
                    this.departureDateTime));
                Console.WriteLine("");
            }
        }


    }

    // A Concrete Subject
    class CarrierSchedule : AirlineSchedule
    {
        // Jesse and Alex only really ever need to fly to one place...
        public CarrierSchedule(string name, DateTime departing): 
            base(name,"Boston", "Seattle", departing)
        {
        }
    }

    // An Observer
    interface IATC
    {
        void Update(AirlineSchedule sender, ChangeEventArgs e);
    }

    // The Concrete Observer
    class AirTrafficControl : IATC
    {
        public string Name { get; set; }

        // Constructor
        public AirTrafficControl(string name)
        {
             this.Name = name;
        }

        public void Update(AirlineSchedule sender, ChangeEventArgs e)
        {

            Console.WriteLine(
                "{0} Air Traffic Control Notified:\n {1}'s flight 497 from {2} " +
                "to {3} new deprture time: {4:hh:mmtt}", 
                Name, 
                e.Airline, 
                e.DeparturnAirport, 
                e.ArrivalAirport, 
                e.DepartureDateTime);
            Console.WriteLine("---------");
        }
        public CarrierSchedule CarrierSchedule { get; set; }
    }
}

5 ответов

Шаблоны проектирования выражают идеи в общем смысле, а не в конкретной иерархии классов, которую следует использовать для реализации шаблона. В C# вы не реализовали бы идею, используя классы и интерфейсы (как, например, в Java), потому что она обеспечивает более простое решение. Вместо этого вы можете использовать события и делегатов. Вот хорошая статья, которую вы можете проверить:

Обратите внимание, что наблюдатель - не единственный шаблон, который может быть более элегантно закодирован в C#. Например, шаблон стратегии может быть реализован с использованием (однострочного) лямбда-выражения в C#:

Тем не менее, я довольно скептически отношусь к шаблонам проектирования во многих отношениях, но они могут быть полезны в качестве ссылки. Однако их не следует использовать вслепую. Некоторые авторы могут подумать, что строго следовать шаблону - единственный способ написать качественное "корпоративное" программное обеспечение, но это не так!

РЕДАКТИРОВАТЬ Вот краткая версия вашего кода Ruby. Я не читал версию C#, потому что она слишком сложная (и я бы даже сказал, запутанная):

class Employee {
  public Employee(string name, string address, int salary) {
    Name = name; Address = address; this.salary = salary;
  }

  private int salary;

  public event Action<Employee> SalaryChanged;

  public string Name { get; set; }
  public string Address { get; set; }
  public int Salary {
    get { return salary; }
    set { 
      salary = value;  
      if (SalaryChanged != null) SalaryChanged(this);
    }
  }

var fred = new Employee(...);
fred.SalaryChanged += (changed_employee) => 
  Console.WriteLine("Send {0} a new tax bill!", changed_employee.Name);

Это прекрасно подходит для проведения мероприятий и делегатов. Лямбда-функции C# 3.0 делают ваш пример еще проще, чем в Ruby:-).

У меня нет книги, поэтому я не могу подтвердить это, но, вероятно, причина, по которой в примере используются события и делегаты, заключается в том, что это первоклассные конструкции на языке C#. По сути, C# уже внедрил шаблон Observer для вас, так что вы можете использовать его где угодно.

Кроме того, я подозреваю, что отчасти причиной того, что пример C# является неуклюжим, является то, что Джесси Либерти не кажется мне ужасно искусным автором. Некоторые из его книг слишком формальны и запутаны (например, "Изучай язык программирования X за Y часов!"). В результате вы получаете неловкие, несколько поспешные примеры, которые выглядят так, как будто они были скопированы из его IDE, как только не было ошибок компилятора.

Почему шаблон Observer намного сложнее в C#, чем в Ruby?

Несколько причин для этого:

1) Утиная печать Ruby означает, что вам не нужно объявлять и реализовывать интерфейс.

2) Пример C# делает намного больше, чем пример Ruby.

3) Пример на C# написан плохо. Вы редко реализуете шаблон канонического наблюдателя вручную, так как события и делегаты запекаются. Чтобы не усложнять ситуацию, давайте переопределим код Ruby в C#, используя идиомы C#:

using System;
using System.Linq;
using System.Collections.Generic;

namespace Juliet
{
    class Employee
    {
        public event Action<Employee> OnSalaryChanged;

        public string Name { get; set; }
        public string Title { get; set; }

        private decimal _salary;
        public decimal Salary
        {
            get { return _salary; }
            set
            {
                _salary = value;
                if (OnSalaryChanged != null)
                    OnSalaryChanged(this);
            }
        }

        public Employee(string name, string title, decimal salary)
        {
            this.Name = name;
            this.Title = title;
            this.Salary = salary;
        }
    }

    class TaxMan
    {
        public void Update(Employee e)
        {
            Console.WriteLine("Send {0} a new tax bill!", e.Name);
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            var fred = new Employee("Fred", "Crane operator", 30000.0M);
            var taxMan = new TaxMan();
            fred.OnSalaryChanged += taxMan.Update;

            fred.Salary = 40000.0M;
        }
    }
}

Теперь код такой же простой, как Ruby.

Я не вижу большой разницы здесь в моей версии C#.

Я думаю, что автор упомянутой книги C# может попытаться сделать свой пример похожим на оригинальный шаблон Observer, где есть Subject, ConcreteSubject, Observer, а также ConcreteObserver классы. Это на самом деле не нужно во многих условиях. Много раз, достаточно просто подписаться на событие методом.

Используя событие и делегат C#, вы можете избавить себя от необходимости поддерживать "список наблюдателей" и связанные с ним методы присоединения / отсоединения. Они также предоставляют простой способ уведомить подписавшихся клиентов о новом событии.

Обновление: только что увидел реализацию @ Томаса. У него есть хорошее использование C# 3 там. Однако, если вы хотите увидеть прямое отображение из кода Ruby, мой пример ниже может помочь.

using System;

namespace Observer
{
    class Program
    {
        static void Main()
        {
            Employee fred = new Employee()
            {
                Name = "Fred",
                Title = "Crane Operator",
                Salary = 40000.0
            };

            TaxMan tax_man = new TaxMan();
            fred.Update += tax_man.OnUpdate;
            fred.Salary = 50000.0;
        }
    }

    public class Subject
    {
        public delegate void UpdateHandler(Subject s);
        public virtual event UpdateHandler Update;
    }

    public class Employee : Subject
    {
        public string Name { get; set; }
        public string Title { get; set; }
        private double _salary;
        public double Salary
        {
            get { return _salary; }
            set
            {
                _salary = value;
                if (Update != null)
                    Update(this);
            }
        }
        public override event UpdateHandler Update;
    }

    public class TaxMan
    {
        public void OnUpdate(Subject s)
        {
            if (s is Employee)
                Console.WriteLine("Send {0} a new tax bill!",
                    (s as Employee).Name);
        }
    }

}

Есть ли другая причина использовать делегаты и события, кроме того, что это делает код короче?

Да. Большинство языков программирования в наши дни имеют некоторую возможность закрытия. Это объединяет анонимные функции плюс возможность для этих функций ссылаться на переменные, объявленные вне их.

В Java, которую часто критикуют за отсутствие этой функции, она действительно существует. Чтобы воспользоваться этим преимуществом, вы должны написать целый анонимный класс (не только один метод), и вы можете ссылаться только на final переменные (т.е. не переменные). Так что это немного многословно и ограничено, но это работает. Вы можете написать абстрактный класс или интерфейс для представления обратного вызова (например, слушателя), а затем вы можете реализовать этот интерфейс с анонимным классом для предоставления обратного вызова.

В C# вы не можете писать анонимные классы, но вы можете писать отдельные анонимные методы. Вы можете сохранить их в переменной некоторого совместимого типа делегата. И анонимный метод может ссылаться на любые переменные в контексте, где живет анонимный метод:

int counter = 0;

Action<int> increase; // a delegate variable

increase = by => counter += by; // anonymous method modifies outer variable

increase(2); // counter == 2
increase(3); // counter == 5

Таким образом, чтобы ответить на эту часть вашего вопроса, одна из основных причин использования делегатов вместо абстрактных классов / интерфейсов в C# заключается в том, что он позволяет анонимным методам формировать замыкание по переменным. Это не просто "делает код короче" - оно позволяет совершенно по-новому взглянуть на ваши программы.

Другие вопросы по тегам