Sue's Burger Shop

Consider a fictional restaurant called Sue's Burger Shop. Let's assume for purposes of simplicity that the shop sells only burgers. Let's model this simply as a burger and start from there. Furthermore, I'm going to assume that you've been awake until this point and you understand that an interface for a base class is a great idea here. So let's model the basic burger in terms of a sandwich (operations such as purchase, eat, and so on are suppressed). See Figure 5.1.

Figure 5.1. The hamburger hierarchy


Now we decide that we want to sell Cheeseburgers as well (see Figure 5.2). Hmm, inheritance to the rescue…

Figure 5.2. Extending to include cheeseburgers


Wow! That was pretty easy; that is, until we have to support onions on the burger and on the cheeseburger. Let's use our trusty inheritance tool (see Figure 5.3) for that as well.

Figure 5.3. Adding the onions


Okay, having to add two classes is a little annoying, but fine, we can cope. The astute reader can guess what's coming next—tomatoes! So, using our tool du jour, inheritance, we now also support hamburger with tomato, hamburger with tomato and onion, cheeseburger with tomato, and cheeseburger with tomato and onion. See Figure 5.4.

Figure 5.4. Adding tomatoes, the hierarchy starts to become difficult


Four more classes is more than a little annoying! But now let's throw in lettuce. See Figure 5.5.

Figure 5.5. Throwing in the lettuce, we now make things more complex


Wow, eight more classes! How many classes do you think it would be when we add the option for ketchup? If you guessed 16, you are there! How about mayonnaise? 32! Now bacon, 64! Salsa, 128! What we are seeing is an exponential number of classes. Yes, each one is trivial, but consider a single operation added to the mix: getPrice(). If we needed to add 5 cents to the price of any cheese order, there might be hundreds of places that would potentially be affected. This is clearly a violation of the single point of maintenance we were striving for earlier. You may think this example is a stretch, but most developers have seen this sort of problem evolve. It is usually handled simply by not supporting the entire set of options. For example, it would be like telling customers they can have onions only on cheeseburgers, not on hamburgers.

Does it work like this in the real world? Usually not (although as we mentioned earlier, this happens more often than not in the coding world). Let's think about the problem a little differently; let's try to treat each of these toppings as just that, an add-on or decoration to the basic hamburger or, for that matter, to sandwiches in general.

Let's start again with our basic structure, but now let's allow a subclass of the sandwich that we will call a SandwichDecorator (see Figure 5.6). The decorator can simply decorate or wrap or top a sandwich so it will reference a sandwich. All the decorator will do is forward or delegate all operations to the sandwich that it wraps. In this case, getPrice() will simply call getPrice() of the wrapped sandwich.

Figure 5.6. The SandwichDecorator


This is pretty straighforward—this is also pretty useless. However, with this model let's consider the first topping: cheese. We use inheritance to indicate that it is a kind of decorator, as shown in Figure 5.7.

Figure 5.7. Adding the CheeseDecorator


Notice that the getPrice operation calls the SandwichDecorator, which simply calls getPrice on the sandwich it decorates (see Figure 5.8). So to create a cheeseburger, we would perform the following steps:

Figure 5.8. Interaction diagram for calling getPrice


Create a hamburger
BasicHamburger h;
Create a CheeseDecorator that decorates a hamburger
Sandwich s = new CheeseDecorator(h)

Calling getPrice on the sandwich would first call
CheeseDecorator::getPrice(),
   which would then call its base operation SandwichDecorator::getPrice,
        which would then call BasicHamburger getPrice() returning HAMBURGER_PRICE
         which would be added to CHEESE_PRICE,
   returning HAMBURGER_PRICE + CHEESE_PRICE.

Now let's extend this model to include onions, as shown in Figure 5.9.

Figure 5.9. Adding the OnionDecorator


At first it would seem that we would have to create an OnionDecorator as well as a CheeseAndOnionDecorator, but a CheeseDecorator inherits from Sandwich, so an OnionDecorator could, in fact, decorate a CheeseDecorator.

So, to support a burger with cheese and onions, we would create it as follows (using a compressed syntax):

