Design Patterns in Ruby: Keeping Up with the Times with the Observer Pattern
One of the knottiest design challenges is the problem of building a system that is highly integrated—that is, a system where every part is aware of the state of the whole. Think about a spreadsheet, where editing the contents of one cell not only changes the number in the grid but also changes the column totals, alters the height of one of the bars in a bar chart, and enables the Save button. Even more simply, how about a personnel system that needs to let the payroll department know when someone's salary changes?
Building this kind of system is hard enough, but throw in the requirement that the system be maintainable and now you are talking truly difficult. How can you tie the disparate parts of a large software system together without increasing the coupling between classes to the point where the whole thing becomes a tangled mess? How do you construct a spreadsheet that properly displays changes in the data without hard-coding a link between the spreadsheet editing code and the bar chart renderer? How can you make the Employee object spread the news about salary changes without tangling it up with the payroll system?
Staying Informed
One way to solve this problem is to focus on the fact that the spreadsheet cell and the Employee object are both 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.
How would all of this work in code? Here is a basic version of an Employee object with no code to tell anyone anything—it just goes about its business of keeping track of an employee:
class Employee attr_reader :name attr_accessor :title, :salary def initialize( name, title, salary ) @name = name @title = title @salary = salary end end
Because we have made the salary field accessible with attr_accessor, our employees can get raises:1
fred = Employee.new("Fred Flintstone", "Crane Operator", 30000.0) # Give Fred a raise fred.salary=35000.0
Let's now add some fairly naive code to keep the payroll department informed of pay changes:
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
We can now change Fred's wages:
payroll = Payroll.new fred = Employee.new('Fred', 'Crane Operator', 30000, payroll) fred.salary = 35000
And the payroll department will know all about it:
Cut a new check for Fred! His salary is now 35000!
Note that since we need to inform the payroll department about changes in salary, we cannot use attr_accessor for the salary field. Instead, we need to write the salary= method out by hand.