4

我在Russ Olsen 的“Ruby 中的设计模式”中阅读了 如何在 Ruby 中实现观察者模式。我注意到这种模式的 Ruby 实现比 C# 实现简单得多,例如Jesse Liberty 和 Alex Horovitz 在“Programming .NET 3.5”中展示的实现。

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

下面是重写的例子,告诉我你怎么看?
我们真的需要使用事件和委托来使用 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; }
    }
}
4

5 回答 5

15

设计模式表达的是一般意义上的想法,而不是应该用于实现模式的特定类层次结构。在 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 中的示例更简单:-)。

于 2010-02-06T19:01:44.057 回答
9

我没有这本书,所以我无法证实这一点,但该示例使用事件和委托的原因很可能是因为它们是 C# 语言中的一流构造。本质上,C# 已经为您实现了观察者模式,因此您可以在任何地方使用它。

此外,我怀疑 C# 示例笨拙的部分原因是因为 Jesse Liberty 并没有让我觉得我是一个非常熟练的作者。他的一些书有点过于公式化和死记硬背(例如“在 Y 小时内学习编程语言 X!”)。结果是你最终得到了一些尴尬的、有点仓促的示例,这些示例看起来像是从他的 IDE 中复制粘贴的,只要没有编译器错误。

于 2010-02-06T18:28:07.807 回答
3

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

有几个原因:

1) Ruby 的鸭式类型意味着您不需要声明和实现接口。

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

于 2010-02-06T19:23:47.003 回答
1

在我的 C# 版本中,我没有看到太大的区别。

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

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

更新:刚刚看到@Tomas 的实现。他在那里很好地使用了 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);
        }
    }

}
于 2010-02-06T19:14:59.583 回答
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# 中使用委托而不是抽象类/接口的一个主要原因是它启用了可以形成变量闭包的匿名方法。这不仅“使代码更短”——它还为您的程序提供了一种全新的思考方式。

于 2010-02-09T17:14:09.940 回答