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.

The fourth chapter is all about analysis. It’s easy to plan our software in our heads where every thing is perfect and nothing goes wrong, but we’re developing things that will be used in the real world, where lots of things go wrong. Analysis will help us adapt our system to the real-world context.

In chapter 3 we focused on changing our software to our client’s new needs which was making the door open when his dog barked, but where we went missing was that we made it recognize any bark instead of the owner’s dog specific barks. You see, it’s the kind of thing that you don’t realize is important until you stop to think about it, I mean, you don’t plan in your head that your client lives in a neighborhood full of other dogs and that they can bark and enter your home too, but this is why we’re taking a deeper peek in analysis.

We start by identifying the problem, which is that our door is letting any dog in, when it isn’t supposed to. Then we move on to planning a solution, which mainly means changing one step of our use case, from this:

Main Path Alternate Paths
1. Fido barks to be let out.
2. The bark recognizer “hears” a bark. 2.1. Fido’s owners hear it barking.
3. The bark recognizer sends a request to the door to open. 3.1. Fido’s owners press the button on the remote.
4. The dog door opens.
5. Fido goes outside.
6. Fido does his business.
6.1. The door shuts automatically.
6.2. Fido barks to be let back inside
6.3. The bark recognizer “hears” a bark (again). 6.3.1. Fido’s owners hear it barking (again).
6.4. The bark recognizer sends a request to the door to open. 6.4.1. Fido’s owners press the button on the remote.
6.5. The dog door opens (again).
7. Fido goes back inside.
8. The door shuts automatically.

To this:

Main Path Alternate Paths
1. The owner’s dog barks to be let out.
2. The bark recognizer “hears” a bark. 2.1. The owner hears it barking.
3. If it’s the owner’s dog barking, the bark recognizer sends a request to the door to open. 3.1. The owner presses the button on the remote.
4. The dog door opens.
5. The owner’s dog goes outside.
6. The owner’s dog does his business.
6.1. The door shuts automatically.
6.2. The owner’s dog barks to be let back inside
6.3. The bark recognizer “hears” a bark (again). 6.3.1. The owner hears it barking (again).
6.4. If it’s the owner’s dog barking, the bark recognizer sends a request to the door to open. 6.4.1. The owner presses the button on the remote.
6.5. The dog door opens (again).
7. The owner’s dog goes back inside.
8. The door shuts automatically.

Notice we also changed all subject names in order to point out that this will work for any client isntead of a specific one. But that change is not all, our analysis also tells us that we’ll be needing to add another use case to our system with the purpose of describing how we will store a specific dog bark which is a completely different goal from the one we have so far, which is getting the dog in and out of the house.

  1. The owner’s dog barks “into” the dog door.
  2. The dog door stores the owner’s dog’s bark.

So we know what we have to do but need to find out the best way to do it. This is what we got so far:
Chapter 4 - Diagram 1
We could simply add a string named allowedBark to our DogDoor class and use that to compare/set barks, but would that be flexible? What if we changed the comparison to MP3 files instead of strings? We’d have to keep changing the whole code structure over and over again. It’s obvious that we’re gonna have create a Bark class and delegate that comparison:
Chapter 4 - Diagram 2

Okay, we’re ready to go! Nope, we’re not. We have to code to the real world, thus, in the real world, does a dog have a single bark? No! It barks differently all the time, specially when it has to go to the bathroom. With that in our minds, let’s make the DogDoor class hold multiple Bark objects:
Chapter 4 - Diagram 3
The [*] means a lot, in this case, a DogDoor will store a lot of barks. It looks all easy when we see it all done, but how the heck are we supposed to know when to figure all these real world things out by ourselves? Easy, it’s all in the use case.
Notice how on step 3 of our main path we described “If its the owner’s dog barking”, we’re talking about a whole dog, and not just a single bark. Yes, it would make sense to create a Dog class, but why would we need a whole dog on our system if his barking is all we care about? Since we got started on it, let’s talk about the whole concept of textual analysis.

