Contents

4.6.11 Keep I/O Routines Separate

Some of you may have read an article I wrote for the February 1986 issue of Dr. Dobb's Journal of Software Tools. It contained a Version 1 of the PLAYING_CARDS package. That version contained overloaded Put procedures that I have removed from the version 2. That's because I realize now that it was a mistake to mix I/O routines with processing routines.

I used to believe I should always include I/O routines for new data types in the package that defines the data types. I thought, "There's not much point in creating things if you can't input or output them." Well, that's true, but it doesn't mean those I/O routines have to be in the same package. Version 1 used TEXT_IO to write phrases like "Three of Hearts" to the screen. If I ever write a book of advanced Ada examples, I will probably expand on this example by using a graphic interface to draw the cards on the screen. TEXT_IO wouldn't be any use in that application. I shouldn't have to modify and recompile the PLAYING_CARDS package just because I'm using a graphics package instead of TEXT_IO to display Cards. If I change the output device or output method, I expect to have to rewrite I/O packages, but I shouldn't have to rewrite any processing packages. Routines that turn pixels on and off (or write messages to the screen) have nothing to do with routines that shuffle and deal cards. Therefore, they don't belong in the same package.

4.6.12 A Limit to Reuse

The PLAYING_CARDS package body is shown in Listing 67. It's simple enough that it doesn't require much explanation, but I do have to justify my Sort routine. People have spent years searching for the ultimate sort routine, and here I've used this simple bubble sort. Isn't this a golden opportunity to reuse a generic sort routine?

I'm all for reuse (the Draw_Poker program reuses RANDOM_NUMBERS, STANDARD_INTEGERS, MONEY, DIM_INT_32, SCROLL_TERMINAL, and ASCII_UTILITIES), but sometimes reuse is more trouble than its worth. It's true, a more exotic sort routine might be faster, but how long can it take to sort five cards, even using the most inefficient routine? If I were sorting 5,000 cards, and speed were important, then I might instantiate somebody else's super-optimized generic sort package. In this case it was quicker to write thirteen lines of simple code than search for a reusable component that will do the job.

If I already had a generic Sort routine that was easy to instantiate, and had established its reliability, of course I would have used it. If I expected to need to sort large collections often, then it would make sense to write (or buy) a generic sort routine, verify it, and use it in whenever I needed it. But this is the first time in 22 years of programming that I've ever needed a sort routine, and I don't anticipate needing one again in the next 22. A reusable sort routine isn't high on my priority list right now.

Writing a book is a lot like doing a real project. There is a deadline that has to be met, and you can't waste your time searching for the most elegant solution when you already have something that works perfectly well, especially when there are other things that aren't done yet. Looking for a generic Sort routine is counterproductive.

4.6.13 Efficiency VCR Verifiability

The Sort routine also brings up another issue. Which is more important, efficiency or verifiability? The answer depends on the situation. The Draw_Poker program runs so quickly I had to add some delay statements to slow it down (I like to keep the player in suspense while the cards are being dealt), and it fits easily in memory so I'm not concerned about size. In this situation I don't care at all about efficiency, but I want to be sure the program works correctly, so this time it is an easy question to answer. Verifiability is the only important feature. It would be a more difficult question to answer if I had to worry about speed and space.

I bring the issue up because schools tend to emphasize efficiency, and as a result young programmers tend to do a bad job by optimizing too well. Suppose such a programmer is faced with the job of writing the Sort routine. First he wastes time calculating logs and powers to determine which is the optimum sort routine to use. Sort routines work for numbers, not playing cards, so he has to modify it to sort cards. (If it is generic this may be as simple as defining the < operator.) Then he has to verify it to see if it works. This is probably going to take longer than coding and verifying a simple sort routine. Time is money. The optimized version costs more (because it took more time to develop), and doesn't do the job any better. That's bad engineering.

Furthermore, the optimized version may cost more again later in the life cycle. If the program fails to work (or needs to be modified to sort by suits as well as ranks), a maintenance programmer will have to look at the code and figure out what it is doing. A simple sort routine is easier to verify than a complicated one. Therefore it will take less time (that is, cost less) to maintain the simple version than the optimized one.

4.6.14 Hidden Dependencies

