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.
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.
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.
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.
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.
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:
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.
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.
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.
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.)
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
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.
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.)
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.
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.
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.