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.

In this fifth chapter we’re gonna get back to our Guitar Searching application from chapter 1 and tune it up to a more flexible software. “But wasn’t it already flexible?”, you ask, yes, it was, but our client just came back to us saying he’s gonna sell mandolins too, and that we need to turn our search engine into something less specific. So when it comes to adding other instruments, our application isn’t that flexible at all, and that’s what we’ll be working on in this chapter.

Let’s freshen our heads with the UML class diagram of the current Guitar Search application:
Chapter 5 - Diagram 1

Our first attempt to generalize our Guitar search into an Instrument search is to use an OO concept called inheritance, where we specialize any instrument classes under an abstract ‘superclass’ called Instrument:
Chapter 5 - Diagram 2

We basically moved all Guitar attributes and operations up to instrument, with the exception of Spec of course, since every instrument has its own specifications. So what’s the use of the abstract classes? These types of classes define behavior, and it’s subclasses implement that behavior. In short, abstract classes are placeholders for actual implementation classes.
The same way we abstracted Instrument we’ll have to abstract InstrumentSpec, since some instruments have different specifications than others, but have also alot in common too. Whenever you find common behavior in two or more places, look to abstract that behavior into a class, and then reuse that behior in common classes. So by abstracting InstrumentSpec too, this is what we get:
Chapter 5 - Diagram 3

Notice the aggregation diamond between Instrument and InstrumentSpec, which means that an Instrument is partly made up of an InstrumentSpec. There are also the uncolored arrows which mean generalization, in our case, that Guitar and Mandolin are specific instrument, this, Instrument is the general case of both. I also added the Stylen ‘enum’, which is a specific attribute that belongs to a MandolinSpec.

Enough with drawings, let’s get to the code!

First thing we need to change is the Guitar class into an Instrument class that will be inherited by Guitar and Mandolin afterwards. For the sake of space economy, I’ve put all of them in a single file named instrument.rb:


class Instrument

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

end

class Guitar < Instrument
  def initialize(serial_number,price,spec)
    super
  end

  def to_s
    "We have a #{@spec.builder} #{@spec.model} #{@spec.num_strings}-string #{@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

class Mandolin < Instrument
  def initialize(serial_number,price,spec)
    super
  end
  
  def to_s
    "We have a #{@spec.builder} #{@spec.model} #{@spec.style}-style #{@spec.type} mandolin: \n #{@spec.back_wood} back and sides, \n #{@spec.top_wood} top. You can have it for only $#{@price}!\n -------- \n"
  end
end

The code is, as usual, very expressive, but it’s worth mentioning is the ‘super’ method which invokes the parent method of the specific method it’s being called in. In this case the initialize method for both child classes are being exactly the same as the parent class, this is due to the fact that in Ruby we don’t have to declare variable types, or else both constructors would have different spec type variables.
An important side note to make here is that in the book, the Instrument class is an abstract class, which, by definition, can’t be instantiated. But in Ruby we don’t have such thing, and a way to fake out that behavior would be to raise an exception in the Instrument class constructor saying it can’t be instantiated, but we’ll skip that for now.

Moving on to the changes in GuitarSpec, which becomes InstrumentSpec with 2 child classes: GuitarSpec and MandolinSpec:


class InstrumentSpec
  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

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

class GuitarSpec < InstrumentSpec
  attr_reader :num_strings

  def initialize(builder, model, type, back_wood, top_wood, num_strings)
    super(builder, model, type, back_wood, top_wood)
    @num_strings = num_strings
  end

  def matches(search)
    super && self.class==search.class && @num_strings==search.num_strings
  end
  
end

class MandolinSpec < InstrumentSpec
  attr_reader :style

  def initialize(builder, model, type, back_wood, top_wood, style)
    super(builder, model, type, back_wood, top_wood)
    @style = style
  end

  def matches(search)
    super && self.class==search.class && @style==search.style
  end

end

Same deal as the last code snippet: we moved all the general attributes up to a parent class and specified what needs to be specified. The ‘super’ appears again, in this case, the parent ‘matches’ method has to return true for the rest of the child classes’ ‘matches’ method to be true.

Moving up the chain to the Inventory class:


class Inventory
  
  def initialize
    @instruments = []
  end

  def add_instrument(serial_number,price,spec)
    klass = case spec
            when GuitarSpec then Guitar
            when MandolinSpec then Mandolin
            end
    @instruments << klass.new(serial_number,price,spec)
  end

  def get_instrument(serial_number)
    @instruments.find { |instrument| instrument.serial_number == serial_number}
  end

  def search(instrument_spec)
    @instruments.find_all{ |instrument| instrument.spec.matches(instrument_spec) }
  end