In a moment, you are about to see the Draw_Poker program listing. Before you look at the whole listing, let me tell you that the first line is with PLAYING_CARDS, MONEY;. This makes it appear to depend only on two other software components, but you already know that MONEY depends on DIM_INT_32, ASCII_UTILITIES, and STANDARD_INTEGERS. DIM_INT_32 depends on STANDARD_INTEGERS and INTEGER_UNITS. ASCII_UTILITIES depends on STANDARD_INTEGERS. PLAYING_CARDS depends on RANDOM_NUMBERS (which you will see in the next section) and STANDARD_INTEGERS. RANDOM_NUMBERS depends on STANDARD_INTEGERS and CALENDAR. Who knows what the CALENDAR package body needs.

That's just a list of the units you get from the context clause. Some of the subunits of Draw_Poker depend on SCROLL_TERMINAL (to simulate hardware I/O), and would depend on special interface packages if the product was ever built. SCROLL_TERMINAL depends on VIRTUAL_TERMINAL, which may depend on DOS, VMS, or CURSOR and TTY. There's no telling what the special interface packages might need.

Usually we try to avoid hidden dependencies because they might cause unexpected side effects. We don't want to be unpleasantly surprised if we make a change to one module and find out that an apparently unrelated module doesn't work anymore.

The really amazing thing about Ada is that all these dependencies exist, but you don't have to worry about them. The dependencies are hidden in the sense that they don't clutter the program listings, but they aren't undocumented. Ada keeps track of the dependencies, so tools can be written that tell you all the units that will be affected if you make a change to a particular unit. Even if you don't use such a tool, Ada always makes sure that everything is current and that all the interfaces match before she will link object code modules into an executable image.

Ada uses layers of abstraction to hide these dependencies so they don't confuse you. You can obtain the power of so many previously written components with so little effort, and without cluttering your program with the details of how they work. The Draw_Poker program appears to be just a couple of pages, but it generates a sizable program because it takes such good advantage of reusable software components.

4.6.15 Building from the Bottom

I haven't said so, but the PLAYING_CARDS package is a bottom-up design. I tried to keep this a secret until now because bottom-up design is frowned upon in some circles. It earned a bad reputation because undisciplined programmers often start at the bottom of a design and keep building. When you start at multiple roots, you have to be incredibly lucky for all of these roots to grow together into a neat solid trunk. Normally there is a burl where things that don't really fit together have been forced into place. A pure bottom-up design usually isn't very good.

That doesn't mean all bottom-up designs are bad. I've shown you how you can build a SCROLL_TERMINAL on top of a VIRTUAL_TERMINAL that is built on top of operating-system interface packages. That's a bottom-up design, and it's good. Bottom-up design helps you write basic utility programs that can be used as building blocks in many different programs.

You only get into trouble when you try to get these individual building blocks to continue to grow and somehow merge with each other to form one program. You just can't start from many places and expect to be able to join them all with one golden spike [Footnote 1]. They probably aren't going to line up. The key to success is to recognize when you have built all the foundation modules you need, then stop working from the bottom-up.

4.6.16 Top-Down Design

When it comes time to establish the program flow, I think it is best to start from the top and work down. This means stating the solution to the problem in the most general terms, then defining those general terms with more specific terms until the solution is spelled out in complete detail. We saw an example of this in Chapter 3.5.9, and here is another example.

Listing 68 shows the top level of the Draw_Poker program. Don't think for a moment it was written sequentially. I used a screen-oriented editor and jumped all over that file. I started with a top level skeleton and put flesh on it. That is, it began like this:

procedure Draw_Poker is begin loop (cursor) end loop; end Draw_Poker; I knew I wanted an infinite loop. I just had to decide what to put in the loop. I began with comments derived directly from the requirements. -- Play as long as the user is willing to bet. -- Shuffle before every deal. -- Deal a hand to the player. -- Show him what he has. -- Let the player hold or draw each card. -- Replace any cards he may have discarded. -- Show him what he has now. -- Pay him if he won. Then, under each comment I wrote a few lines of code to do what the comment said. Usually I just invented a subprogram to do it. For example, under -- Show him what he has. I wrote put(PLAYERS_HAND, VALUE);. I knew I would need a routine that would display the cards in the hand, and display the value of the hand (NOTHING though ROYAL_FLUSH). At that point in the program development I didn't really care how it worked, just so long as it did work.

