Chapter 14. Easier Object Construction with the Builder

I remember the day we bought my son his first bike. The morning went well enough—the drive to the store, finding the right size, and the lengthy but critical process of selecting the right color. Then there was the middle phase of getting the bike home and getting everything out of the box. Of course, some assembly was required. Perhaps more than some. What followed our happy arrival home was the third phase, the phase that took all afternoon and involved a descent into frustration and multiple skinned knuckles. I spent hours trying to pull together a minor junkyard of parts according to instructions that would have baffled the entire National Security Agency. As it turned out, picking that bike was the easy part; putting it together was the real challenge.

Objects can be like that, too. Chapter 13 was all about using factories to lay your hands on the right kind of object. But sometimes getting hold of the right object is not the main problem. No, sometimes the problem is in configuring the object.

In this chapter, we will look at the Builder pattern, a pattern designed to help you configure those complex objects. We will see that, as you might expect, there is a fair bit of overlap between builders and factories. We will also look at magic methods, a Ruby technique that can make your builders even easier to use. In addition, we will consider the issues that arise in reusing a builder. Finally, we will see how the Builder pattern can help you avoid accidentally creating invalid objects and perhaps even help you create valid ones.

Building Computers

Imagine that you are writing a system that will support a small computer manufacturing business. Each machine is custom made to order, so you need to keep track of the components that will go into each machine. To keep things simple, suppose each computer is made up of a display, a motherboard, and some drives:


   class Computer
     attr_accessor :display
     attr_accessor :motherboard
     attr_reader   :drives

     def initialize(display=:crt, motherboard=Motherboard.new, drives=[])
       @motherboard = motherboard
       @drives = drives
       @display = display
     end
   end


The display is easy; it is either a :crt or an :lcd. The motherboard is a whole object in itself; it has a certain amount of memory and holds either an ordinary CPU or a superfast turbo processor:


   class CPU
     # Common CPU stuff...
   end

   class BasicCPU < CPU
     # Lots of not very fast CPU-related stuff...
   end

   class TurboCPU < CPU
     # Lots of very fast CPU stuff...
   end

   class Motherboard
     attr_accessor :cpu
     attr_accessor :memory_size
   def initialize(cpu=BasicCPU.new, memory_size=1000)
     @cpu = cpu
     @memory_size = memory_size
    end
   end


The drives, which come in three flavors (hard drive, CD, and DVD) are modeled by the Drive class:


   class Drive
     attr_reader :type # either :hard_disk, :cd or :dvd
     attr_reader :size # in MB
     attr_reader :writable # true if this drive is writable

     def initialize(type, size, writable)
       @type = type
       @size = size
       @writable = writable
     end
   end


Even with this somewhat simplified model of, constructing a new instance of Computer is painfully tedious:


   # Build a fast computer with lots of memory...

   motherboard = Motherboard.new(TurboCPU.new, 4000)

   # ...and a hard drive, a CD writer, and a DVD

   drives = []
   drives << Drive.new(:hard_drive, 200000, true)
   drives << Drive.new(:cd, 760, true)
   drives << Drive.new(:dvd, 4700, false)

   computer = Computer.new(:lcd, motherboard, drives)


The very simple idea behind the Builder pattern is that you take this kind of construction logic and encapsulate it in a class all of its own. The builder class takes charge of assembling all of the components of a complex object. Each builder has an interface that lets you specify the configuration of your new object step by step. In a sense, a builder is sort of like a multipart new method, where objects are created in an extended process instead of all in one shot. A builder for our computers might look something like this:


   class ComputerBuilder
     attr_reader :computer

     def initialize
       @computer = Computer.new
     end

     def turbo(has_turbo_cpu=true)
       @computer.motherboard.cpu = TurboCPU.new
     end

     def display=(display)
       @computer.display=display
     end

     def memory_size=(size_in_mb)
       @computer.motherboard.memory_size = size_in_mb
     end

     def add_cd(writer=false)
       @computer.drives << Drive.new(:cd, 760, writer)
     end

     def add_dvd(writer=false)
       @computer.drives << Drive.new(:dvd, 4000, writer)
     end

     def add_hard_disk(size_in_mb)
       @computer.drives << Drive.new(:hard_disk, size_in_mb, true)
     end
   end


The ComputerBuilder class factors out all of the details involved in creating an instance of Computer. To use it, you simply make a new instance of the builder and step through the process of specifying all the options that you need on your computer:


   builder = ComputerBuilder.new
   builder.turbo
   builder.add_cd(true)
   builder.add_dvd
   builder.add_hard_disk(100000)


Finally, you get the shiny new Computer instance from the builder:

   computer = builder.computer

Figure 14-1 shows the UML diagram for our basic builder.

Figure 14-1. A Builder

image

The GOF called the client of the builder object the director because it directs the builder in the construction of the new object (called the product). Builders not only ease the burden of creating complex objects, but also hide the implementation details. The director does not have to know the specifics of what goes into creating the new object. When we use the ComputerBuilder class, we can stay blissfully ignorant of which classes represent the DVDs or the hard disks; we just ask for the computer configuration that we need.

