Почему шаблон Observer намного сложнее в C #, чем в Ruby? - PullRequest
4 голосов
/ 06 февраля 2010

Я прочитал в "Шаблоны проектирования в Ruby" Русса Олсена как шаблон Observer может быть реализован в Ruby. Я заметил, что реализация Ruby этого шаблона намного проще, чем реализация C #, например реализация показана в "Программирование .NET 3.5" Джесси Либерти и Алекс Горовиц .

Итак, я переписал пример шаблона Observer «Программирование .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 ]

13 голосов
/ 06 февраля 2010

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

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

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

EDIT Вот краткая версия вашего кода на 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: -).

9 голосов
/ 06 февраля 2010

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

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

3 голосов
/ 06 февраля 2010

Почему шаблон 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.

1 голос
/ 09 февраля 2010

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

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

В 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 #заключается в том, что он включает анонимные методы, которые могут образовывать замыкания по переменным.Это не просто «делает код короче» - оно позволяет совершенно по-новому взглянуть на ваши программы.

1 голос
/ 06 февраля 2010

Я не вижу большой разницы в моей версии 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);
        }
    }

}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...