This post is part of a series of reviews on the book Head First: Object Oriented Analysis and Design. Check out the Introduction post for a full table of contents along with some initial notes.

This first chapter of this book review will be about a simple example that covers some the basic concepts of building ‘great software’ (as said in the book). The example shown is a Guitar Store that has simple features such as keeping an inventory of guitars and searching through them, so let’s get on with it.

First thing we need to do is build our base, which is our Guitar class and the Inventory class. Here’s the UML diagram for both classes:
Chapter 1 - Diagram 1

And here’s the code for them, first, the guitar.rb file:

class Guitar

  attr_reader :serial_number, :price, :builder, :model, :type, :back_wood, :top_wood
  attr_writer :price
  
  def initialize(serial_number, price,builder, model, type, back_wood, top_wood)
    @serial_number = serial_number
    @price = price
    @builder = builder
    @model = model
    @type = type
    @back_wood = back_wood
    @top_wood = top_wood
  end

  def to_s
    "#{@builder} #{@model} #{@type} guitar: \n #{@back_wood} back and sides, \n #{@top_wood} top. You can have it for only $#{@price}!"
  end
end

I added a to_s method which converts the object to a defined string so we can print this object directly. Initialize is the Ruby native constructing method, and attr_reader along with attr_writer are methods that do the work of getters and setters, so we don’t have to waste time writing all of those.

Not much secret there, now to the the inventory.rb file:


class Inventory
  
  def initialize
    @guitars = []
  end

  def add_guitar(serial_number,price,builder,model,type,back_wood,top_wood)
    guitar = Guitar.new(serial_number,price,builder,model,type,back_wood,top_wood)
    @guitars << guitar
  end

  def get_guitar(serial_number)
    @guitars.find { |guitar| serial_number == guitar.serial_number }
  end

  def search(search_guitar)
    @guitars.find { |guitar| search_guitar.builder == guitar.builder && search_guitar.model == guitar.model && search_guitar.type == guitar.type && search_guitar.back_wood == guitar.back_wood && search_guitar.top_wood == guitar.top_wood }
  end
end

We initialize the class by creating an array and assigning it to an instance variable. The add_guitar simply creates a guitar and adds it to the array, get_guitar uses an iterator to loop through the array until the condition passed to the block is met, and the same thing is done in the search method. Does it work? Seems to, doesn’t it? Let’s edit our main.rb file to test this out:

require 'guitar.rb'
require 'inventory.rb'

def initialize_inventory(inventory)
  inventory.add_guitar('V95693', '1499.95'.to_f, "Fender", "Stratocastor", "electric", "Alder", "Alder")
end

inventory = Inventory.new

initialize_inventory(inventory)

what_erin_likes = Guitar.new('',0,"fender","Stratocastor","electric","Alder","Alder")

guitar = inventory.search(what_erin_likes)

unless guitar.empty?
  puts "Erin, you might like this #{guitar.to_s}"
else
  puts "Sorry, Erin, we have nothing for you."
end

Our dear Erin comes in and asks for her dream guitar, so we gladly use our search method to find out that…it didn’t find anything. But we added a guitar just like the one she wanted to the inventory, so what went wrong? You might go crazy and want to rebuild the whole thing, it does look like a mess, but let’s organize ourselves and go through what the books defines as the three steps to writing ‘great software’.

  1. Make sure your software does what the customer wants it to do.
  2. Apply basic OO principles to add flexibility.
  3. Strive for a maintainable, reusable design.

First thing we need to do is make the software actually work, because right now it’s not doing its job to find guitars. The problem is that the string comparison isn’t working due to different letter cases. We could just call a method to make them all lowercase, but let’s do something fancier.

The book uses Enum, which are enumerated types that function sort of like constants, but in Ruby we don’t have Enums to work with, so, to have the same effect, we’re gonna have to do some magic. Here is the attributes.rb file that will be responsible for holding all the guitar attributes:

class Type
  VALUES = ['acoustic', 'electric']
end

class Builder
  VALUES = ['Fender', 'Martin', 'Gibson', 'Collings', 'Olson', 'Ryan', 'PSR', 'Any']
end

class Wood
  VALUES = ['Indian_Rosewood', 'Brazillian_Rosewood', 'Mahogany', 'Maple', 'Cocobolo', 'Cedar', 'Adirondack', 'Alder', 'Sitka']
end

[Type, Builder, Wood].each do |att|

  att::VALUES.each do |value|
    temp = Class.new( att )
    temp.class_eval %Q!
    def to_s
      '#{value.gsub('_',' ')}'
    end
    !

  att.const_set( value.upcase, temp.new )
  end
end

