Learning the Abstract Factory Pattern: An Example

Suppose I have been given the task of designing a computer system to display and print shapes from a database. The type of resolution to use to display and print the shapes depends on the computer that the system is currently running on: the speed of its CPU and the amount of memory that it has available. My system must be careful about how much demand it is placing on the computer.

The challenge is that my system must control the drivers that it is using: low-resolution drivers in a less-capable machine and high-resolution drivers in a high-capacity machine, as shown in Table 10-1.

Table 10-1. Different Drivers for Different Machines
For driver… In a low-capacity machine, use… In a high-capacity machine, use…
Display LRDDHRDD
 Low-resolution display driver High-resolution display driver
PrintLRPDHRPD
 Low-resolution print driverHigh-resolution print driver

In this example, the families of drivers are mutually exclusive, but this is not usually the case. Sometimes, different families will contain objects from the same classes. For example, a mid-range machine might use a low-resolution display driver (LRDD) and a high-resolution print driver (HRPD).

The families to use are based on the problem domain: which sets of objects are required for a given case? In this case, the unifying concept focuses on the demands that the objects put on the system:

  • A low-resolution family— LRDD and LRPD, those drivers that put low demands on the system

  • A high-resolution family— HRDD and HRPD, those drivers that put high demands on the system

My first attempt might be to use a switch to control the selection of driver, as shown in Example 10-1.

Example 10-1. Java Code Fragments: A Switch to Control Which Driver to Use
// JAVA CODE FRAGMENT

class ApControl {
   .  .  .
  void doDraw () {
     .  .  .
    switch (RESOLUTION) {
      case LOW:
        // use lrdd
      case HIGH:
        // use hrdd
    }
  }
  void doPrint () {
     .  .  .
    switch (RESOLUTION) {
     case LOW:
        // use lrpd
      case HIGH:
        // use hrpd
    }
  }
}

While this does work, it presents problems. The rules for determining which driver to use are intermixed with the actual use of the driver. There are problems both with coupling and with cohesion:

  • Tight coupling— If I change the rule on the resolution (say, I need to add a MIDDLE value), I must change the code in two places that are otherwise not related.

  • Low cohesion— I am giving doDraw and doPrint two unrelated assignments: they must both create a shape and must also worry about which driver to use.

Tight coupling and low cohesion may not be a problem right now. However, they usually increase maintenance costs. Also, in the real world, I would likely have many more places affected than just the two shown here.

Switches may indicate a need for abstraction.

Often, a switch indicates (1) the need for polymorphic behavior, or (2) the presence of misplaced responsibilities. Consider instead a more general solution such as abstraction or giving the responsibility to other objects.


Another alternative would be to use inheritance. I could have two different ApControls: one that uses low-resolution drivers and one that uses high-resolution drivers. Both would be derived from the same abstract class, so common code could be maintained. I show this in Figure 10-1.

Figure 10-1. Alternative 2—handling variation with inheritance.


While inheritance could work in this simple case, it has so many disadvantages that I would rather stay with the switches. For example:

  • Combinatorial explosion— For each different family and each new family I get in the future, I must create a new concrete class (that is, a new version of ApControl).

  • Unclear meaning— The resultant classes do not help clarify what is going on. I have specialized each class to a particular special case. If I want my code to be easy to maintain in the future, I need to strive to make it as clear as possible what is going on. Then, I do not have to spend a lot of time trying to relearn what that section of code is trying to do.

  • Need to favor composition— Finally, it violates the basic rule to “favor composition over inheritance.”

In my experience, I have found that switches often indicate an opportunity for abstraction. In this example, LRDD and HRDD are both display drivers and LRPD and HRPD are both print drivers. The abstractions would therefore be display drivers and print drivers. Figure 10-2 shows this conceptually. I say “conceptually” because LRDD and HRDD do not really derive from the same abstract class.

Figure 10-2. Drivers and their abstractions.


Note: At this point, I do not have to be concerned that they derive from different classes because I know I can use the Adapter pattern to adapt the drivers, making it appear they belong to the appropriate abstract class.


Defining the objects this way would allow for ApControl to use a DisplayDriver and a PrintDriver without using switches. ApControl is much simpler to understand because it does not have to worry about the type of drivers it has. In other words, ApControl would use a DisplayDriver object or a PrintDriver object without having to worry about the driver's resolution.

See Figure 10-3 and the code in Example 10-2.

Figure 10-3. ApControl using drivers in the ideal situation.


Example 10-2. Java Code Fragments: Using Polymorphism to Solve the Problem
// JAVA CODE FRAGMENT

class ApControl {
   .  .  .
  void doDraw () {
     .  .  .
    myDisplayDriver.draw();
  }
  void doPrint () {
     .  .  .
    myPrintDriver.print();
  }
}

One question remains: How do I create the appropriate objects?

I could have ApControl do it, but this can cause maintenance problems in the future. If I have to work with a new set of objects, I will have to change ApControl. Instead, if I use a “factory” object to instantiate the objects I need, I will have prepared myself for new families of objects.

In this example, I will use a factory object to control the creation of the appropriate family of drivers. The ApControl object will use another object—the factory object—to get the appropriate type of display driver and the appropriate type of print driver for the current computer being used. The interaction would look something like the one shown in Figure 10-4.

Figure 10-4. ApControl gets its drivers from a factory object.


From ApControl's point of view, things are now pretty simple. It lets ResFactory worry about keeping track of which drivers to use. Although I am still faced with writing code to do this tracking, I have decomposed the problem according to responsibility. ApControl has the responsibility for knowing how to work with the appropriate objects. ResFactory has the responsibility for deciding which objects are appropriate. I can use different factory objects or even just one object (that might use switches). In any case, it is better than what I had before.

This creates cohesion: all that ResFactory does is create the appropriate drivers; all ApControl does is use them.

There are ways to avoid the use of switches in ResFactory itself. This would allow me to make future changes without affecting any existing factory objects. I can encapsulate a variation in a class by defining an abstract class that represents the factory concept. In the case of ResFactory, I have two different behaviors (methods):

  • Give me the display driver I should use.

  • Give me the print driver I should use.

ResFactory can be instantiated from one of two concrete classes and derived from an abstract class that has these public methods, as shown in Figure 10-5.

Figure 10-5. The ResFactory encapsulates the variations.


Strategies for bridging analysis and design.

Below are three key strategies involved in the Abstract Factory.

Strategy Shown in the Design
Find what varies and encapsulate it. The choice of which driver object to use was varying. So, I encapsulated it in ResFactory.
Favor composition over inheritance. Put this variation in a separate object—ResFactory—and have ApControl use it as opposed to having two different ApControl objects.
Design to interfaces, not to implementations. ApControl knows how to ask ResFactory to instantiate drivers—it does not know (or care) how ResFactory is actually doing it.


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

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