为什么Observer模式在C#中比在Ruby中复杂得多?

时间:2010-02-06 18:23:40

标签: c# ruby observer-pattern

我读过"Design Patterns in Ruby" by Russ Olsen 如何在Ruby中实现Observer模式。我注意到了Ruby的实现 这种模式比C#实现简单得多, 例如"Programming .NET 3.5" by Jesse Liberty and Alex Horovitz中显示的实现。

所以我改写了“Programming .NET 3.5”观察者模式的例子 (使用“Ruby中的设计模式”)(pdf版本的第251页) 算法,可以下载两种实现的源代码 来自上述网站。

下面是重写的例子,告诉我你的想法是什么?
我们是否真的需要使用事件和委托来使用Observer模式 在C#?


更新 读完评论后,我想问一下这个问题:
有没有其他理由使用委托和事件,除了它使代码更短? 我不谈论GUI编程。

UPDATE2 我终于明白了,委托只是一个函数指针,事件是委托的更安全版本,它只允许两个操作+ =和 - =。

我重写了“Programming .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 个答案:

答案 0 :(得分:13)

设计模式表达一般意义上的想法,而不是应该用于实现模式的特定类层次结构。在C#中,您不会使用类和接口(例如在Java中)实现该想法,因为它提供了更直接的解决方案。您可以改用事件代理。这是一篇很好的文章,您可以查看:

请注意,观察者不是唯一可以在C#中更优雅地编码的模式。例如,策略模式可以使用C#中的(单行)lambda表达式实现:

那就是说,我对设计模式在许多方面持怀疑态度,但它们可能有用作参考。但是,不应盲目使用它们。有些作者可能认为严格遵循模式是编写高质量“企业”软件的唯一方法,但事实并非如此!

修改 这是您的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 lambda函数使你的例子比Ruby更简单: - )。

答案 1 :(得分:9)

我没有这本书,所以我无法证实这一点,但是这个例子使用事件和委托的原因可能是因为它们是C#语言中的第一类构造。基本上, C#已经为您实现了Observer模式 ,因此您可以在任何地方使用它。

此外,我怀疑C#示例笨重的部分原因是因为杰西自由并没有让我成为一名非常娴熟的作家。他的一些书有点太公式化和死记硬背(例如“在Y小时学习编程语言X!”)。结果就是你得到了一些尴尬的,有些匆忙的例子,看起来就像没有编译错误就从他的IDE中复制粘贴了。

答案 2 :(得分:3)

  

为什么观察者模式更多   在C#中比在Ruby中复杂吗?

有几个原因:

1)Ruby的duck typing意味着你不需要声明和实现一个接口。

2)C#示例比Ruby示例做得更多。

3)C#示例编写得很糟糕。由于事件和委托已经完成,你很少很少手工实现规范的观察者模式。为了保持公平,让我们使用C#习惯用C#重新实现Ruby代码:

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一样简单。

答案 3 :(得分:1)

我在C#版本中看不出太多差异。

我认为提到C#book的作者可能会尝试使他的示例看起来像原始观察者模式,其中有SubjectConcreteSubjectObserver和{{1} }类。在许多情况下这些实际上是不必要的很多时候,用方法订阅事件就足够了。

通过使用C#提供的事件和委托,您可以自己无需维护“观察者列表”及其相关的附加/分离方法。它们还提供了一种向订阅客户通知新事件的简便方法。

更新:刚刚看到@ Tomas的实施。他很好地利用了C#3。但是,如果你想看到Ruby代码的直接映射,我下面的例子可能有所帮助。

ConcreteObserver

答案 4 :(得分:1)

  

还有其他理由可以使用   代表和事件除此之外   使代码更短?

是。如今,大多数编程语言都具有一些“封闭”功能。这结合了匿名函数,以及这些函数引用在它们之外声明的变量的能力。

在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#中使用委托而不是抽象类/接口的一个主要原因是它启用了可以在变量上形成闭包的匿名方法。这不仅仅是“缩短代码” - 它还能让您以全新的方式思考您的程序。