end

Notice the change in the search method compared to the one in chapter 1, turns out find_all returns an array of objects that match the condition in the block. There are also small changes in variable names and that case condition that creates a different instrument depending on the spec’s class. Again, due to Ruby’s dynamic typing we’re saving ourselves alot of extra code, such as a new search method for each instrument type (which would be necessary in Java).

Before we can test, let’s just add a new attribute:

#other Attributes up here
class Style
  VALUES = ['F']
end

[Type, Builder, Wood, Style].each do |att|
#rest of the code down here

Now we’re all set to test our new design:

require 'instrument'
require 'inventory'
require 'attributes'
require 'instrument_spec'

def initialize_inventory(inventory)
  guitar = GuitarSpec.new(Builder::FENDER, "Stratocastor", Type::ELECTRIC, Wood::ALDER, Wood::ALDER, 6)
  inventory.add_instrument('V95693', '1499.95'.to_f, guitar)
  inventory.add_instrument('V9512', '1549.95'.to_f, guitar)
  mandolin = MandolinSpec.new(Builder::MARTIN, "Stratocastor", Type::ACOUSTIC, Wood::ALDER, Wood::ALDER, Style::F)
  inventory.add_instrument('V98834', '1299.95'.to_f, mandolin)
  inventory.add_instrument('V99712', '1349.95'.to_f, mandolin)
end

inventory = Inventory.new

initialize_inventory(inventory)

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

what_joseph_likes = MandolinSpec.new(Builder::MARTIN, "Stratocastor", Type::ACOUSTIC, Wood::ALDER, Wood::ALDER, Style::F)

guitars = inventory.search(what_erin_likes)

