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.

New chapter, new review, new project.

This second chapter focuses on the importance of making sure you’re doing what your clients want by using requirements and use cases. On account to a new chapter, we’ll also be doing a new example application that will simulate a dog door. Our clients want a dog door for their dog Fido that will open and close in response to a remote control, and it’s our duty to satisfy them

We’ll start simple by creating the DogDoor class, which will be responsible for opening and closing the door.

class DogDoor
  attr_reader :is_open

  def initialize
    @is_open = false
  end

  def open
    puts "The dog door opens"
    @is_open = true
  end

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

Of course we’re not actually gonna put code that will integrate this with an actual dog door, we’ll only be simulating the process by outputting status messages. Anyway, now we have to deal with our Remote class, which will be responsible for interacting with a dog door.

class Remote

  def initialize(door)
    @door = door
  end

  def press_button
    puts "Pressing the remote control button..."
    @door.is_open ? @door.close : @door.open
  end

end

Pretty straight forward and expressive code. With this done, we can now test our little system on our main.rb file:

require 'dog_door.rb'
require 'remote.rb'

door = DogDoor.new

remote = Remote.new(door)

puts "Fido barks to go outside..."

remote.press_button

puts "Fido has gone outside..."

remote.press_button

puts "Fido's all done..."

remote.press_button

puts "Fido's back inside"

remote.press_button

By doing the ‘happy path’, everything seems to work out, but what if our client forgets to close the door? Then it’d stay open and a lot of other animals could get in the house. So, basically, when things don’t go as expected, we have a software that doesn’t do what the customer wants it do, despite the fact that it’s not our problem it’s not working, which leaves us stuck in phase 1 to building ‘great software’, as learned in chapter 1.

To avoid this kind of thing from happening, the best thing is to talk to our clients and let them feed us requirements, which, in singular, means a single need detailing what a particular product or service should be or do. After some talk with our clients, we get the following requirements:

  1. The dog door must be at least 12″ (enough for Fido to get through) tall. (this doesn’t really influence on our software, but it’s still a requirement and should be pointed out)
  2. A button on the remote control opens the dog door if the door is closed, and closes the dog door if the door is open
  3. Once the dog door has opened, it should close automatically if the door isn’t already closed.

So we’ve written down what our client wants the dog door to do, but it’s our job to make it work. To do this we have to imagine every possible scenario so that our software will always work no matter what, and so we write use cases to describe what our system will do to accomplish a particular customer goal:

  1. Fido (the dog) barks to be let out.
  2. Fido is heard barking by his owners.
  3. The remote control button is pressed.
  4. The dog door opens.
  5. Fido goes outside.
  6. Fido does his business.
    1. The door shuts automatically.
    2. Fido barks to be let back inside.
    3. Fido is heard barking again.
    4. The remote control is pressed again.
    5. The dog door opens again.
  7. Fido goes back inside.
  8. The door shuts automatically.

Notice we even described an alternate path where Fido takes too long to go back inside. Going a little more technical, there are 3 core concepts about use cases:

  1. Clear value: every use case must clearly help a customer achieve a goal. (in our example, the value is that our clients can control their dog door with a remote)
  2. Start and Stop: every use case must have a definite condition that starts and another one that ends it. (step 1 and 8 in our example)
  3. External Initiator: every use case must have something outside of the system that starts it off. (Fido, the dog, in our example)

Now that we have both our requirements and our use cases, we can finally move on to coding again, and this time we’ll know that it will satisfy our customers. Since the first requirement doesn’t have to do with software and that we already have the second one coded, we’ll basically have only to code the third requirement. On the Remote class:

class Remote

  def initialize(door)
    @door = door
  end

  def press_button
    puts "Pressing the remote control button..."
    if @door.is_open
      @door.close
    else
      @door.open
      Thread.new do
        sleep 5
        @door.close
      end
    end
  end

end

The native Java code uses Timer and TimerTask to tell the code to wait a certain time before executing something, but behind doors it uses threads. Since Ruby doesn’t have a Timer neither a TimerTask native class to help us out here, I simply used threads. The press_button method checks if the door is closed, and if it is, opens it and starts a new thread that will sleep for 5 seconds and then tell the door to close.

This code alone is flawed, the main thread will create a new alternate thread and simply terminate everything when it (the main thread) ends, which is when it reaches the end of its code, therefore terminating execution, so the alternate thread that was sleeping will simply die along with the main thread, and the @door.close method will never execute. This happens due to Ruby’s native way of dealing with threads, where in Java it’s possible to assign a thread as a ‘main’ thread so the JVM won’t terminate until it ends, in Ruby we don’t have that facility and must work our way around. The best (and most organized) way to do this in Ruby would be to have a threadpool, where we’d be constantly checking all thread status aside from our main code. But we don’t have the need for all that, so this is how I handled it:

require 'dog_door.rb'
require 'remote.rb'

door = DogDoor.new

remote = Remote.new(door)

puts "Fido barks to go outside..."

thread = remote.press_button

puts "Fido has gone outside..."


puts "Fido's all done..."


puts "Fido's back inside"

while thread.alive?
  sleep(0.1)
end

Press_button is returning the alternate thread, remember? Turns out we can use that reference and loop around the alive? method that every Thread has, and, inside that loop, tell the main thread to sleep until the alternate thread dies. Putting thread managing code in the middle of a simulator like this is indeed ugly, but, for the sake of simplicity, we’ll let this one pass. Who would know that in our humble learning experience we’d find a Ruby limitation?

Aside from the coding detail, it seems that we got the main path of our use case working. But what if Fido stays outside playing instead of coming back in? We haven’t simulated the alternate path, so lets do that now:

require 'dog_door.rb'
require 'remote.rb'

door = DogDoor.new

remote = Remote.new(door)

puts "Fido barks to go outside..."

thread = remote.press_button

puts "Fido has gone outside..."


puts "Fido's all done..."

sleep(3)

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

puts "\nFido starts barking..."

puts "...so the remote control is grabbed."

thread = remote.press_button


puts "Fido's back inside"

while thread.alive?
  sleep(0.1)
end

Is it working? This is the output:

Fido barks to go outside…
Pressing the remote control button…
The dog door opens.

Fido has gone outside…
Fido’s all done…
The dog door closes.

…but he’s stuck outside!

Fido starts barking…
…so the remote control is grabbed.
Pressing the remote control button…
The dog door opens.

Fido’s back inside
The dog door closes.

Even in the alternate path our dog door held on beautifully, and the customer is even more satisfied that we cared enough to actually think of an alternate path. Happy customers, happy us. See you in the next chapter.