Sandwich s = new OnionDecorator(new CheeseDecorator(new BasicHamburger).

Invoking s->getPrice() would first call the oniondecorators getPrice()
which would call its base getPrice() which would call its wrapped getPrice()
which would call the CheeseDecorator getPrice
which would call its base getPrice() which would call its wrapped getPrice()
which would call the BasicHamburger getPrice()
which would return HAMBURGER_PRICE
which would return HAMBURGER_PRICE + CHEESEBURGER_PRICE
which would return HAMBURGER_PRICE + CHEESEBURGER_PRICE +
        ONION_PRICE!

Trust me![1]

[1] Believe it or not, the topic of using onions in this example was actually a point of contention because many places charge nothing for them (that is, getPrice returns 0). However, at my local establishment, onions (caramelized, fried, or otherwise) have an additional charge, which irks me a bit, so it provided a reasonable choice.

Now, how many additional classes are needed to support all these toppings? The basic hamburger and decorator bring it to 10 classes (see Figure 5.10)! A far cry from the (4+8+16+32+64+128) 252 classes discussed earlier.

Figure 5.10. Adding the toppings without class explosion


For the complete sample code for this example in C++, see Appendix B: BurgerShop Code.

The main routine follows. Figure 5.11 illustrates the sample Burgerland program execution.

Figure 5.11. Sample Burgerland program execution


Main
// BurgerLand.cpp : Defines the entry point for the console
       application.
//
#include <iostream>
#include <memory>

#include "Sandwich.h"
#include "CheeseDecorator.h"
#include "BaconDecorator.h"
#include "TomatoDecorator.h"
#include "BasicHamburger.h"

int main(int argc, char* argv[])
{
    Sandwich* sand = new CheeseDecorator(new
       BaconDecorator(new TomatoDecorator(new BasicHamburger)));
std::cout << sand->getName() << " price = $" << sand->getPrice();
    delete sand;
return 0;
}

Reflections

We have accomplished turning an exponential problem into a linear problem; one more topping equals one more class. Furthermore, to change the implementation of the cheese topping (price, vendor, whatever) would simply involve a single point of maintenance.

Notice that we can support all the toppings architecturally without implementing all of them. In addition, we can place multiple developers on this framework without their stepping on each other's toes. One great benefit of this is that testing can also be done in isolation; changes to subclasses usually do not impact the testing already done to other subclasses.

Creating any variation of toppings (even adding double cheese) can be done simply by creating the decorators around the appropriate hamburger. This can get somewhat ugly for the client as the number of decorators increases. However, the bigger issue is that this assumes that there are no rules on the order in which these toppings are to be added. (I know I don't want the tomatoes placed between the meat patty and the cheese—yuck!) We often want to combine the Decorator Pattern with a Builder pattern. In this case we assign the rules assembly to a Builder class, which then attaches the toppings in the appropriate order (see Figure 5.12).

Figure 5.12. Adding a Builder


To build a bacon cheeseburger, a client would simply call something similar to the following code:

BurgerBuilder bb;
bb.startSandwich();
bb.addBacon();
bb.addCheese();
Sandwich s = bb.getSandwich();

And the BurgerBuilder would ensure that the bacon is on top of the cheese.

Simplifications

Although this is an extreme example (and then some), it does illustrate the power of the decorator pattern. Obviously, the code of this application could be greatly reduced through parameterization of price and name—if that was the only variance. Suppose, however, that some of the decorators were not quite so simple. Perhaps some prices vary based on an inventory level or computed value. We would like to keep the core framework in place but avoid coding each of the decorator variations that differ only by name and price. This can be done simply by adding a template approach to handle the base case and yet still permit us to specialize other cases. In this way a reduction in code can result by unifying behavior where possible.

An example of a simple template definition to avoid coding the many classes is shown here:

/////////////////////////////////////////////////////////////
// TSandwichDecorator.h: interface for the Standard Template
       Decorator class.
/////////////////////////////////////////////////////////////
#if
       !defined(AFX_TSANDWICHDECORATOR_H__675D6594_481A_11D3_B
       A98_00500428B24D__INCLUDED_)
#define
       AFX_TSANDWICHDECORATOR_H__675D6594_481A_11D3_BA98_00500
       428B24D__INCLUDED_

#if _MSC_VER > 1000
#pragma once
#endif // _MSC_VER > 1000

#include "SandwichDecorator.h"

template <const Amount price>
class TSandwichDecorator : public SandwichDecorator
{
public:
inline TSandwichDecorator(const string& name, Sandwich* sandwich);
inline virtual ~TSandwichDecorator();
    inline virtual string getName();
    inline virtual Amount getPrice();
private:
    const string m_name;
};


template <const Amount price>
inline TSandwichDecorator<price>::TSandwichDecorator(const
       string& name, Sandwich* sandwich):
   SandwichDecorator(sandwich), m_name(name)
{

}

template <const Amount price>
inline TSandwichDecorator<price>::~TSandwichDecorator()
{

}

template <const Amount price>
inline string TSandwichDecorator<price>::getName()
{
    return getSandwich().getName() + ", " + m_name;
}

template <const Amount price>
inline Amount TSandwichDecorator<price>::getPrice()
{
    // current price - here for now - future calc
    return getSandwich().getPrice() + price;
}


#endif //
       !defined(AFX_TSANDWICHDECORATOR_H__675D6594_481A_11D3_B
       A98_00500428B24D__INCLUDED_)

This could then be used as follows:

// BurgerLandWithTemplate.cpp : Defines the entry point for
      the console application.
//
#include <iostream>
#include <memory>

#include "Sandwich.h"
#include "CheeseDecorator.h"
#include "BaconDecorator.h"
#include "TomatoDecorator.h"
#include "BasicHamburger.h"
#include "TSandwichDecorator.h"

static const string cheddarName = "Cheddar Cheese";
typedef TSandwichDecorator<.50> CheddarCheeseDecorator;

int main(int argc, char* argv[])
{
    Sandwich* sand = new CheeseDecorator(new
      BaconDecorator(new TomatoDecorator(new
      CheddarCheeseDecorator(cheddarName, new BasicHamburger))));
std::cout << sand->getName() << " price = $" << sand->getPrice();
    delete sand;
return 0;
}

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

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