I realized I would need a function called Value_Of that would look at a hand and tell me if it contained a winning combination of cards. I considered the merits of simply passing the PLAYERS_HAND to the put routine and letting put call Value_Of instead of the main program calling Value_Of and passing the result to put. You can see I finally decided to do the latter. I did this partly because I wanted to avoid having both put and Payout call Value_Of (both need to know the value of the hand), and partly because I wanted to make it obvious at the top level that put was displaying the value of the hand. (If the procedure call was just put(PLAYERS_HAND); then it would not be obvious that put calls Value_Of and tells the player if he has a winning combination or not.)

This approach allowed me to partition the problem into five smaller problems. If I had five programmers working for me, I could have assigned one the job of writing a procedure gets the player's wager. I could let the second one write a function that determines the value of a hand. The other three programmers could work on procedures that display the hand and value, let the player discard, and drop the player's winnings loudly in a dish.

When I wrote the top level program I didn't worry about any declarations. I just compiled the program and got lots of error messages. Then, based on the error messages, I declared objects (STOCK, PLAYERS_HAND, WAGER, VALUE), the data type Values, and two library packages (PLAYING_CARDS, MONEY). I find that easier than trying to guess what declarations I will need before writing the code.

4.6.17 Renaming Declarations

Time out for a short comment on a technical point. Notice I have included the line function "="(LEFT, RIGHT : MONEY.Cents) return boolean renames MONEY."=";. I needed that because of the line exit when WAGER = MONEY.Type_Convert(0);. Let's talk about those two lines for a moment.

If I just wrote exit when WAGER = 0; I would get a type mismatch error. WAGER is a dimensioned quantity. It is a value expressed in Cents. The number 0 is a pure number with no units attached to it. It could represent dollars, francs, guilders, or pounds sterling. I happens that 0 cents equals 0 francs regardless of the current rate of exchange, but that's just a coincidence. I have to convert 0 to 0 Cents, and I can do that using the Type_Convert function in the MONEY package.

Having done that, I now have a problem with the = sign. The visible meanings for the = sign include comparisons of integers, real numbers, and Values, but not Cents. The function that compares Cents is in the MONEY package (inherited from DIM_INT_32). It isn't directly visible. I have three choices. First I can use MONEY;, which makes everything in MONEY visible. Second, I can use this awkward expression: exit when MONEY."="(WAGER,MONEY.Type_Convert(0)); (I'm sure you can see why I avoided that solution.) The third choice is to use a renaming declaration to make the operation visible. I used the third option partly because I wanted to include an example of renaming in this book. I have a slight preference for the first solution (especially if there are several operators that need to be seen), but if organizational programming guidelines prohibit USE clauses, the renaming technique is a simple way to comply.

4.6.18 Prototyping

No amount of planning will ever anticipate all the problems you will encounter, and the sooner you find out where the problem areas are, the better. As soon as you have established a top-level design, it is a good idea to write a prototype of that design. You can take any shortcut you like. Use a different language on a different computer if it will help you get the job done quicker. The important thing is to practice solving the problem once so you will learn things you need to learn to solve the problem for real.

If I were really going to build and sell the Draw_Poker machine, I would be looking at a significant hardware investment. I would pay engineers a lot of money to design and embedded computer, graphic displays, a mechanism that accepts coins and bills, the winnings dispenser, and the control panel containing the buttons the players push to deal and hold cards. Before I spend all that money, I want to be sure of the design.

What do I expect to learn from a prototype? If I knew that I wouldn't have to build the prototype. I usually learn things I never would have thought of in a million years. The Draw_Poker prototype was no exception.

The Draw_Poker top-level design defined five separately compiled subunits. The prototype was built by writing the simplest possible bodies for those subunits.

