`

Object-Oriented Analysis and Design

阅读更多
Object-Oriented Analysis and Design, Part 1

Object-Oriented design is easy once you learn how to identify the right objects.

By Alistair Cockburn, with C++ code by Chuck Allison,  C/C++ Users Journal
May 01, 1998
URL:http://www.ddj.com/cpp/184403494

Introduction

"If you have done an object-oriented (OO) design, how do you describe it to another person? What do you talk about or show, to convey your design?" I have asked this of experienced designers for several years now. I usually get back a very long silence, ending with a shrug of the shoulders. This is a tough question, not covered in classes and books.

This two-article series presents a problem I use both to teach and test OO design. It is a simple but rich problem, strong on "design," minimizing language, tool, and even inheritance concerns. The problem represents a realistic work situation, where circumstances change regularly. It provides a good touch point for discussions of even fairly subtle designs in even very large systems. This problem was successfully solved by six first-year computer science students with no OO background. It was less successfully addressed by business people, commercial programmers learning OO, and experienced OO programmers.

If you do a great job on this problem, congratulate yourself. If you are taken by surprise at the developments, you are in good company. What I hope you get from this series is:

    * how to talk about designs,
    * a bit of how to evaluate and improve designs,
    * and a sample problem against which to test your favorite design technique.

Warm-Up: What Is "Design?"

To learn what design is, I have been asking my students for years now, before we start on any OO lectures, to sketch a simple design of an everyday system and present it to the class on two overhead transparencies. No talking is to accompany the transparencies. The class then tries to understand, from the transparencies, the design of the system. At the end the class discusses which of the teams' presentations best captured the notion of design.

The system I ask them to design, and am now asking you to design, is a small, one-branch bank. Sketch the design for the whole thing, not just the computer part. Take no more than 15 minutes. Work with someone else, if possible. Capture the design on a single sheet of paper. (Your pencil lets you put much more on a page than the fat transparency pens I give the students.) You should be able to show this piece of paper to pretty much anyone on the street, and they would recognize the bank's basic design. On your marks, get set. Go.

The acclaimed winners in my classes consistently present four things:

1) The name of the key components, or things, in the system

2) The purpose, basic function, or main responsibility of each thing

3) A drawing of the key communication paths between them

4) A list of the services provided

I have adopted these four things as essential to communicating design. I see these things used to describe the design of significant parts of large companies, of large OO systems, and also of code-level solutions to hard programming problems (see "In search of methodology" [2]).

Nowadays, I insist upon these items in program or system design documentation (along with email records of design arguments and resolutions). The set of four design items tells us why an UML/OMT-style drawing is not satisfying to experienced designers. A UML/OMT drawing gives the components, but says neither what their purpose is nor what their main communication channels are.

Of the four design elements, the brief "responsibility statement" is the most important. It tells an experienced person what will be included and what will be excluded from the data and behavior for the component. Ward Cunningham, Kent Beck, and Rebecca Wirfs-Brock have been championing the responsibility statement for years [1, 4]. The drawing of the communication channels summarizes the communication patterns and gives a quick view of the traffic patterns and collaborations. It gives the viewer a rapid understanding of the system's behavior. Trygve Reenskaug's "role models" are an articulate and precise way to describe communication channels [3]. Rebecca Wirfs-Brock's "contracts" were an earlier attempt to capture them [4]. The communication channels drawing expands into interaction diagrams, which demonstrate that the components, holding to their responsibilities, really can work together properly to deliver the services.

I gave you the bank exercise just to introduce the four key items needed for good design. The problem I use for instruction in this article is the design of a coffee machine. In discussing and comparing designs in this article, I shall focus on Component Names, Component Main Responsibilities, and Intercomponent Communication Channels. The services list will come to you through the requirements statement. Expect me to ask, "Which component knows the price of the coffee? Which object knows how to make the drink?" I shall show, and ask you to show, a few interaction diagrams to demonstrate that the system really works. Although you may use inheritance in solving the problem, I am going to spend less time on inheritance. If you get the basic design wrong, no inheritance structure will fix it. If your basic design is strong, you can play with the inheritance structure to make it better.

Figure 1 shows a sample solution to the bank design question that uses the four elements. Note that the Customer is not a component of the bank and has no responsibilities. (There is no way, at system design time, to distinguish a customer from a robber; that has to show up in the operation of the system.) The components of the bank are:

    * The table. It provides a flat surface for writing and filling out forms.
    * The teller. This person is responsible for coordinating the actions involved in the transaction, such as getting money from the vault and entering the transaction into the computer.
    * The vault. Holds the money.
    * The computer. Knows the current state and history of the customers' accounts.

Now let's go on to the main problem.

The Coffee Machine Problem

You and I are contractors who just won a bid to design a custom coffee vending machine for the employees of Acme Fijet Works to use. Arnold, the owner of Acme Fijet Works, like the common software designer, eschews standard solutions. He wants his own, custom design. He is, however, a cheapskate. Arnold tells us he wants a simple machine. All he wants is a machine that serves coffee for 35 cents, with or without sugar and creamer. That's all. He expects us to be able to put this little machine together quickly and for little cost. We get together and decide there will be a coin slot and coin return, coin return button, and four other buttons: black, white, black with sugar, and white with sugar.

Now design the machine using objects. What are the components, what are their responsibilities, how do they work together to deliver the simple service: give coffee for 35 cents? This process will work best if you cover up the diagram in Figure 2 and sketch your own design before reading further. Go.

Which component knows the price of the drink? Which component knows how to make the drink?

Test your design against Kim's test scenarios:

    * Kim puts in a quarter and then selects a coffee.
    * Kim puts two quarters in and then selects a coffee.
    * Kim puts in a quarter, then pushes the coin return lever.
    * Kim puts in two quarters, then walks away from the machine and forgets to come back.
    * Kim buys two coffees, white with sugar. The sugar dispenser runs out of sugar after the first.

Figure 2 shows the basic design that students commonly give me at this point, after we have straightened out the standard bugs. Figure 3 shows the interaction diagram. The code in Listing 1 shows sample C++ declarations for the classes involved (a full implementation is delayed for a better design). For space reasons, I omit details of making change and handling insufficient change. Note this is not a good design; it is a typical one. I hope you do better. Try to say why your design is "better" than this one. The answer will be clear by the time we get done.

The typical coffee machine design submitted to me has four main components:

COFFEE MACHINE (1) DESIGN:

    * Cash Box. Knows amount of money put in; gives change; knows price of coffee; turns Front Panel on and off.
    * Front Panel. Captures selection; knows what to mix in each; instructs Mixer what to mix.
    * Mixer. Knows how to talk to the dispensers.
    * Dispensers (cup, coffee powder, sugar, creamer, water). Knows how to dispense a fixed amount; knows when it is empty.

Arnold Visits

After five machines are installed and have been operating for a while, Arnold comes along and says, "I would like to add bouillon, at twenty-five cents. Change the design." We add one more button for bouillon, and one more container for bouillon powder. How else do you change your design?

Below is the typical design I get at this stage of the story (see also Figures 4 and 5) . Notice how the division of responsibilities is fundamentally different from the first solution.

COFFEE MACHINE (2) DESIGN:

    * Cash Box. Knows amount of money put in; gives change.
    * Front Panel. Captures selection; knows price of selections, materials needed for each; asks Cash Box how much money was put in; instructs Mixer what to mix.
    * Mixer. Same as before.
    * Dispensers. Same as before.

Snap Analysis of Designs 1 and 2

According to popular report, we are using object-orientation to reduce the impact of changes. Notice that we have completely revamped the machine. Arghh! Evidently, the first design was not a really good design. The key error was that the cash box knew the price of the coffee. That means that asking for different prices for different drinks raised havoc with the design. The new design is better in this regard. Since the front panel knows the selections and the price, we can change prices at will, and only change one component.

I voice a worry at this point: the front panel looks awfully smart. It hardly deserves to be called a "front panel." I call this the "mainframe" approach to design. One of the objects in the system has all the smarts. Almost any change to the system can be accomplished by changing the mainframe object. You may have come up with a slightly different mainframe approach. You keep the front panel dumb; all it does is register a selection. You add a fifth object, which in a spark of inspiration, you call the Controller. The Controller knows everything the second design's front panel knows, only does not physically have the buttons. The front panel tells the Controller the selection, the Controller talks to the cash box, the Controller tells the mixer what to dispense.

Although the trajectory of change in the mainframe approach involves only one object, people soon become terrified of touching it. Any oversight in the mainframe object (even a typo!) means potential damage to many modules, with endless testing and unpredictable bugs. Those readers who have done system maintenance or legacy system replacement will recognize that almost every large system ends up with such a module. They will affirm what sort of a nightmare it becomes.

Although I voice the worry, I cannot quantify it, and so we leave the design the way it is, with either the front panel or the controller being the mainframe object.

Arnold Visits Again

Arnold comes back a while later with a brilliant idea. He has heard that some companies use their company badges to directly debit the cost of coffee purchases from their employees' paychecks. Since his employees already have badges, he thinks this should be a simple change.

We add a badge reader and link to payroll. I ask you to make the design change. Ready, set, Go.

How does your new design change the system? You should find that this is not as bad as it sounds. To make the result clean and robust, we need only a change in attitude. It is no longer possible to ask the cash box for the amount of money put in. None was put in. So we borrow the inquiry style used by credit cards. When you go to pay for a meal, the restaurant does not ask American Express or Visa, "How much money does this person have?" The restaurant asks, "Can this person accept a charge of the following amount?" And the answer comes back yes or no.

So now, the cash box answers only one question from the front panel: does the customer have <amount> of credit? We become indifferent to whether the customer pays by cash or direct debit. Further, payroll can shut the credit down at a certain point for whatever reason. If your original design already worked this way, give yourself ten extra credit points. Even though it would have cost nothing to have designed this way from the start, the design was not obvious at the start. You might have arrived at the design if you had enquired after how the design requirements might evolve in the future. The responsibility, "knows how much money is put in," is sensitive to assumptions and to a technology. "Knows whether there is sufficient credit" works the same for cash, but is less specific about assumptions and technology. It is therefore more robust. Design (3) (Figure 6) changes very little from design (2). The new responsibilities are:

COFFEE MACHINE (3) DESIGN:

    * Cash Box. Accepts cash or charge; answers whether a given amount of credit is available.
    * Front Panel. Same as before, but only asks Cash Box if sufficient credit is available.
    * Mixer and dispenser. Same as before.

The code in Listing 2 implements Design 3. Try to say why this design is or is not "better" — in arguable terms. Avoid arguments like, "because one should/shouldn't use objects." I have even found that "simpler" is an argument fraught with conflict. See what you come up with.

I am still not happy with the design. We'll see why in the next part of the article. See if you can improve the design, and say why it is "better," while waiting. If you have a really good or different solution, send the responsibility statements and interaction channels to me at arc@acm.org. I'll publish some of the more interesting designs on my web page, http://members.aol.com/acockburn.


Arnold Gets Competition

People are starting to buy Starbucks lattes instead of Arnold's coffees. So Arnold wants the machine modified just slightly, so that he can create a "drink of the week." He wants to be able to add new drinks and change prices any time, to match his competition. He wants to be able to add espresso, cappuccino, hot chocolate, latte, choco-latte, steamed milk — in short, anything he can mix together.

We add a couple more buttons, a milk steamer and dispenser, and some more powder dispensers. We conclude that our cash box design is pretty safe as it is, and discuss how to meet Arnold's request.

The first, and typical suggestion at this point is to beef-up the mainframe object so that it knows everything. I put my foot down, finally, and ask for a really good, robust design, so that we or Arnold can change the products and prices at will, without tearing the machine apart any further. This works best if you cover up the diagrams that follow and do your design on your own. Ready, set, Go.

The design we come up with at this point bears no resemblance to our original design. It is, I am happy to see, robust with respect to change, and it is a much more reasonable "model of the world." For the first time, we see the term "product" show up in the design, as well as "recipe" and "ingredient." The responsibilities are quite evenly distributed. Each component has a single primary purpose in life; we have avoided piling responsibilities together. The names of the components match the responsibilities.

COFFEE MACHINE (4) DESIGN:

    * Coffee Machine. Knows how the machine is put together; handles input.
    * Cash Box. Knows how much credit is available; handles money and direct debit.
    * Selector. Knows products and selection; coordinates payment and drink making.
    * ProductRegister. Knows what products are available.
    * Product. Knows its own price and recipe.
    * Recipe. Tells dispensers to dispense ingredients in sequence.
    * DispenserRegister. Acts as a librarian for the dispensers; controls nothing.
    * Dispenser. Controls dispensing; tracks amount it has left.
    * Ingredient. Knows its name only.

Figure 1 shows the new design. Figure 2 shows an interaction diagram for it. The code appears in Listings 1 and 2. Listing 3 shows sample input for the test program, and the output is in Listing 4.

When Arnold wants to add a product, we add a new Product to the Product Register. Should Arnold change the recipe or price of a product, we have exactly one, localized change to make. In other words, we have reduced the trajectory of change, as described in all the advertising on object technology.

For the first time, we have a component that is not static. The selected Product rolls around the system, sharing its knowledge with the fixed components. For many newcomers to Object-Oriented (OO) design, such an object is non-intuitive. However, it is a common sort of OO design, as it provides the localization of changes we have been searching for.

There are two other changes from the previous designs. First, there is no mixer. Some of you will have noticed in the earlier designs that the mixer was doing very little, and with the new design, it vanishes entirely. The other change is that Ingredient is its own type of object. In the first designs, we used strings and enumerated types to identify a product or an ingredient. One of the things I have learned over the years is that a string identifier or fancy data type in a program almost always will come into its own over the life of the project, and so these days I turn them into objects right from the start. Had Arnold's designers done that, they would have spotted the Product object a lot earlier. I also suspect that Ingredient is going to grow in capabilities over time.

Evaluating the Quality of a Design

We saw in the first article that a design consists of:

    * components,
    * responsibilities,
    * interaction channels,
    * services

But what makes a design good? How might we have detected the flaws in the earlier designs, or gotten to that fourth design faster? My answer is in the form of a principle:

Discussing the quality of an OO design is discussing the futures it naturally supports.

Let us first settle on one important point: each of the four designs was fine in its time. Each was object-oriented, and each worked. There was no intrinsic fault in any. What differs in them is that they support different futures. In the first design, we did not think that the future might call for prices varying by product. Hence, the design did not easily support that. In the first two designs, we did not think of direct debit or credit cards.

The third design is constructed so that most of the system is indifferent to whether cash or credit is used, supporting both futures naturally. The fourth design is constructed so that most of the system is indifferent to the actual prices and recipes. It naturally supports a future of changing recipes and prices.

At OOPSLA '97, there was a workshop on OO Design Quality. In that workshop, we found several dozen dimensions of quality or "goodness." One of those was simplicity. At our table, we found people disagreeing strongly as to which design was simpler, and then found there were at least six different ways to look even just at what makes a design simpler. However, there was consensus there, as in most places I visit, that a good OO design supports change well.

It is good to find consensus on even one point. But does that mean we can't discuss the quality of a design until we have planned the future? E-e-yuck! I don't have a real answer yet, but it does appear that capturing the abstractions in the domain actually helps with handling futures. Designs 1-3 of our coffee machine did not capture the abstractions, and design 4 does a better job.

I regularly question the futures and check the trajectory of change in designs, and in fact that is how my class and I found design 4 in the first place. We took a student's design and asked: "How many components do we have to change to add 'mocha' at $1.25?" The answer was "two." So I asked: "Is it possible to create a design in which only one component has to change?"

We found that two responsibilities were affected, those of price and recipe. They were in separate objects at the time. To create a single point of change, we merged the two responsibilities and asked whether their merger meant anything. Someone called out: "Product!" And the room gave an audible sigh of relief, as they recognized a better fit of the abstractions to the problem (see test 2, below). It does seem that recognizing and creating the abstractions would have led us to the design that protects the futures quicker.

Six Tests for Evaluating a Design

In general, I use and have watched people use six tests to help evaluate a design. They are

1. Data Connectedness

2. Abstraction

3. Responsibility Alignment

4. Data Variations

5. Evolution

6. Communications Patterns

The Data Connectedness test checks whether you actually can traverse the network of collaborations to gather all the information you need to deliver the services. (I have seen a major system design fail this test!) The Abstraction test checks whether the name of the object conveys its abstractions that is, whether the abstraction has a natural meaning and use in the language of the domain experts. Very many objects do not do well in this test. Although the test is subjective, my experience is that everyone gets a sort of "ahh" feeling when you improve the abstraction, as with the introduction of the Product object, above.

The Responsibility Alignment test checks whether the name, main responsibility statement, data, and functions align. During design evolution, usually the functions grow well past what is required by their names or primary responsibilities. Sometimes that is the time to split the object; sometimes it is time to rethink what abstraction you really have in front of you. The Data Variations test checks that the design naturally handles all the sorts and shapes of data the system will encounter. The Evolution test is the one I already described. It asks, what changes are likely in the business rules, technology, services, etc., and how does the design handle them? How many components have to change? Finally, the Communications Patterns test checks for oddly shaped run-time communications patterns. The designer particularly looks for cycles, but sometimes other odd shapes show up. Sometimes nothing is necessarily wrong with any one shape, but you may get suspicious and discover something out of alignment.

The experienced designers I compare notes with pay closest attention to the Abstraction and Responsibility Alignment tests. One even said that the Responsibility Alignment test covers all the rest. When I showed this coffee machine problem to him, he immediately criticized the first three designs. He said: "There are no abstractions here, just machine parts; and the 'front panel' doesn't even say what it is good for, it just says where it is." So, in the absence of a known future, it seems that the Abstraction and Responsibility Alignment tests may help predict the robustness of the design. I am not ready to assert this too strongly yet, not based on the evidence of a mere coffee machine problem. But I am ready to nominate it.

Postscript

The coffee machine problem is handy. I have used it to teach basic OO design in many situations: to college freshmen in a two-hour challenge situation, to business people with whom I have to spend several days doing OO business modeling, and to experienced non-OO programmers learning OO object design. I am also using it to compare OO design approaches. I still encounter new solutions. I hope you found it challenging, too. If you have a different and "better" solution, feel free to write me at arc@acm.org. o

Alistair Cockburn, Consulting Fellow at Humans and Technology, is in Oslo this year as special advisor to the Central Bank of Norway. His recent book is Surviving OO Projects. Besides teaching OO design and project management, Alistair specializes in cognitively simple design techniques and asking "What is OO design quality?" He likes sitting underwater, dancing, and learning languages.

Figure 1: The Coffee Machine, design 4

Figure 2: Interaction diagram for design 4

Listing 1: Coffee Machine Design 4

// coffee4.h

#include <stdio.h>
#include <vector>
#include <map>
#include <algorithm>
#include <string>
#include <assert.h>

using std::string;

// class Ingredient:
// Abstraction of a simple ingredient.
// Knows its name only
class Ingredient
{
public:
    Ingredient(const string& name) : myName(name){}
    string name() const {return myName;}

private:
    string myName;
};

// class Dispenser:
// Abstraction of a dispenser of an ingredient.
// Controls dispensing, tracks amount left.
class Dispenser
{
public:
    Dispenser(Ingredient* pIngredient, int shots)
        : contents(pIngredient)
    {
        assert(pIngredient);
        shotsLeft = shots;
    }
    ~Dispenser() {delete contents;}
    const Ingredient* contains() const {return contents;}
    void add(int shots) {shotsLeft += shots;}
    void dispense(int shots)
    {
        printf("Dispensing %d shot(s) of %s\n", shots,
               contents->name().c_str());
        shotsLeft -= shots;
        assert(shots >= 0);
    }
    int shotsAvailable() const {return shotsLeft;}

private:
    const Ingredient* contents;
    int shotsLeft;
};

// class DispenserRegister:
// Abstraction of the thing that knows all the dispensers.
// Acts as a librarian for the dispensers, controls nothing.
class DispenserRegister
{
public:
    ~DispenserRegister()
    {
        map_type::iterator p = dispensers.begin();
        while (p != dispensers.end())
            delete (*p++).second;
    }
    void addDispenser(Dispenser* pDispenser)
    {
        assert(pDispenser);
        dispensers[pDispenser->contains()] = pDispenser;
    }
    Dispenser* dispenserOf(const Ingredient* pIngredient)
    {
        assert(pIngredient);
        return dispensers[pIngredient];
    }

private:
    typedef std::map< const Ingredient *, Dispenser*,
                      std::less<const Ingredient* > > map_type;
    map_type dispensers;
};

// class Recipe:
// Abstraction of a recipe.
// Tells the dispensers to dispense ingredients in sequence.
class Recipe
{
public:
    Recipe(const Ingredient* pI1, const Ingredient* pI2,
           const Ingredient* pI3, const Ingredient* pI4 = 0,
           const Ingredient* pI5 = 0)
    {
        assert(pI1);
        addIngredient(pI1);
        assert(pI2);
        addIngredient(pI2);
        assert(pI2);
        addIngredient(pI2);
        if (pI4) addIngredient(pI4);
        if (pI5) addIngredient(pI5);
    }
    void addIngredient(const Ingredient* pIngredient)
    {
        ingredients.push_back(pIngredient);
    }
    void makeOn(DispenserRegister* pDispenserRegister)
    {
        for (int i = 0; i < ingredients.size(); ++i)
            pDispenserRegister->dispenserOf(ingredients[i])
                              ->dispense(1);
    }

private:
    std::vector<const Ingredient*> ingredients;
};

// class Product:
// Abstraction of the drink.
// Responsible for knowing its price and recipe.
class Product
{
public:
    Product(const string& name, int price, Recipe* recipe)
        : myName(name)
    {
        myPrice = price;
        myRecipe = recipe;
    }
    ~Product() {delete myRecipe;}
    string name() const {return myName;}
    int price() const {return myPrice;}
    Recipe* recipe() const {return myRecipe;}
    void makeOn(DispenserRegister* pDispenserRegister)
    {
        myRecipe->makeOn(pDispenserRegister);
    }
   
private:
    string myName;
    int myPrice;
    Recipe* myRecipe;
};

// class ProductRegister:
// Abstraction of the thing that holds all the products.
// Knows what products are available.
class ProductRegister
{
public:
    ~ProductRegister()
    {
        // Need to destroy all products:
        for (int i = 0; i < products.size(); ++i)
            delete products[i];
    }
    Product* productFromIndex(int index) const
    {
        if (index < 0 || index >= products.size())
            throw "Invalid product index";
        return products[index];
    }
    void addProduct(Product* pP) {products.push_back(pP);}

private:
    std::vector<Product*> products;
};

// class Cashbox:
// Abstraction of a change maker or cashbox on a real machine.
// Responsible for knowing how much credit the customer has,
// making change, accepting coins.
// This version is suited for, but does not include credit cards.
class Cashbox
{
public:
    Cashbox() {credit = 0;}
    void deposit(int amount)
    {
        credit += amount;
        printf("Depositing %d cents. You have %d cents credit.",
               amount, credit );
    }
    void returnCoins()
    {
        if (credit > 0)
        {
            printf("Returning %d cents.", credit);
            credit = 0;
        }
    }
    bool haveYouFor(const Product* choice) const
    {
        return credit >= choice->price();
    }
    void deductFor(const Product* choice)
    {
        credit -= choice->price();
        returnCoins();
    }

private:
    int credit;
};

// class Selector:
// Abstraction of the internal selector and controller.
// Knows products & selection, coordinates payment and drink making.
class Selector
{
public:
    Selector(Cashbox* pC, ProductRegister* pP, DispenserRegister* pD)
    {
        pCashbox = pC;
        pProductRegister = pP;
        pDispenserRegister = pD;
    }
    ~Selector()
    {
        delete pDispenserRegister;
        delete pProductRegister;
    }
    void select(int choiceIndex)
    {
        Product* pProduct =
            pProductRegister->productFromIndex(choiceIndex);
        if (pCashbox->haveYouFor(pProduct))
        {
            pProduct->makeOn(pDispenserRegister);

        /* We omit here, for space, rebuilding the list of
         * legitimate selections, and checking for empty dispensers.
         * Selector should ask the ProductRegister, which asks each
         * product to check all its ingredients:
         *   pProductRegister.checkQuantities
         *   (pDispenserRegister) */

            pCashbox->deductFor(pProduct);
        }
        else
            puts("Sorry, you need more money. No drink.");
    }

private:
    Cashbox* pCashbox;
    ProductRegister* pProductRegister;
    DispenserRegister* pDispenserRegister;
};

// class CoffeeMachine:
// Abstraction of the outer machine, holding all the parts.
// Responsible for constructing machine, capturing external input.
class CoffeeMachine
{
    Cashbox* pCashbox;
    Selector* pSelector;
public:
    CoffeeMachine()
    {
        DispenserRegister*
        pDispenserRegister = new DispenserRegister;
        ProductRegister* pProductRegister = new ProductRegister;
        pCashbox = new Cashbox;
        pSelector = new Selector(pCashbox, pProductRegister,
                                 pDispenserRegister);
       
        // Load-up the ingredients - normally would
        // obtain externally:
        Ingredient* cup = new Ingredient("cup");
        Ingredient* coffee = new Ingredient("coffee");
        Ingredient* creamer = new Ingredient("creamer");
        Ingredient* sugar = new Ingredient("sugar");
        Ingredient* water = new Ingredient("water");
        Ingredient*
        bouillionPowder = new Ingredient("bouillion powder");
       
        pDispenserRegister->addDispenser(new Dispenser(cup, 30));
        pDispenserRegister->addDispenser(new Dispenser(coffee, 10));
        pDispenserRegister->
            addDispenser(new Dispenser(creamer, 10));
        pDispenserRegister->addDispenser(new Dispenser(sugar, 10));
        pDispenserRegister->
            addDispenser(new Dispenser(bouillionPowder, 10));
        pDispenserRegister->addDispenser(new Dispenser(water, 30));
       
        // Load-up the Products - normally would obtain externally:
        Product*
        black = new Product("black", 35,
                            new Recipe(cup, coffee, water));
        Product*
        white = new Product("white", 35,
                            new Recipe(cup, coffee, creamer,
                                       water));
        Product*
        sweet = new Product("sweet", 35,
                            new Recipe(cup, coffee, sugar, water));
        Product*
        whiteSweet = new Product("whiteSweet", 35,
                                 new Recipe(cup, coffee, sugar,
                                            creamer, water));
        Product*
        bouillion = new Product("bouillion", 25,
                                new Recipe(cup, bouillionPowder,
                                           water));
       
        pProductRegister->addProduct(black);
        pProductRegister->addProduct(white);
        pProductRegister->addProduct(sweet);
        pProductRegister->addProduct(whiteSweet);
        pProductRegister->addProduct(bouillion);
    }
    ~CoffeeMachine()
    {
        delete pSelector;
        delete pCashbox;
    }
    bool oneAction()
    {
        char cmdline[BUFSIZ];
        char action[BUFSIZ];
        int value;
        puts("\n______________________________________");
 
        puts
     ("\tPRODUCT LIST: all 35 cents, except bouillion (25 cents)");
        puts
     ("\t1=black, 2=white, 3=sweet, 4=white & sweet, 5=bouillion");
        puts
     ("\tSample commands: insert 25, select 1.  Your command:");
        gets(cmdline);
        sscanf(cmdline, "%s %d", action, &value);
        if (strcmp(action,"insert") == 0)
            pCashbox->deposit(value);
        else if (strcmp(action,"select") == 0 &&
                 value>=1 && value<=5)
            pSelector->select( value-1 );
        else if (strcmp(action,"quit") == 0)
            return false;
        else
          printf
     (" Did not understand request %s %d.\n",action,value);
        return true;
    }
};
/* End of File */

Listing 2: Tests the implementation of Coffee Machine Design 4

// tcoffee4.cpp

#include "coffee4.h"

main()
{
    CoffeeMachine m;
    while (m.oneAction())
        ;
}
//End of File

Listing 3: Sample input for tcoffee4.cpp

insert 50
select 1
insert 25
insert 25
select 2
insert 25
insert 10
select 3
insert 50
select 4
insert 25
select 5
quit

Listing 4: Sample output from tcoffee4.cpp

C:>tcoffee4
______________________________________
    PRODUCT LIST: all 35 cents, except bouillion (25 cents)
    1=black, 2=white, 3=sweet, 4=white & sweet, 5=bouillion
    Sample commands: insert 25, select 1.  Your command:
insert 50
Depositing 50 cents. You have 50 cents credit.
______________________________________
    PRODUCT LIST: all 35 cents, except bouillion (25 cents)
    1=black, 2=white, 3=sweet, 4=white & sweet, 5=bouillion
    Sample commands: insert 25, select 1.  Your command:
select 1
Dispensing 1 shot(s) of cup
Dispensing 1 shot(s) of coffee
Dispensing 1 shot(s) of water
Returning 15 cents.
______________________________________
    PRODUCT LIST: all 35 cents, except bouillion (25 cents)
    1=black, 2=white, 3=sweet, 4=white & sweet, 5=bouillion
    Sample commands: insert 25, select 1.  Your command:
insert 25
Depositing 25 cents. You have 25 cents credit.
______________________________________
    PRODUCT LIST: all 35 cents, except bouillion (25 cents)
    1=black, 2=white, 3=sweet, 4=white & sweet, 5=bouillion
    Sample commands: insert 25, select 1.  Your command:
insert 25
Depositing 25 cents. You have 50 cents credit.
______________________________________
    PRODUCT LIST: all 35 cents, except bouillion (25 cents)
    1=black, 2=white, 3=sweet, 4=white & sweet, 5=bouillion
    Sample commands: insert 25, select 1.  Your command:
select 2
Dispensing 1 shot(s) of cup
Dispensing 1 shot(s) of coffee
Dispensing 1 shot(s) of creamer
Dispensing 1 shot(s) of water
Returning 15 cents.
______________________________________
    PRODUCT LIST: all 35 cents, except bouillion (25 cents)
    1=black, 2=white, 3=sweet, 4=white & sweet, 5=bouillion
    Sample commands: insert 25, select 1.  Your command:
insert 25
Depositing 25 cents. You have 25 cents credit.
______________________________________
    PRODUCT LIST: all 35 cents, except bouillion (25 cents)
    1=black, 2=white, 3=sweet, 4=white & sweet, 5=bouillion
    Sample commands: insert 25, select 1.  Your command:
insert 10
Depositing 10 cents. You have 35 cents credit.
______________________________________
    PRODUCT LIST: all 35 cents, except bouillion (25 cents)
    1=black, 2=white, 3=sweet, 4=white & sweet, 5=bouillion
    Sample commands: insert 25, select 1.  Your command:
select 3
Dispensing 1 shot(s) of cup
Dispensing 1 shot(s) of coffee
Dispensing 1 shot(s) of sugar
Dispensing 1 shot(s) of water
______________________________________
    PRODUCT LIST: all 35 cents, except bouillion (25 cents)
    1=black, 2=white, 3=sweet, 4=white & sweet, 5=bouillion
    Sample commands: insert 25, select 1.  Your command:
insert 50
Depositing 50 cents. You have 50 cents credit.
______________________________________
    PRODUCT LIST: all 35 cents, except bouillion (25 cents)
    1=black, 2=white, 3=sweet, 4=white & sweet, 5=bouillion
    Sample commands: insert 25, select 1.  Your command:
select 4
Dispensing 1 shot(s) of cup
Dispensing 1 shot(s) of coffee
Dispensing 1 shot(s) of sugar
Dispensing 1 shot(s) of creamer
Dispensing 1 shot(s) of water
Returning 15 cents.
______________________________________
    PRODUCT LIST: all 35 cents, except bouillion (25 cents)
    1=black, 2=white, 3=sweet, 4=white & sweet, 5=bouillion
    Sample commands: insert 25, select 1.  Your command:
insert 25
Depositing 25 cents. You have 25 cents credit.
______________________________________
    PRODUCT LIST: all 35 cents, except bouillion (25 cents)
    1=black, 2=white, 3=sweet, 4=white & sweet, 5=bouillion
    Sample commands: insert 25, select 1.  Your command:
select 5
Dispensing 1 shot(s) of cup
Dispensing 1 shot(s) of bouillion powder
Dispensing 1 shot(s) of water
______________________________________
    PRODUCT LIST: all 35 cents, except bouillion (25 cents)
    1=black, 2=white, 3=sweet, 4=white & sweet, 5=bouillion
    Sample commands: insert 25, select 1.  Your command:
quit

About the Code

The code in Listings 1 and 2 was compiled with Microsoft Visual C++ version 5.0 and Borland C++ version 5.02, and complies with the official ISO standard for C++ approved in November 1997. To save space, all functions are defined in situ, i.e., inside their respective class definitions. The code is intentionally not too "bulletproof" so as not to obscure the expression of Alistair's design. I did take some liberties as implementer, however. You will note that I made generous use of the heap via the new and delete operators.

This approach allows for flexible relationships between objects, and reflects the fact that most of the objects in the design are all "first class," i.e., they have a life of their own. This requires a clear policy of who owns which object, so objects get cleaned up properly in destructors. For example, a Recipe is made up of Ingredients, but each Dispenser owns its Ingredient and is responsible for deleting it. In this implementation, the following relationships hold:

CoffeeMachine owns: the Selector, the Cashbox

Selector owns: the DispenserRegister, the ProductRegister

DispenserRegister owns: all Dispensers
References

[1] Ward Cunningham, and Kent Beck. "A Laboratory for Teaching Object-Oriented Thinking," ACM SIGPLAN 24(10):1-7, 1989.

[2] Alistair Cockburn. "In search of methodology," Object Magazine, July, 1994, pp.52, 54-56, 76.

[3] Trygve Reenskaug, with P. Wold, O. Lehne, et al. Working with Objects (Prentice-Hall, 1996).

[4] Rebecca Wirfs-Brock, Brian Wilkerson, Lauren Wiener. Designing Object-Oriented Software (Prentice-Hall, 1990).

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics