Hayden Hayden - 2 months ago 20
Ruby Question

Demystify global class operator design pattern

I was working through a online code bootcamp tutorial on "advanced class methods" when I noticed something strange. First the Person class is defined, and specifically, the #normalize_names method. Basically, this method iterates through every instance of class Person stored in @@all, and capitalizes the first and last names stored in person.name

class Person
attr_accessor :name
@@all = []
def self.all
@@all
end

def initialize(name)
@name = name
@@all << self
end

def self.normalize_names
self.all.each do |person|
person.name = person.name.split(" ").collect{|w| w.capitalize}.join(" ")
end
end
end


Then the tutorial explains that "Given how complex normalizing a person's name is, we should actually encapsulate [the method] into the Person instance." So, subsequently the #normalize_names method is refactored to look like this.

def normalize_name
self.name.split(" ").collect{|w| w.capitalize}.join(" ")
end

def self.normalize_names
self.all.each do |person|
person.name = person.normalize_name
end
end


Thus "The class method that acts on the global data of all people is simplified and delegates the actual normalization to the original instances. This is a common pattern for global class operators." Why is this common? What about the complexity of normalizing names merits this design pattern?

Answer Source

The glaring thing that the first normalize_names implementation is missing, is a way to update just a single person? one option is to write another class method, which accepts a single person instance. In the interest of separation of concerns, this could be split into two methods:

def self.normalize_names
  # note that 'self' can be omitted in most cases
  all.each { |person| normalize_name person }
end

def self.normalize_name(person)
  # note use of proc shorthand to shorten collect { |x| x.capitalize }
  person.name = person.name.split(" ").collect(&:capitalize).join(" ")
end

Now at least there is a way to update just a single person (should the need arise). Although there's no firm requirement to use instance vs class methods, it may be desired to have a slightly more terse API, e.g. person.normalize_name and not Person.normalize_name(person). That's probably the biggest reason to move it to an instance method, to be honest:

def self.normalize_names
  all.each &:normalize_name
end

def normalize_name
  # note that attr_writers ("self.name =") are one of the few places you need 
  # "self", to differentiate it from variable assignment
  self.name = name.split(" ").map(&:capitalize).join " "
end