Polymorphic Builders

This chapter began by contrasting the Builder pattern with factories and saying that builders are less concerned about picking the right class and more focused on helping you configure your object. Factoring out all of that nasty construction code is the main motivation behind builders. Nevertheless, given that builders are involved in object construction, they also are incredibly convenient spots to make those "which class" decisions.

For example, imagine that our computer business expands into producing laptops along with traditional desktop machines. Thus we now have two basic kinds of products: desktop computers and laptops.


   class DesktopComputer < Computer
     # Lots of interesting desktop details omitted...
   end

   class LaptopComputer < Computer
     def initialize( motherboard=Motherboard.new, drives=[] )
       super(:lcd, motherboard, drives)
     end

     # Lots of interesting laptop details omitted...

   end


Of course, the components of a laptop computer are not the same as the ones you find in a desktop computer. Fortunately, we can refactor our builder into a base class and two subclasses to take care of these differences. The abstract base builder deals with all of the details that are common to the two kinds of computers:


   class ComputerBuilder
     attr_reader :computer

     def turbo(has_turbo_cpu=true)
       @computer.motherboard.cpu = TurboCPU.new
     end

     def memory_size=(size_in_mb)
       @computer.motherboard.memory_size = size_in_mb
     end
   end


The DesktopBuilder knows how to build desktop computers. In particular, it knows to create instances of the DesktopComputer class and it is aware that desktop computers use ordinary drives:


   class DesktopBuilder < ComputerBuilder
     def initialize
       @computer = DesktopComputer.new
     end

     def display=(display)
       @display = display
     end

     def add_cd(writer=false)
       @computer.drives << Drive.new(:cd, 760, writer)
     end

     def add_dvd(writer=false)
       @computer.drives << Drive.new(:dvd, 4000, writer)
     end

     def add_hard_disk(size_in_mb)
       @computer.drives << Drive.new(:hard_disk, size_in_mb, true)
     end
   end


By contrast, the laptop builder knows to create instances of LaptopComputer and to populate that object with instances of the special LaptopDrive:


   class LaptopBuilder < ComputerBuilder
     def initialize
       @computer = LaptopComputer.new
     end

     def display=(display)
       raise "Laptop display must be lcd" unless display == :lcd
     end

     def add_cd(writer=false)
       @computer.drives << LaptopDrive.new(:cd, 760, writer)
     end

     def add_dvd(writer=false)
       @computer.drives << LaptopDrive.new(:dvd, 4000, writer)
     end

     def add_hard_disk(size_in_mb)
       @computer.drives << LaptopDrive.new(:hard_disk, size_in_mb, true)
     end
   end


Figure 14-2 shows the UML diagram for our new, polymorphic builder. If you compare Figure 14-2 with the UML diagram for the abstract factory (Figure 13-2), you will see that the two patterns share a certainly family resemblance.

Figure 14-2. A polymorphic builder implementation

image

Alternatively, we could have written a single builder class that creates either a laptop or a desktop system depending on the value of a parameter.

Builders Can Ensure Sane Objects

In addition to making object construction easier, builders can make object construction safer. That final "give me my object" method makes an ideal place to check that the configuration requested by the client really makes sense and that it adheres to the appropriate business rules. For example, we might enhance our computer method to make sure that it has a sane hardware configuration:


     def computer
       raise "Not enough memory" if @computer.motherboard.memory_size < 250
       raise "Too many drives" if @computer.drives.size > 4
       hard_disk = @computer.drives.find {|drive| drive.type == :hard_disk}
       raise "No hard disk." unless hard_disk
       @computer
     end


Nor do we have to simply throw up our hands and raise an exception in the face of an incomplete configuration:


     # ...
     if ! hard_disk
       raise "No room to add hard disk." if @computer.drives.size >= 4
       add_hard_disk(100000)
     end
     # ...


The preceding code simply adds in a hard drive if there is room for one and the client did not specify one.

Reusable Builders

An important issue to consider when writing and using builders is whether you can use a single builder instance to create multiple objects. For example, you might quite reasonably expect that you could use a LaptopBuilder to create a couple of identical computers in one go:


   builder = LaptopBuilder.new
   builder.add_hard_disk(100000)
   builder.turbo

   computer1 = builder.computer
   computer2 = builder.computer


The trouble is, because the computer method always returns the same computer, both computer1 and computer2 end up being references to the same computer, which is probably not what you expected here. One way to deal with this issue is to equip your builder with a reset method, which reinitializes the object under construction:


   class LaptopBuilder

     # Lots of code omitted...

     def reset
       @computer = LaptopComputer.new
     end

   end


The reset method will let you reuse the builder instance, but it also means that you have to start the configuration process all over again for each computer. If you want to perform the configuration once and then have the builder produce any number of objects based on that configuration, you need to store all of the configuration information in instance attributes and create the actual product only when the client asks for it.

Better Builders with Magic Methods

Our computer builder is certainly an improvement over spreading all of that object creation, configuration, and validation code throughout your application. Unfortunately, even with the builder, the process of fitting out a new computer is less than elegant. As we have seen, you still have to create the builder and then call any number of methods to configure the new computer. The question is, can we make the process of configuring the new computer a bit terser and perhaps a shade more elegant?