mandolins = inventory.search(what_joseph_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

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

Little (real little) things have changed in this code, and our store now supports both guitars and mandolins!

Erin, you might like these guitars:
We have a Fender Stratocastor 6-string electric guitar:
Alder back and sides,
Alder top. You can have it for only $1499.95!
——–
We have a Fender Stratocastor 6-string electric guitar:
Alder back and sides,
Alder top. You can have it for only $1549.95!
——–
Joseph, you might like these mandolins:
We have a Martin Stratocastor F-style acoustic mandolin:
Alder back and sides,
Alder top. You can have it for only $1299.95!
——–
We have a Martin Stratocastor F-style acoustic mandolin:
Alder back and sides,
Alder top. You can have it for only $1349.95!
——–

But what if our client wanted to add bass guitars, banjos, dobros and fiddles to his shop? We’d need to basically change the whole code structure, such as adding new instrument classes, new instrument spec classes and changing the add_instrument method. In short, we’d take a lot of time to change it, which means bad flexibility. But that’s fine, this is the way to find out if your application is well-designed or not, and make improvements to it case it isn’t (which is our case).

Let’s flex our OO muscles and review some important concepts to better design what we have.

  • Interface: Coding to an interface, rather than to an implementation, makes your software easier to extend. By coding to an interface, your code will work with all the interface’s subclasses–even the ones that haven’t been created yet.
  • Encapsulation: Duplicate code is something encapsulation prevents, but another aspect of encapsulation is that you get to protect your classes from unnecessary changes. The key is to encapsulate what varies.
  • Change: The easiest way to make your software resilient to change is to make sure each class has only one reason to change, this way you avoid classes that tries to do too many things.

Now we can apply those concepts to our current software, taking it to the next level in design.

The first and most obvious flaw in our current design is the fact that we’re having specific classes for each instrument, which is mostly affecting the add_instruments method (in the Java version it would affect the multiple search() method too) with a bunch of implementation-specific code. The thing is: classes are about behavior. Does a Banjo function differently from a Guitar in the perspective of our application? No! They’re all instruments that have specifications, so it doesn’t really matter to us what kind of instrument it is, it just matters that it is an instrument.
In practice, this means turning the ‘abstract’ Instrument class into a concrete one. We can then create another attribute named InstrumentType to tell us which instrument we’re dealing with. So this is what we get, first the Instrument class:


class Instrument

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

end

So now the Inventory#add_instrument method becomes:

def add_instrument(serial_number,price,spec, instrument_type)
  @instruments << Instrument.new(serial_number,price,spec, instrument_type)
end

We applied the first of the 3 mentioned OO concepts, let’s move on to the next: encapsulation. Yes, we did already apply encapsulation in the first chapter when we moved the guitar specifications to a GuitarSpec class to avoid duplicate code since both clients and instruments needed them, so now we’re gonna apply a second encapsulation that will also follow the ‘encapsulate what varies’ concept. The Mandolin, as you saw, has a specific ‘style’ attribute, while a guitar has a specific ‘num_strings’ attribute, and so on, so, basically, instrument attributes are varying between instruments which causes us to create subclasses for instrument specs such as GuitarSpec and MandolinSpec.

Instead of all that tiresome work, we can cut everything up from InstrumentSpec and put it into a hash! This way each InstrumentSpec will have its own set of properties, without having to associate with any subclasses, this is how it will work out:

class InstrumentSpec
  attr_reader :properties
  
  def initialize(properties)
    @properties = properties
  end

  def matches(search)
    search.properties.each { |property, value| return false if value != @properties[property] }
  end

end

This way any new properties can be added without messing with this class, and we can avoid the awful massive comparison code by simply comparing hash values. Notice that this new matches method focuses on the provided spec to be searched, meaning that even if it has a total of only 2 properties but those properties match the ones in inventory specs, matches() will return true.

Time to put our newly designed application to the test:

require 'instrument'
require 'inventory'
require 'attributes'
require 'instrument_spec'

def initialize_inventory(inventory)
  guitar = InstrumentSpec.new(:instrument_type => InstrumentType::GUITAR, :builder => Builder::FENDER, :model => "Stratocastor", :type => Type::ELECTRIC, :back_wood => Wood::ALDER, :top_wood => Wood::ALDER, :num_strings => 6)
  inventory.add_instrument('V95693', 1499.95, guitar)
  mandolin = InstrumentSpec.new(:instrument_type => InstrumentType::MANDOLIN, :builder => Builder::MARTIN, :model => "F-5G", :type => Type::ACOUSTIC, :back_wood => Wood::ALDER, :top_wood => Wood::ALDER, :style => Style::F)
  inventory.add_instrument('V98834', 1299.95, mandolin)
  banjo = InstrumentSpec.new(:instrument_type => InstrumentType::BANJO, :builder => Builder::GIBSON, :model => "RB-3 Wreath", :back_wood => Wood::ALDER, :num_strings => 5)
  inventory.add_instrument('8900231', 2945.95, banjo)
end

inventory = Inventory.new
initialize_inventory(inventory)
client_spec = InstrumentSpec.new(:back_wood => Wood::ALDER)
matching_instruments = inventory.search(client_spec)

unless matching_instruments.empty?
  puts "You might like these instruments: \n"
  matching_instruments.each do |instrument|
    puts "We have a #{instrument.spec.properties[:instrument_type]} with the following properties:"
    instrument.spec.properties.each { |property,value| puts "#{property.to_s.gsub('_',' ').capitalize}: #{value}" }
    puts "You can have this #{instrument.spec.properties[:instrument_type]} for $#{instrument.price}\n---\n"
  end
else
  puts "Sorry, we have nothing for you."
end

The client strangely wants any instrument that has Alder wood in it (big Alder fan), and since we have 3 different types of instruments with Alder in it, all three of them are returned for the client to choose from:

You might like these instruments:
We have a Guitar with the following properties:
Type: Electric
Instrument type: Guitar
Model: Stratocastor
Top wood: Alder
Num strings: 6
Builder: Fender
Back wood: Alder
You can have this Guitar for $1499.95

We have a Mandolin with the following properties:
Type: Acoustic
Instrument type: Mandolin
Model: F-5G
Top wood: Alder
Style: F
Builder: Martin
Back wood: Alder
You can have this Mandolin for $1299.95

We have a Banjo with the following properties:
Instrument type: Banjo
Model: RB-3 Wreath
Num strings: 5
Builder: Gibson
Back wood: Alder
You can have this Banjo for $2945.95

Our software is now so flexible that all a programmer will have to do to add new instruments or instrument properties is basically alter the attributes file, adding new data to it. Just to keep a clean conscience, here’s the class diagram for the finalized version of our Instrument Store:
Chapter 5 - Diagram 4

An important thing in software design is knowing when to stop, I mean, how will we know when we’ve done enough? The answer is simple: when it’s doing what your client wants and when it’s flexible enough to be changeable without much effort. When those things are done with, it’s time to move on, so see you next chapter!