The first subunit is the procedure get that gets the WAGER from some special hardware that recognizes the values of coins and paper money. I can easily simulate this using the SCROLL_TERMINAL to ask the user how much he wants to be, converting the input string to a number of pennies, and returning the amount. When I wrote this module (Listing 69), I had to check for error conditions. Some of these error conditions couldn't happen in the real machine. The value of the U.S. dollar is less than it has been in the past, but it isn't negative yet. The real machine won't have to check for negative values of money, but it will have to check for the minimum and maximum bets. All of a sudden I realized, "I never specified what the machine should do if the player enters less than $1 or more than $999.99." I said it shouldn't accept those bets, but should it just spit the money back out without comment? Should it tell the user what he did wrong? If so, should I flash a light behind a red plastic lens that says, "BET WAS TOO SMALL", or should I display that message in big red letters on the screen? These are decisions that could affect the control panel or display screen, and I should make them now, before the hardware is designed and built.

The second subunit of Draw_Poker is Value_Of, shown in Listing 70. Unlike the other subunits, this one won't get thrown away when I build the real machine. Putting it in the prototype gives us an opportunity to start testing it early in the design phase. It turned out that Version 1.0 of this subunit failed to recognize ACE, TWO, THREE, FOUR, FIVE is a STRAIGHT. I discovered that while playing with the prototype. A rigorous testing program may have discovered that flaw, but then again, it might not. It always pays to have as all the experience you can with a product before you begin to sell it.

The third subunit is put (listing 71). I was surprised to learn that it ran too fast. Draw_Poker shuffled and dealt all five cards before I got my fingers off the keyboard, and put displayed them before I was ready to see them. I can't explain why, but that made me feel uncomfortable when I was playing the game. I guess I missed the thrill of seeing the first three cards turn up hearts, and wondering, "Will the last two also be hearts?" When I added a one-second delay in the display loop, it made it a much better game.

I also didn't like the fact that the cards I held were redealt to me. (If you compile and run the prototype, you will find that after you decide to hold or discard each card, all the cards disappear, and you are dealt a new hand. Some of the cards in that new hand are the cards you elected to hold.) I didn't fix that in the prototype because it was too much trouble, but I learned something important even though I didn't fix it. I now know that a graphic display of the playing cards will have to be able to erase individual cards, and slowly move new cards into the empty holes. I can tell that to the person designing the graphic display before the design is started. If I hadn't done the prototype, I probably wouldn't have thought of that, and I would have been unhappy with a display that showed the whole hand all at once. It probably would have been difficult, time consuming, and expensive to go back and modify the display routine.

The fourth and fifth subunits, Discard_From (Listing 72) and Payout (Listing 73) aren't particularly interesting or informative, but you need them if you want to play the game.

I wrote this prototype on my IBM PC AT clone. That's much more power than I need to do the job. When building the production units, I want to put in the cheapest computer that will do the job. How do I know what size computer to use? Can I get by with a single-board 8086 computer with 640 KB memory, or will I need 80286 with 4 MB?

Without the prototype, I'd just have to make a wild guess. The prototype doesn't tell me all the answers, but it helps me make a reasonable estimate. The Alsys compiler will let me generate object code for the 8086 or 80286. I can compile the prototype both ways to see what difference it makes.

The size of the real program won't be exactly the same size as the prototype. There are major differences, especially in the display routines, but at least I can tell a little bit about the program size from the prototype. I know exactly how big the Value_Of function will be. I know how few bytes are needed for the top-level procedure. I'll have to put some serious thought into how much the other routines will take, but I can make some assumptions and do some experiments. I won't be able to estimate the program to within a few bytes, certainly, but I should be able to tell if I will need extended memory or not. As I work on each subunit I can revise my memory estimate and check to make sure I'm not getting into trouble. If I am in trouble, the sooner I find out about it, the better.

4.6.19 Validation and Verification

Before we sell the product we have to validate and verify the program. This two-step certification process (1) assures that the program contains modules or statements that satisfy every stated requirement (and no unstated ones), and (2) verify that each module operates correctly. Early in the program development it doesn't make sense to try to verify that each module operates correctly because most of them haven't even been written yet, but is never too early to validate the design against the requirements.

If we validate the Draw_Poker program at this point in the development, we find some interesting things. It does everything it is required to do, but it also does some extra things. It has a default bet of $1. There isn't any requirement to do that. It also tells the player if he has a winning hand before he discards and cards. It lets the player end the program by entering a $0 bet. These are extra features not found in the requirements, and we have to address them somehow. We will have to (1) change the requirements, (2) change the design, or (3) ignore the problem for now.

