viphat / til

Today I Learned
http://notes.viphat.work
0 stars 1 forks source link

[Ruby] - [Design Patterns in Ruby] - 5. Observer Pattern #223

Open viphat opened 6 years ago

viphat commented 6 years ago

How can you make the Employee object spread the news about salary changes without tangling it up with the payroll system?

The observer pattern is used when you are building a system where the state of one object effects the state of other objects. Take the weather channel for example, it’s their job to report the forecast for the day. They report this information to you, the person observing the broadcast. If the anchor on the weather channel reports that it is going to rain maybe that would mean that you can’t cut the lawn today… it doesn’t really matter. All the anchor is responsible for is reporting the weather, it’s up to you to take that information and plan your day accordingly.

Staying Informed

One way to solve this problem is to focus on the fact that the Employee object is acting as a source of news. Fred gets a raise and his Employee record shouts out to the world (or at least to anyone who seems interested), “Hello! I’ve got something going on here!” Any object that is interested in the state of Fred’s finances need simply register with his Employee object ahead of time. Once registered, that object would receive timely updates about the ups and downs of Fred’s paycheck.

The straightforward way:

class Payroll
  def update(changed_employee)
    puts("Cut a new check for #{changed_employee.name}!")
    puts("His salary is now #{changed_employee.salary}!")
  end
end

class Employee
  attr_reader :name, :title
  attr_reader :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

The Observer Pattern

ch05fig01


class Payroll
  def update(changed_employee)
    puts("Cut a new check for #{changed_employee.name}!")
    puts("His salary is now #{changed_employee.salary}!")
  end
end

class Employee
  attr_reader :name, :title, :salary

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

  def salary=(new_salary)
    @salary = new_salary
    notify_observers
  end

  def add_observer(observer)
    @observers << observer
  end

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

  private

  def notify_observers
    @observers.each do |observer|
      observer.update(self)
    end
  end
end
fred = Employee.new('Fred', 'Crane Operator', 30000.0)      
payroll = Payroll.new   
fred.add_observer( payroll )      
fred.salary = 35000.0

Add more observer:

class TaxMan
  def update( changed_employee )
    puts("Send #{changed_employee.name} a new tax bill!")
  end
end
tax_man = TaxMan.new
fred.add_observer(tax_man)

The GoF called this idea of building a clean interface between the source of the news that some object has changed and the consumers of the news the Observer Pattern

Better way to implement Observer Pattern in Ruby

Using module observable

require 'observer'

class Employee
  include Observable

  attr_reader :name, :address, :salary

  def initialize(name, title, salary)
   @name = name
   @title = title
   @salary = salary
  end

  def salary=(new_salary)
    return if @salary == new_salary
    @salary = new_salary
    changed # marked that object has been changed
    notify_observers(self) # Each call to notify_observers sets the changed flag back to false
  end
end

Abusing the observer pattern

Xét trường hợp một implementation đơn giản như này:

require 'observer'

class Employee
  include Observable

  attr_reader :name, :address, :salary

  def initialize(name, title, salary)
   @name = name
   @title = title
   @salary = salary
  end

  def salary=(new_salary)
    return if @salary == new_salary
    @salary = new_salary
    changed
    notify_observers(self)
  end

  def title=(new_title)
    return if @title == new_title
    @title = new_title
    changed
    notify_observers(self)
  end
end

Khi Fred được promote lên vị trí cao hơn (với title mới) và có mức lương cao chót vót (ứng với title mới):

fred = Employee.new("Fred", "Crane Operator", 30000)
fred.salary = 1_000_000
fred.title = 'Vice President of Sales'

Giả sử một trong các observer có dùng đến title của Fred

class Payroll
  def update(changed_employee)
    puts("Cut a new check for #{changed_employee.name}!")
    puts("His title is now #{changed_employee.title}!")
    puts("His salary is now #{changed_employee.salary}!")
  end
end

Thì khi chạy đoạn code trên, ta sẽ thấy tình trạng inconsistent xuất hiện, vì chúng ta đã cập nhật mức lương của Fred trước khi cập nhật title của anh ấy nên khi payroll.update được gọi ở lần đầu tiên (do salary=) thì kết quả sẽ hiển thị sai.

You can deal with this problem by not informing the observers until a consistent set of changes is complete.

# Don't inform the observers just yet   
fred.salary = 1000000
fred.title = 'Vice President of Sales'

# Now inform the observers!
fred.changes_complete
viphat commented 6 years ago

There are a few similarities between the Observer and Strategy patterns. Both patterns employ an object (the Observer’s subject and the Strategy’s context) that makes calls to another object (the Observer’s observer or Strategy’s strategy). The difference between the two patterns is the purpose and use case. The Strategy pattern relies on the strategy to do the work, while the Observer pattern informs the observers of what is going on with the subject.