One way we might do this is by creating a magic method. The idea behind a magic method is to let the caller make up a method name according to a specific pattern. For example, we might configure a new laptop with

   builder.add_dvd_and_harddisk

or perhaps even

   builder.add_turbo_and_dvd_and_harddisk

Magic methods are very easy to implement using the method_missing technique that we first met back when we were talking about proxies (in Chapter 10). To use a magic method, you simply catch all unexpected methods calls with method_missing and parse the method name to see if it matches the pattern of your magic method name:


     def method_missing(name, *args)
       words = name.to_s.split("_")
       return super(name, *args) unless words.shift == 'add'
       words.each do |word|
         next if word == 'and'
         add_cd if word == 'cd'
         add_dvd if word == 'dvd'
         add_hard_disk(100000) if word == 'harddisk'
         turbo if word == 'turbo'
       end
     end


This code breaks the method name up along the underscores and tries to make sense of it as a request to add various options to the computer.

The magic method technique is certainly not limited to builders. You can use it in any situation where you want to let client code specify multiple options succinctly.

Using and Abusing the Builder Pattern

The need for the Builder pattern sometimes creeps up on you as your application becomes increasingly complex. For example, in its early days your Computer class may have just tracked the CPU type and memory size. Using a builder for so simple a class would clearly be overdoing things. But as you enhanced the Computer class to model the drives, the number of options and the interdependences between those options suddenly explodes—and a build starts to make more sense. It is usually fairly easy to spot code that is missing a builder: You can find the same object creation logic scattered all over the place. Another hint that you need a builder is when your code starts producing invalid objects: "Oops, I checked the number of drives when I create a new Computer over here, but not over there."

As with factories, the main way that you can abuse the Builder pattern is by using it when you don’t need it. I don’t think it is a good idea to anticipate the need for a builder. Instead, let MyClass.new be your default way of creating new objects. Add in a builder only when cruel fate, or ever-escalating requirements, force your hand.

Builders in the Wild

One of the more interesting builders that you will find in the Ruby code base claims that it is not a builder at all. Despite its name, MailFactory[1] is a really nice builder that helps you create e-mail messages. Although e-mail messages are, at their heart, just chunks of plain text, anyone who has ever tried to construct a message with multipart MIME attachments knows that even plain text can be very complicated.

MailFactory propels you past all of this complication by providing a nice builder-style interface to create your message:


   require 'rubygems'
   require 'mailfactory'

   mail_builder = MailFactory.new
   mail_builder.to ='[email protected]'
   mail_builder.from = '[email protected]'
   mail_builder.subject = 'The document'
   mail_builder.text = 'Here is that document you wanted'
   mail_builder.attach('book.doc')


Once you have told MailFactory (builder!) all about your e-mail message, you can get the text of the message—the thing that you can ship off to an SMTP server—by calling the to_s method:


   puts mail_builder.to_s
   to: [email protected]
   from: [email protected]
   subject: Here is that document you wanted
   Date: Wed, 16 May 2007 14:02:32 -0400
   MIME-Version: 1.0
   Content-Type: multipart/mixed;
   boundary="--=_NextPart_3rj.Kbd9.t9JpHIc663P_4mq6"
   Message-ID: <[email protected]>

   This is a multi-part message in MIME format.
   ...


The most prominent examples of magic methods are the finder methods in ActiveRecord. In exactly the same way that our last computer builder allowed us to specify computer configurations with the name of the method we call, ActiveRecord allows us to encode database queries. You might, for instance, find all of the employees in the Employee table by Social Security number:

   Employee.find_by_ssn('123-45-6789')

Or you could search by first and last names:

   Employee.find_by_firstname_and_lastname('John', 'Smith')

Wrapping Up

The idea behind the Builder pattern is that if your object is hard to build, if you have to write a lot of code to configure each object, then you should factor all of that creation code into a separate class, the builder.

The Builder pattern suggests that you provide an object—the builder—that takes a multipart specification of your new object and deals with all the complexity and drudgery of creating that object. Builders, because they are in control of configuring your object, can also prevent you from constructing an invalid object. The builder is uniquely positioned to look over the client's shoulder and say, "No, I think a fifth wheel on that car may be a bit too much . . ."

With a little ingenuity, you can create magic methods to facilitate the building. To build a magic method, you catch arbitrary method calls to your object with method_missing, parse the names of those nonexistent methods, and build the right thing based on the name. Magic methods tend to take speedy object construction to the next level by allowing the client to specify a number of configuration options with a single method call.

When you create a builder, and especially when you use one, you need to be aware of the reusability issue. Can you use a single instance of a builder to create multiple instances of the product? It is certainly easier to write one-shot builders, or builders that need to be reset before reuse, than it is to create completely reusable builders. The question is this: Which kind of builder are you creating or using?

The Builder pattern is the last of the patterns that we will examine that is concerned with object creation.[2] In the next chapter, we will stop talking about creating new objects and go back to talking about doing something interesting with all those brand-new objects: create an interpreter.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.191.189.85