These discrepancies crept in because the requirements are for a coin operated machine, but I built a prototype on a general purpose computer. The coin operated game should run forever, but I have to be able to stop the prototype program so I can use the computer for other things. It's a real nuisance to have to reboot the system every time I want to stop the prototype. I added the zero bet to give me an easy way to quit. I don't want to change the requirements, nor do I want to change the design of the prototype, so I'll ignore the problem for the moment. If I were going to continue with this example, I would add some comments to the prototype code to remind me to change the design for the coin operated version. (I'm ignoring the fact I could just as easily use CONTROL-C to quit, because I wanted an example of an instance when I might purposely violate the requirements in a prototype.)

The default bet makes no sense in a coin-operated game. The bet is whatever amount of money has been inserted. It was convenient for the prototype, but not really necessary, and it violates the requirements. I should take it out.

The early display of a winning hand was a side effect of using the same display routine before and after cards were discarded. In this case I decided to change the requirements because it makes the game more attractive (that is, easier to sell) to the player. (I could have also solved the problem by changing the design to match the requirements. This is easily done by assigning VALUE := NOTHING; before displaying the hand the first time.)

4.6.20 Integration

Things that work individually don't always work when you put them together. Or perhaps they work, but they don't work the way you expected them to. You never find these things out until you integrate the system. Many software development projects leave integration till the end. I believe in early integration. This is easy to do in Ada, if you have written a prototype.

Suppose we have built the Draw_Poker prototype and done the validation on it. We've made the changes eliminating the default bet and the zero bet. We can integrate modules as soon as they are finished.

If I were actually going to build and market the machine, I imagine the get procedure would be done first. I've seen dollar bill changers and candy machines, so I know devices that recognize the value of money exist. A little investigation would probably turn up a list of vendors who sell something I could use. I'd pick one and figure out how to connect it to a parallel I/O port. There are probably two handshaking signals. The first lets the device tell the computer that money has been entered. The second lets the computer acknowledge that it has read the amount and is ready for the device to accept more money.

The get routine has to monitor the input handshaking line and read the I/O port every time some money is inserted in the device. Then it can add that amount to a running total and use the other handshaking line to indicate it is ready for more money. It also has to monitor the DEAL button. When the user presses the DEAL button on the control panel, it passes the WAGER up to Draw_Poker and clears the total.

The get routine could be tested using a breadboard circuit. The money input device and the DEAL button could be mounted on the breadboard and wired to a connector that plugs into the parallel I/O port. A simple test routine could be written to make sure it works. The body of the program might look something like this

SCROLL_TERMINAL.put_line("enter money"); get(WAGER); -- routine under test SCROLL_TERMINAL.put_line(MONEY.Image(WAGER)); Just put in a known amount of money, press DEAL, and check the screen to see if it correctly tells you how much you entered. Do this as often as it takes you convince yourself that it is working correctly.

After you are convinced it works, link this real get procedure in place of the simulated get procedure you used in the Draw_Poker prototype. When you put some money in the machine and press DEAL, the prototype does what it always used to do. (The terminal screen shows you some cards, asks you which you want to keep, and pays you if you won.)

The disturbing thing is that it doesn't do anything while it is waiting for you to enter money. It doesn't prompt you, flash lights, or anything. It just sits there until you put some money in it. Someone walking by the machine doesn't even know it is on! Now your prototype has told you something else. There is a flaw in the design.

You have to decide what to do. You could add something to the get routine to make it flash a light behind a lens that says, "What's your bet?". If so, you need to add that light to the control panel.

On the other hand, the machine has a nice color display monitor. You may want get to call a graphic cartoon routine that tells people to step up and put money in the machine. That change may involve turning get into a task, adding a Come_On task, and using a timed call in a select statement to call Come_On if the player hasn't entered any money lately. We're talking about major changes here!

Using the prototype to integrate pieces of the solution can warn you of problems when it is still early enough to do something about it. You don't want to find out that you need a "What's your bet?" light after you have manufactured 20,000 control panels. You don't have time to start developing a lot of new graphic routines just before the final design review. I think it is vital to use a prototype program as an integration test bed early in development.

4.6.21 Maintenance Manual

