Pull to refresh

Использование паттерна Observer в Ruby

Часто в различных системах, возникает необходимость совершать действия в одной части приложения при изменениях в другой. Задача стоит в информировании всех системы о изменениях в одной из ее частей. Например у нас есть пользователи со статусами, при изменении статуса требуется выполнить ряд действий: разослать сообщение другим, сохранить старый статус в историю изменений. Посмотрим как реализовать такую логику в приложении.

У нас есть класс User:

class User
attr_reader :name
attr_accessor :status

def initialize(name, status)
@name = name
@status = status
end

end


И есть класс News, который рассылает сообщения в системе.

class News
def update(user)
puts("#{user.name} changed his status!")
puts("His status is now #{user.status}!")
end
end


Теперь изменим немного класс User для того что бы он смог сообщать о изменениях в своем статусе.

class User
attr_reader :name , :status

def initialize(name, status, news)
@name = name
@status = status
@news = news
end

def status=(status)
@status = status
@news. update (self)
end
end

class News
def update (user)
puts("#{user.name} changed his status!")
puts("His status is now #{user.status}!")
end
end


Проверим и получим сообщение о изменении в статусе пользователя от класса news:

news = News.new
alex = User.new('Alex', 'Happy', news)
alex.status = "Sad"


Лучший способ информировать систему о изменениях.


Основная проблема такого подхода в том, что класс User тесно связан с классом News. Теперь например мы должны еще сохранить старый статус в истории статусов: необходимость изменять класс User при этом очень неудачна, так как сам класс и его логика на самом деле не меняется.
Лучше немного отойти от частной реализации и создать более общий интерфейс для информирования системы:

class User
attr_reader :name
attr_accessor :status

def initialize(name, status)
@name = name
@status = status
@observers = []
end

def status=(status)
@status = status
notify_observers
end

def notify_observers
@observers.each do |observer|
observer.update(self)
end
end

def add_observer(observer)
@observers << observer
end

def delete_observer(observer)
@observers.delete(observer)
end

end


Теперь любой объект, который хочет узнавать о обновлениях, может быть просто добавлен как наблюдатель (observer) обьекта класса User.

news = News.new
alex = User.new('Alex', 'Happy')
alex.add_observer(news)
alex.status = "Sad"


Теперь мы избавились от неявной связи между классами User и News. Код класс User теперь не зависит от количества объектов заинтересованных в информации о изменениях объекта User. Кроме того объекты класса User могут существовать и совсем без наблюдателей (observers).

Немного рефакторинга

Допустим мы хотим пользоваться этим паттерном для различных классов и совсем не хотим повторять этот код каждый раз. Для этого выделим этот паттерн в отдельный модуль.

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


Теперь мы можем просто включить модуль Subject в любой класс

class User
attr_reader :name
attr_accessor :status

def initialize(name, status)
super()
@name = name
@status = statusend
end

def status=(status)
@status = status
notify_observers
end
end


Observers in Rails

В рельсах реализован очень удобный механизм для наблюдения за моделями. Используя их мы можем отрефакторить код и убрать неявные связи между моделями.

Плохой пример


class Project < ActiveRecord::Base
after_create :send_create_notifications

private
def send_create_notifications
self.members.each do |member|
ProjectMailer.deliver_notification(self, member)
end
end
end


В этом примере мы используем callback модели для рассылки сообщений после ее создания. В данном случае лучше будет использовать observers, так как мейлер это подсистема, которая используется и другими классами.

Использование observers

class Project < ActiveRecord::Base
# nothing here
end

class NotificationObserver < ActiveRecord::Observer
observe Project

def after_create(project)
project.members.each do |member|
ProjectMailer.deliver_notice(project, member)
end
end
end


Теперь код рассылки сообщений вынесен из класса Project и намного удобнее поддерживать код почтовых оповещений в отдельном классе.
Tags:
Hubs:
You can’t comment this publication because its author is not yet a full member of the community. You will be able to contact the author only after he or she has been invited by someone in the community. Until then, author’s username will be hidden by an alias.