суббота, 12 января 2013 г.

Паттерны Ruby. Наблюдатель (Observer)

Шаблон Наблюдатель (Observer) применяется в сильно интегрированных системах, где каждая часть должна знать состояние всей системы, или большей ее части (например, если нужно показать график изменений какого-либо значения без жесткого указания связи в коде или работнику нужно оповестить об изменении зарплаты соответствующие отделы).
Диаграмма классов паттерна Наблюдатель (Observer)


Рассмотрим пример, когда платежная ведомость должна знать об изменении зарплаты сотрудника.
Приведем простейшую версию класса сотрудник:
#-*- encoding: utf-8 -*-
class Employee
attr_reader :name
attr_accessor :salary, :title
def initialize(name, title, salary)
@name = name
@title = title
@salary = salary
end
end
fred = Employee.new("Fred Flinstone", "Crane operator", 30000.0)
# Так как мы сделали attr_accessor к полю salary, можно написать
fred.salary = 35000.0
view raw employee.rb hosted with ❤ by GitHub


Она никого ни о чем не уведомляет, эта версия просто представляет собой базовые поля объекта.

Теперь добавим простейшую реализацию слежения за изменением зарплаты - Employee будет принимать платежную ведомость и вызывать ее метод update при изменении зарплаты:
#-*- encoding: utf-8 -*-
class Payroll
def update(changed_employee)
puts "Новый чек для работника #{changed_employee.name}"
puts "Его зарплата теперь составляет #{changed_employee.salary}!"
end
end
class Employee
attr_reader :name, :title
attr_accessor :title, :salary
def initialize(name, title, salary, payroll)
@name = name
@title = title
@salary = salary
@payroll = payroll
end
def salary=(new_salary)
@salary = new_salary
# уведомляем наблюдателя об изменении зарплаты
@payroll.update(self)
end
end
payroll = Payroll.new
fred = Employee.new("Fred Flinstone", "Crane operator", 30000.0, payroll)
fred.salary = 35000.0
# Новый чек для работника Fred Flinstone
# Его зарплата теперь составляет 35000.0!
view raw payroll.rb hosted with ❤ by GitHub

Недостатком данного кода является жесткая привязка объектов. Что будет если понадобится извещать больше одного слушателя? Нам придется лезть в код Employee и менять его, что, согласитесь, очень не правильно.

Обобщим решение, создав переменную, хранящую массив наблюдателей и реализовав методы по добавлению и удалению наблюдателей:

#-*- encoding: utf-8 -*-
class Employee
attr_reader :name
attr_accessor :salary
def initialize(name, salary)
@name = name
@salary = salary
@observers = []
end
def notify_observers
@observers.each do |observer|
observer.update_from_observable(self)
end
end
def add_observer(observer)
@observers << observer
end
def delete_observer(observer)
@observers.delete(observer)
end
def salary=(new_salary)
@salary = new_salary
notify_observers
end
end
class Payroll
def update_from_observable(context)
puts "Обновление в Payroll"
puts "Зарплата теперь #{context.salary}"
end
end
class Watcher
def update_from_observable(context)
puts "Обновление в Watcher"
puts "Зарплата теперь #{context.salary}"
end
end
fred = Employee.new("Fred", 30000.0)
# добавляем двух наблюдателей
fred.add_observer(Payroll.new)
fred.add_observer(Watcher.new)
# и изменяем зарплату
fred.salary = 35000.0
# Обновление в Payroll
# Зарплата теперь 35000.0
# Обновление в Watcher
# Зарплата теперь 35000.0
view raw observer.rb hosted with ❤ by GitHub

Так выглядит простейшая реализация паттерна с использованием модулей. Чтобы их использовать, достаточно в класс наблюдателя включить модуль Observer и реализовать метод update_from_observable(context). Context при этом является объектом наблюдаемого. В наблюдаемый объект нужно включить модуль Observable, добавить наблюдателей с помощью метода add_observer и вызывать метод notify_observers каждый раз, когда необходимо уведомить наблюдателей об изменениях.
#-*- encoding: utf-8 -*-
# модуль наблюдателя
module Observer
module InstanceMethods
def update_from_observable(context)
raise "Implement method update in observer"
end
end
def self.included(receiver)
receiver.send :include, InstanceMethods
end
end
# модуль объекта наблюдения
module Observable
module InstanceMethods
def notify_observers
@observers.each do |observer|
observer.update_from_observable(self)
end
end
def add_observer(observer)
@observers ||= []
@observers << observer
end
def delete_observer(observer)
@observers.delete(observer)
end
end
def self.included(receiver)
receiver.send :include, InstanceMethods
end
end
class Employee
attr_reader :name
attr_accessor :salary
include Observable
def initialize(name, salary)
@name = name
@salary = salary
end
def salary=(new_salary)
@salary = new_salary
notify_observers
end
end
class Payroll
def update_from_observable(context)
puts "Обновление в Payroll"
puts "Зарплата теперь #{context.salary}"
end
end
class Watcher
def update_from_observable(context)
puts "Обновление в Watcher"
puts "Зарплата теперь #{context.salary}"
end
end
fred = Employee.new("Fred", 30000.0)
# добавляем двух наблюдателей
fred.add_observer(Payroll.new)
fred.add_observer(Watcher.new)
# и изменяем зарплату
fred.salary = 35000.0
# Обновление в Payroll
# Зарплата теперь 35000.0
# Обновление в Watcher
# Зарплата теперь 35000.0

Комментариев нет:

Отправить комментарий