In most cases, if you give a maintenance programmer a source-code listing and a pile of documents relating to the program, the only thing the maintenance programmer will read is the source code. That's because the source code is the only thing that counts and the only thing you can trust. It doesn't matter what it says in the documentation, the computer is going to do what the source code tells it to do. People have great intentions of keeping the documentation correct and up-to-date, but they seldom do. Often it is poorly written and confusing. Maintenance programmers usually don't read it. You may not like it, but those are the facts of life.

Since the only thing you can be sure the maintenance programmer will read is the source code, that's where the bulk of the information has to be. Chapter 2.11.1 described in detail how to document specifications and bodies. Putting these important comments in the source code (instead of a separate document) increases the probability that someone will read them.

Even if you write good comments in the source code, there are still things that need to go in a maintenance manual. The problem is getting the maintenance programmer to read the maintenance manual. Most people want to put everything in the maintenance manual. They want structure charts and data flow diagrams for every module. They wind up with a massive, expensive document that's boring and hard to read. All the information is there, but nobody can find it in the clutter. Few people have the patience to even try. That's why I believe it is important to keep the maintenance manual short and well organized. If you give someone a small, helpful document, he might read it.

A good maintenance manual begins with the theory of operation. This is a brief overview of what the program is doing. A few carefully chosen diagrams (structure charts, state transition diagrams, or data flow diagrams) should be used, but there is no need for diagrams of every module. After giving an overview, you should list the modules and tell how each module fits in the general scheme.

Analysis and design decisions should be documented in the body of the maintenance manual. If there were several viable ways to do something, explain why one approach was taken and others rejected. You should devote a subsection to each software component.

Section 3.1 of this book is a pretty good example of a maintenance manual for the ASCII_UTILITIES package. It gives general background information for the subprograms in the circuit control forms in some places but not others. These are the kinds of things that need to be documented but don't belong in the code itself. Section 3.5.9 could be turned into a maintenance manual for the Get_Response subunit by adding a structure chart and data flow diagram. Section 3.6 is NOT a very good example of a maintenance manual because it is too long and goes off on too many tangents. (It was written to be a tutorial of general concepts, not a maintenance manual for the FORM_TERMINAL. I was looking for excuses to digress and found lots of them.)

4.6.22 Other Software Engineering Concepts

There are three other things you need to consider when working on a big project that you don't need to worry about for small projects. They are (1) configuration management, (2) error reporting, and (3) cost and schedule. These things don't have a lot to do with Ada and probably don't belong in this book at all, but I just wanted to call your attention to them briefly.

4.6.22.1 Configuration Management.

A big part of software engineering is configuration management. When you build a large software product, you have to break it down into modules. The side effect of modularization is that you now have a lot of little pieces to keep track of. You have to know which pieces you need to build a larger unit. The little pieces are often revised, and you need to make sure you are using the correct revision.

Ada takes care of some aspects of configuration management. She knows what units need to be linked to create a main program. She knows if any of the units are obsolete. This could lead you to believe there is no need for configuration management if you use Ada. Unfortunately, that's not true.

Ada doesn't relieve you of the responsibility of configuration management. I've been trying to keep current copies of all the listings in this book on an AT clone (with the Meridian compiler), a genuine AT (with the Alsys compiler) and a VAX (with the DEC compiler). Every time I make an improvement in a listing on one machine I have to remember to make the same correction on the other two. It is a nightmare. Even if you use Ada, you still have to use some discipline, and/or a configuration management tool, to keep things straight.

4.6.22.2 Error Reporting.

Another important part of the life cycle is error reporting. This is perhaps just another aspect of configuration management because you need to keep track of the software errors you discover during development and after delivery. This should include a description of the symptoms, the consequences of the failure, the revision it was found in, and the correction to the software. I've never seen anything that makes me believe error reporting is any easier or more difficult in Ada than any other language.

4.6.22.3 Cost and Schedule.

For a little program like More, it would take you longer to estimate how long it will take than it takes to do it. Planning isn't an issue in these cases. Big programs involve massive expenditures over long periods of time, and you need to have a good idea of how much time and money the project will involve.