Each class has constant with an array of values associated to it. To avoid duplicate code I (with some help from my boss) used a concept called Anonymous Classes and the class_eval method which is frequently used as a shortcut to writing methods inside a class . The algorithm is basically looping through each class, creating a new instance of it, adding to_s methods for each value in the VALUES constant and transforming that value into another constant in the current class being looped through. This is all done so that each variable can be accessed like this: Type::ACOUSTIC, which is how the book does it in Java (actually he does it by Type.ACOUSTIC, but it’s the same idea).

Notice we didn’t create one for Model because there really isn’t a limited set of those. Now let’s change our main.rb file to adapt to our new way of calling guitar attributes:

require 'guitar.rb'
require 'inventory.rb'
require 'attributes.rb'

def initialize_inventory(inventory)
  inventory.add_guitar('V95693', '1499.95'.to_f, Builder::FENDER, "Stratocastor", Type::ELECTRIC, Wood::ALDER, Wood::ALDER)
end

inventory = Inventory.new

initialize_inventory(inventory)

what_erin_likes = Guitar.new('', 0, Builder::FENDER, "Stratocastor", Type::ELECTRIC, Wood::ALDER, Wood::ALDER)

guitar = inventory.search(what_erin_likes)

unless guitar.empty?
  puts "Erin, you might like this #{guitar.to_s}"
else
  puts "Sorry, Erin, we have nothing for you."
end

Remember to edit the Inventory search method to compare the models in downcase:

def search(search_guitar)
    @guitars.find { |guitar| search_guitar.builder == guitar.builder && search_guitar.model.downcase == guitar.model.downcase && search_guitar.type == guitar.type && search_guitar.back_wood == guitar.back_wood && search_guitar.top_wood == guitar.top_wood }
  end

But wouldn’t it be better if that search method returned more than one result? Clients always love multiple choices, so let’s get to it:

def search(search_guitar)
    matches = []
    @guitars.each do |guitar|
      if search_guitar.builder == guitar.builder && search_guitar.model.downcase == guitar.model.downcase && search_guitar.type == guitar.type && search_guitar.back_wood == guitar.back_wood && search_guitar.top_wood == guitar.top_wood
        matches << guitar
      end
    end
    matches
  end

Now to the test (I changed the to_s method in the Guitar class just for output cosmetics):

require 'guitar.rb'
require 'inventory.rb'
require 'attributes.rb'

def initialize_inventory(inventory)
  inventory.add_guitar('V95693', '1499.95'.to_f, Builder::FENDER, "Stratocastor", Type::ELECTRIC, Wood::ALDER, Wood::ALDER)
  inventory.add_guitar('V9512', '1549.95'.to_f, Builder::FENDER, "Stratocastor", Type::ELECTRIC, Wood::ALDER, Wood::ALDER)
end

inventory = Inventory.new

initialize_inventory(inventory)

what_erin_likes = Guitar.new('', 0, Builder::FENDER, "Stratocastor", Type::ELECTRIC, Wood::ALDER, Wood::ALDER)

guitars = inventory.search(what_erin_likes)

unless guitars.empty?
  puts "Erin, you might like these guitars: \n #{guitars.to_s}"
else
  puts "Sorry, Erin, we have nothing for you."
end

Notice I didn’t have to loop through the guitars array and call the to_s method on each guitar inside it, because Ruby always does that for me when I call the to_s method on the whole array.

With all these changes, our UML class diagrams also get updated:
Chapter 1 - Diagram 2

Anyway, here’s the output we all expected and were rewarded with:

Erin, you might like these guitars:
We have a Fender Stratocastor electric guitar:
Alder back and sides,
Alder top. You can have it for only $1499.95!
——–
We have a Fender Stratocastor electric guitar:
Alder back and sides,
Alder top. You can have it for only $1549.95!
——–

This means we successfully passed through the first step on how to build ‘great software’, which is making sure your software does what the customer wants it to do. This means we can move on to the next step, which is applying basic OO principles to add flexibility. We’ll basically be avoiding duplicate code as much as possible and making sure our objects are well-designed.

If we stop to think about it, our clients aren’t providing a whole Guitar object, they just hand us some specifications that will then lead to a Guitar object. The proof of this is that serial_number and price were null values in Erin’s request:

what_erin_likes = Guitar.new('', 0, Builder::FENDER, "Stratocastor", Type::ELECTRIC, Wood::ALDER, Wood::ALDER)

Since that doesn’t seem object-orientedly (what?) right, let’s make use of a basic OO principle called encapsulation, which allows you to hide the inner workings of your application’s parts, but yet make it clear what each part does. Since the client doesn’t provide a Guitar object, let’s create a class for something he does provide, which is a Guitar Specification (guitar_spec.rb):

class GuitarSpec
  attr_reader :builder, :model, :type, :back_wood, :top_wood
  def initialize(builder,model,type,back_wood,top_wood)
    @builder = builder
    @model = model
    @type = type
    @back_wood = back_wood
    @top_wood = top_wood
  end
end