Textual analysis is the art of looking at all the nouns and verbs in your use case to figure out classes and methods. For example, if we analyse our current use case for nouns, we’ll get the following results: the (owner’s) dog, bark recognizer, dog door, the owner, request, remote control, the button, inside/outside, bark. As you can see, most of these are our current classes, and this is the whole point of textual analysis, by doing it we can figure out, for instance, that we needed a bark class. Of course we don’t have to be 100% loyal to it, just to what makes sense(button is a part of remote, it doesn’t need to be a whole class). Since textual analysis is such a powerful tool to help us write software, it’s important to always make sure it clearly and accurately explain what a system does so we can ease out the whole process.

Okay, we’re all set, let’s represent this in a more complete class diagram:
Chapter 4 - Diagram 4

Notice I made use of associations now to better represent the relations between classes. So all we have left to do now is the code, so let’s get to it by first creating our bark class:

class Bark
  attr_reader :sound

  def initialize(sound)
    @sound = sound
  end
  
  def equals(bark)
    self.sound==bark.sound
  end

end

The only thing that matters to us in a bark is it’s sound, so we can use it to compare the whole object to another bark object. Moving up the chain to the DogDoor class:

class DogDoor
  attr_reader :is_open, :allowed_barks

  def initialize
    @is_open = false
    @allowed_barks = []
  end

  def open
    puts "The dog door opens.\n\n"
    @is_open = true
    Thread.new do
        sleep 2
        self.close
      end
  end

  def close
    puts "The dog door closes.\n\n"
    @is_open = false
  end

  def add_allowed_bark(bark)
    @allowed_barks << bark
  end

end

Basically the same thing from the last chapter, except that we added a new instance array of allowed barks and a method to add to that array. Not much to discuss here, let’s move up the chain again to the BarkRecognizer:

class BarkRecognizer

  def initialize(door)
    @door = door
  end

  def recognize(bark)
    puts "Bark recognizer: Heard a '#{bark.sound}'"
    if @door.allowed_barks.find {|allowed| allowed.equals(bark)}
      @door.open
    else
      puts "This dog is not allowed."
    end
  end

end

Notice the simplicity? Its all due to internal iterators, one of Ruby’s beauties. Instead of using an external iterator such as a loop going through each allowed bark, why don’t we pass that duty to the allowed_barks array itself? Let it iterate over itself instead! If the Array#find method finds a match to the condition (which we delegated to the Bark class) inside the block it returns the object (which can be interpreted as true), else it returns nil, which is false, meaning the barking dog isn’t allowed.
We’ve got our backend all set then! Let’s code up our simulator:

require 'dog_door.rb'
require 'remote.rb'
require 'bark_recognizer.rb'
require 'bark.rb'

door = DogDoor.new

door.add_allowed_bark(Bark.new('rowlf'))
door.add_allowed_bark(Bark.new('rooowlf'))
door.add_allowed_bark(Bark.new('rawlf'))
door.add_allowed_bark(Bark.new('woof'))


remote = Remote.new(door)

recognizer = BarkRecognizer.new(door)

#Simulate the hardware hearing a bark
puts "Fido starts barking."
thread = recognizer.recognize(Bark.new("rowlf"))

puts "Fido has gone outside..."

puts "Fido's all done..."

sleep(3)

puts "...but he's stuck outside!"

#Simulate the hardware hearing a bark that isn't Fido
small_bark = Bark.new("yip")
thread = recognizer.recognize(small_bark)

sleep(3)

#Simulate the hardware hearing a bark again
puts "\nFido starts barking..."
thread = recognizer.recognize(Bark.new("rooowlf"))

puts "Fido's back inside"

while thread.alive?
  sleep(0.1)
end

We now have a working software that is ready to be delivered to the real world, and it all happened so easily due to the good practice of use cases and text analysis. If you started reading from here and got lost with all this code that I didn’t bother explain, be sure to check chapter 2 and chapter 3, where I did explain them. And for the people who were following since the last chapters, yes, this is the final version of our Dog Door system.

About these ads