I always use time and money in the same breath, because for software development they are practically the same thing. If you want to estimate how much a software project will cost, it really comes down to estimating how many man- hours it will take. There may be a few expenses that don't have anything to do with labor, but they are easy to estimate. You may have to buy a compiler or other software engineering tools, but you can pick up the telephone, call a few vendors, and you know what you will have to spend for them. The real trick is figuring out how many people you need and for how long, so you know how much you will have to spend in salaries and office expenses.

Other costs (such as testing, documentation, configuration management, and error reporting) will be related to the size of the project, which is related to how long it takes to develop. So the problem really boils down to "how do you know how many man-hours will be required to complete the project?" If you know how many man-hours it takes to write the software, you can multiply by some factor (that you have determined from your previous experience with software projects) to determine the overhead and support costs.

I'm still searching for a method that reliably predicts the duration of a software project. Experience seems to indicate that software development lasts as long as there is money to fund it, so if you tell me how much money you will give me, I will tell you how long it will take. I know that's not the answer you want to hear, so let me try another one. My first estimate is usually a wild guess. You probably don't like that answer any better, but that's the only honest one I can give you.

Although I don't have a good way to get an initial schedule estimate, I do know a way to tell if I am on schedule. After a little while, I can revise the estimated schedule based on progress made in the elapsed time.

How do you measure progress? When you've written 100 lines of code, are you 1% done or 10% done? You can't tell unless you know the program is going to be 1,000 lines or 10,000 lines, but you won't know that until the project is all over. Then it's too late. Counting lines of code doesn't tell you anything.

Fortunately it is a little easier to measure progress in Ada than other languages. That's because you can partition the work into modules and figure completion on the basis of number of modules completed. For example, take the Draw_Poker program. At the beginning of the program, you know Draw_Poker consists of the modules PLAYING_CARDS, MONEY, Get, Value_Of, Put, Discard_From, and Payout. There are seven major modules, so if you'll settle for a crude estimate you can figure that each module is one-seventh (14%) of the job. Each time a module is completed, you know you are another seventh of the way home.

To be more accurate requires a little more effort, judgment, and skill. Let's assume that MONEY is a completed reusable component and you don't need to figure it in the schedule. That leaves us with six modules. Let's rank them in order of difficulty. I think Put will be the hardest (it involves complicated graphics). The Get and Payout modules will be moderately hard (because they interface with hardware I don't have yet). PLAYING_CARDS is likely to be lengthy. Value_Of and Discard_From will be the easiest. If I let one unit of effort be the effort required to write the easiest module, I can estimate the relative difficulty of the others. PLAYING_CARDS, I feel, will take five times as long as Discard_From. Payout will probably take twice as long as PLAYING_CARDS, and so on. Intuition (and that's all it is) tells me the effort to complete the whole program can be allocated in this way.

---------------------------------- Units of Effort Module Name ---------------------------------- 50 Put 10 Get 10 Payout 5 PLAYING_CARDS 1 Value_Of 1 Discard_From That all adds up to 77 units of work. Each unit of work is worth 1.3% of the total job. When Payout is done, then the project is 13% complete. When Put is done, it is 65% complete. If Payout and Put are both done, the job is 78% finished.

We don't have to wait for a module to be complete to estimate how far along we are. PLAYING_CARDS consists of eleven subprograms, which are probably equally difficult. If any three of the eleven are done, then 27% of PLAYING_CARDS is done. Since PLAYING_CARDS represents five of the seventy- seven units of work (6.5%), then 27% of 6.5% of the job is done. (It is 1.76% done.)

Once a month, I can measure my progress. If each month shows 5% increase in the amount of completion, I can revise my estimate to 20 months, regardless of what the initial wild guess was. (Total project time = time spent so far / fraction complete. 20 months = 1 month / 0.05 = 2 months / 0.10.)

It probably won't be nice and linear because you probably didn't estimate the relative difficulties of the individual software components properly. When you discover some part of the project is more difficult than expected, you can change the relative difficulty based on actual experience. As you get farther along in the project, the estimate should get better. If your initial wild guess was high, it will predict an early completion. If the wild guess was low, it will tell you that you are behind schedule in a few months. ------------------------------------------------------------ 1. Note to international readers who might be unfamiliar with American history. Two companies were given the task of building the first transcontinental railroad. One started from the East, the other started from the West. When they met in the middle, the last two sections of track were joined by a golden spike.


Contents | Next ...