We basically transfered all the guitar-related specifications into another class. What’s the point in always creating an object with a null price and serial number and throwing those in unnecessarily into a search method that won’t even use them? Plus, the code is much more organized into logical sections. To make the encapsulation we just did work with the rest app, we need to make some changes. First, to the Guitar Class:


class Guitar

  attr_reader :serial_number, :price, :spec
  attr_writer :price
  
  def initialize(serial_number,price,spec)
    @serial_number = serial_number
    @price = price
    @spec = spec
  end

  def to_s
    "We have a #{@spec.builder} #{@spec.model} #{@spec.type} guitar: \n #{@spec.back_wood} back and sides, \n #{@spec.top_wood} top. You can have it for only $#{@price}!\n -------- \n"
  end
end

Next, the Inventory class:


class Inventory
  
  def initialize
    @guitars = []
  end

  def add_guitar(serial_number,price,spec)
    guitar = Guitar.new(serial_number,price,spec)
    @guitars << guitar
  end

  def get_guitar(serial_number)
    @guitars.find { |guitar| serial_number == guitar.serial_number}
  end

  def search(guitar_spec)
    matches = []
    @guitars.each do |guitar|
      if guitar_spec.builder == guitar.spec.builder && guitar_spec.model.downcase == guitar.spec.model.downcase && guitar_spec.type == guitar.spec.type && guitar_spec.back_wood == guitar.spec.back_wood && guitar_spec.top_wood == guitar.spec.top_wood
        matches << guitar
      end
    end
    matches
  end
end

And finally, our test file:

require 'guitar.rb'
require 'inventory.rb'
require 'attributes.rb'
require 'guitar_spec.rb'

def initialize_inventory(inventory)
  spec = GuitarSpec.new(Builder::FENDER,"Stratocastor", Type::ELECTRIC, 6,Wood::ALDER,Wood::ALDER)
  inventory.add_guitar('V95693','1499.95'.to_f,spec)
  inventory.add_guitar('V9512','1549.95'.to_f,spec)
end

inventory = Inventory.new

initialize_inventory(inventory)

what_erin_likes = GuitarSpec.new(Builder::FENDER, "Stratocastor", Type::ELECTRIC, 6, Wood::ALDER, Wood::ALDER)

guitars = inventory.search(what_erin_likes)

unless guitars.empty?
  puts "Erin, you might like these guitars: \n #{guitars.to_s}"
else
  puts "Sorry, Erin, we have nothing for you."
end

Notice that the client is now providing a GuitarSpec instead of a whole guitar, and it even made it easier to add guitars to the inventory. Step 2 complete. Here’s its UML class diagrams:
Chapter 1 - Diagram 3

But it still needs more design changes, I mean, if we needed to add another guitar specification we’d still need to change things in a lot of places, which is contrary to our step 3: Strive for a maintainable, reusable design. With that in mind, let’s focus on refactoring our application so we won’t need to recode the whole thing every time a new specification comes along.

First comes the easy part, to add another guitar specification, good thing we know where that goes:

class GuitarSpec
  attr_reader :builder, :model, :type, :num_strings, :back_wood, :top_wood
  def initialize(builder,model,type,num_strings,back_wood,top_wood)
    @builder = builder
    @model = model
    @type = type
    @num_strings = num_strings
    @back_wood = back_wood
    @top_wood = top_wood
  end
end

Our current search method is dumbly comparing all the client’s specifications will all the guitar’s specifications in our inventory, but why don’t we just compare the objects instead of their attributes? This way we delegate the problem to the GuitarSpec object instead of dealing with it in the Inventory object, which is a much smarter solution because all we’ll need to change when adding specifications will be the GuitarSpec object (sounds pretty logical now, doesn’t it?):

def search(guitar_spec)
    matches = []
    @guitars.each do |guitar|
      if guitar_spec.matches(guitar.spec)
        matches << guitar
      end
    end
    matches
  end

Delegation ready, now time to set up the GuitarSpec to take care of the comparisons:

class GuitarSpec
  attr_reader :builder, :model, :type, :num_strings, :back_wood, :top_wood
  
  def initialize(builder,model,type,num_strings,back_wood,top_wood)
    @builder = builder
    @model = model
    @type = type
    @num_strings = num_strings
    @back_wood = back_wood
    @top_wood = top_wood
  end

  def matches(search)
    @builder==search.builder && @model.downcase==search.model.downcase && @type==search.type && @num_strings==search.num_strings && @back_wood==search.back_wood && @top_wood==search.top_wood
  end
end

Now we only need to change the GuitarSpec class to add more specifications. Step 3 complete. We now have a piece of simple, but killer software. Here’s the final UML class diagram (I only built this because we’ll be using it in another chapter):
Chapter 1 - Diagram 4

And we’re done with the introduction chapter that presents us with the three easy steps to always developing ‘great software’. I translated most of the Java code to the best I know so far of Ruby, so if you have any other more idiomatic solutions, please post =)

Cheers!