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