Of course, just knowing where you want to go isn't enough. You also have to know how to get there. Software engineers use a methodology to arrive at the goal.
The primary difference between software engineering and programming is the level of documentation. Software engineering requires you to document (1) requirements, (2) analysis, (3) design decisions, (4) error reports, (5) test results, and (5) configurations.
Some of these topics fall outside the scope of a book about Ada, but we can at least touch on some of them, particularly when we talk about how they affect Ada.
There will certainly be some who will say that the development of the Draw_Poker program does not satisfy the requirements in 2167A. My response to that charge is, "That's right. I make no attempt to satisfy those requirements."
I first realized how dangerous it is to confuse requirements with goals one time when I was reviewing a proposal for a target detecting device. The engineer presenting the proposal began his speech by saying, "The requirement is to design a complicated, multi-level interrupt-driven microprocessor-based target detecting device." I interrupted him, saying that I couldn't believe that was his requirement, but he insisted it was. I asked him if his sponsor in Washington said, "I don't care what kind of targets this thing detects, as long as it is complicated and uses multiple levels of interrupts!" He assured me that's what the sponsor demanded. I couldn't convince him that the goal was to detect certain kinds of targets in a certain environment.
It may be that the problem can be solved using an interrupt-driven microprocessor, and there might several levels of interrupts, and it might be complicated, but that shouldn't be a requirement. Suppose someone thinks of a simpler, cheaper, more accurate, more reliable solution that only uses a single-level interrupt. Do you want to exclude that solution because it doesn't meet the requirements? I don't!
The goal is absolute. It represents a need that must be satisfied. Requirements reflect the current thinking of what intermediate steps must be taken on the road to achieving the goal. Requirements can (and should) change if a better way to achieve the goal is discovered.
I believe the first step in software engineering is to document the goal. To the mainstream, respected software professionals, this means writing a software specification. Although I agree with this in principle, I've found that writing a software specification doesn't help much. Software specification tend to focus on requirements, not goals. This often locks you into a requirement that is counter- productive to the goal. Furthermore, the software specification tends to be written in legalese. In theory it is so precise it can be interpreted only one way by a court of law. In practice it is usually so complicated that nobody really understands it, and everybody thinks it means something different (especially the customer and programmers). Typically the customer isn't happy with the final product, and the blame eventually falls on the party with the poorest lawyer.
I set design goals by writing a sales brochure and a user's manual. The sales brochure emphasizes the goals (what it does, why you need it, and what it costs). The user's manual explains how it works. Most people wait until the project is over before writing these documents. Then they realize too late that the product doesn't do what the customer needs, is more expensive than existing products, or is too complicated to use.
Whenever I pick up the manual for my word processor, I ask myself, "Did someone write this user's manual before the product was designed? Did they maliciously intend it to be this difficult to use?" I suspect that some managers made a list of requirements consisting of every feature they could think of. Then a hoard of programmers did whatever they had to do so they could claim they met all the requirements. Then some unfortunate soul got stuck with the job of trying to write a manual to explain how to use it. (Of course the technical writer gets all the blame for the lousy manual.) Consider this: If you can't easily explain how to use your program, how can any user be expected to learn to use it? The user's manual sets the requirements for the user interface. You have to write the user's manual first, or who knows what mess you will end up with.
Perhaps the most important section of the sales brochure is the cost analysis. It tells prospective customers the average amount of money gambled per hour, the house advantage, and the resulting income per hour. When this income is compared this with the cost of the machine, it shows how quickly it pays for itself. Then a comparison of the expected income to the anticipated monthly maintenance shows how much money the machine makes per day. Every day the customer delays in purchasing the machine, he loses that much money. He can't afford not to buy it!
The cost analysis is just as important to us as it is to the customer. It tells us the maximum amount we can sell the machine for. If our sales brochure has to say that it takes 10 years for the machine to pay for itself, no rational person will buy it. We will have limited the market to people who are too stupid to realize it is a bad investment. Knowing how much we can get for each machine, the number of machines we can produce per month, and the amount of profit we need to make each month, we can figure how much profit we must make on each machine. When we subtract the profit from the selling price, that gives us the production cost. If we can't produce the machine at that price (including nonrecurring engineering costs), then there isn't any point in even starting the project.
The sales brochure should also include hardware features (vandal resistant, won't accept slugs, silent alarm when theft attempts are detected, and so on) that we will ignore here since this is a book on software. You can't ignore those aspects in the real world.
I think this is a major cause of defense contract overruns. Checking a design against a software specification doesn't tell you much. It just tells if requirements are being satisfied. That doesn't tell you if you are meeting your goals or not. If you aren't meeting the requirements, the tendency is to lower them so you can meet them. That doesn't help. You just spend more money to build something that's inadequate.
Let's return to the Draw_Poker example. If this is to be the first of a whole line of video card games, then it seems likely that some of the routines we develop for this game will be useful on other games. For example, routines that shuffle, deal, and display playing cards aren't limited to poker. These routines are in the PLAYING_CARDS package (Listing 66), so they can be reused in other card gmes.
If we were writing this project in FORTRAN or assembly language, we might recognize the utility of general-purpose routines that shuffle, deal, and otherwise manipulate playing cards. We would be wise to collect them in a file and compile (or assemble) them into object code modules that could be saved in a library. You might think that the PLAYING_CARDS package is just like one of these files of library routines. Well, in some ways it is, but it is really much more. What makes the PLAYING_CARDS package different from a FORTRAN or assembly language library is the fact that those libraries contain nothing more than executable routines. If you look in the PLAYING_CARDS package specification you will find routines, but you will also find abstract data types, constants, and error conditions. This makes it more complete (and therefore more useful) software component than a simple library of general-purpose routines.
Furthermore, the decisions I make about data type representations will greatly affect how I write my executable routines. If I change the representation of playing cards late in the program, then I'll probably have to throw away everything I've done already.
Ada's abstract data types take the burden from you and put it on the compiler. She lets you make the data types fit the problem, rather than trying to change the problem to fit the available data types. Then she let's you develop algorithms that don't depend on how the data is represented. This leaves you free to change the representation at any time without losing much (if any) of the work you've already done.
Your initial reaction to abstract data types might be influenced by your emotional reaction to the term "abstract." If your immediate reaction to the term "abstract art" is, "A confusing picture that's distorted and hard to understand," then you probably are a little afraid of abstract data types. You will expect them to be weird and hard to understand. Well, don't let a few bad artists scare you off.
A good abstract artist has the ability to separate the important features from meaningless ones. Certainly an abstract artist distorts reality, but a good one does so in a way that emphasizes the important points and makes the trivial points disappear. If this is properly done, it isn't confusing at all. In fact, it conveys meaning to the spectators with remarkable clarity because the message shown by a few powerful images without the clutter of extraneous details.
The same thing is true of the abstract of a technical paper. The abstract describes the paper by concentrating the important facts in a small space, without cluttering the description with a lot of minute details.
An abstract data type does the same thing an abstract painting or the abstract of a paper does. It represents the important characteristics of the object without cluttering it up with the unimportant details (details like how many bits are used and how the bits are encoded).
If an abstract artist wanted to capture the essential details of a poker game on canvas, what would he do? He would watch the game, and mentally decide what was important to the fundamental activity. He would mentally eliminate the table from the picture because it isn't necessary to the game. Sure, it keeps the cards from falling on the floor, but it doesn't affect who wins, so it isn't an important part of the picture. The only important objects the artist would include in the picture are the cards that were dealt, the players' hands, and the deck. It doesn't matter what shape, size, or color the cards are, as long as you can tell what rank and suit they have. Ranks and suits are important abstract qualities of cards. The thickness of the card and design on the back aren't important. The artist is free to represent a card in any manner, just so long as you can tell what suit and rank it has.
The actions that are important are the shuffling of the cards, dealing of the cards, discarding the cards, and determining the value of a hand to see who the winner is. The skillful artist must figure out how to show these dynamic actions happening on a static piece of canvas.
The artist may or may not show the cards being sorted. Sorting the cards in a hand is an optional action. It makes it easier to see if a hand holds a winning combination, but you could figure that out without sorting the hand if you had to. Who knows, maybe there is a way to hash code the values in a hand, that allows you to tell a winning hand from a losing hand quicker than you could if you sorted it. You are probably wise to sort a hand, but it isn't a requirement.
The PLAYING_CARDS package specification (Listing 66) is just an abstract painting in words instead of oils. It describes important objects and actions, and eliminates all the other details.
Look near the beginning of the package specification. I've told Ada to create a data type called Suits. Objects that are Suits can have values of CLUBS, DIAMONDS, HEARTS, or SPADES. I don't care how Ada represents these things internally. She can use 0, 1, 2, 3 or 1, 2, 3, 4, or 'C', 'D', 'H', 'S', or anything else she desires. It doesn't matter to me, as long as she is consistent. I have, however, given her an implied precedence. CLUBS is the lowest value and SPADES is the highest. Similarly I've defined an ordered set of values called Ranks.
Then I have defined three data types that are even more abstract. Cards, Hands, and Decks are private data types. We don't know what values they can have or how they are represented, but we really don't care about those details at this level of the program.
Skipping over the three exceptions for the moment, we see the things we can do with these abstract objects. We can find out the suit and rank of a card. We can open a new deck or shuffle it. We can open a new hand, sort it, peek at each individual card, play any card, tell if a card has been played, see if a hand is full, or deal a card to a particular hand.
Returning to the exceptions, I have defined three things that could go wrong. When you take the cellophane off a box of cards and inspect it, you might find an extra seven of hearts, or discover that the two of spades is missing. This shouldn't happen, but you should know about it if it does. You don't want to force every application program to check for it, so the Open_New action does it automatically. A more likely error is that an application program will get carried away and try to deal a 53rd card from the deck. If the application program is careless enough to do that, it probably isn't doing any special error checking for that condition. The Deal action better take responsibility for checking for that error. By the same reasoning we can conclude that an application program may try to deal a card to a hand that is already full, and not check for that error. That's why I included these exceptions in the package. They remind me that these are error conditions I must check for when I write the package body, and tells whoever uses this package what error flags might be raised.
These are all the things we need to do to all the objects we need for a poker game. To be truly reusable we should also include objects needed for other card games (tricks and trumps for bridge, melds for canasta, and so on), but I didn't want to complicate the package with unnecessary objects or operations.
Suppose I had used visible records and arrays. (That is, suppose I had moved the type definitions from the private part to the place where the three "... is private;" declarations are. That would have eliminated the need for the Suit_Of and Rank_Of functions because application programs could simply use CARD.SUIT and CARD.RANK. That is exactly what I wanted to avoid. By making the definition of Cards private, I can be sure that I can change the definition from a record to a simple integer if I like, and it won't affect any other part of the program.
You say, "Why would you want to change the representation of Cards to an integer?" Well, suppose I discovered that my program wouldn't fit in memory. Looking at the code generated by the compiler I see that a card is represented by a two component record. The first component, the SUIT, is a number 0 to 3 represented as a 32 bit integer. The second component, the RANK, is a number 0 to 12, also represented by a 32 bit integer. Therefore it takes 64 bits (8 bytes) to represent a card. Since there are 52 cards in a deck, the representation of those 52 cards takes 416 bytes. If I represented a card as a number from 0 through 51, then I could use 1 byte per card, and only 52 bytes per deck. That's a storage savings of 88%!
If I change to an integer representation, and I have used private types, all I have to do is rewrite the Suit_Of and Rank_Of functions. That's easy enough to do. I can use integer division by 13 and the VAL attribute to get the SUIT, and modulo 13 operator and VAL attribute to get the RANK. I can recompile any program that uses PLAYING_CARDS and I can be sure it will still work. (It will take less space, and may run slightly slower, but it will still work.)
If I change the representation of a card from a record to a 1-byte integer, and have used visible types without the Suit_Of and Rank_Of functions, then I will have to find every line of every application program that contains CARD.SUIT or CARD.RANK. Granted this is easy to do with a text editor, and Ada will tell me if I missed any, but I still have to insert division and modulo operators all over the place. There is a good chance I will make a mistake doing that.
So visible types aren't a good choice. But if private is good, then limited private must be better. Isn't it? Well, the decision between private and limited private isn't always easy. If you immediately see a reason why you will want to assign values to an object, or need to check to see if two objects are equal, then you can't use a limited private type. (Limit private types don't have assignment operators or equality tests.) If you don't see a need to do these things, it is better to try to use a limited private type.
Rather than spending a lot of time and effort figuring out if I need private or limited private, I usually just try limited private first and see if that leads me into trouble. I couldn't see any reason to assign a value to Cards (I wasn't going to put any cheating in the game), nor did I see a need to check two Cards for equality (there's only one deck, and every card is unique), so I decided to use limited private for Cards at first. It didn't appear that I would need assignment or equality for Hands or Decks either, so I made them limited private, too.
I ran into trouble when I tried to make a copy of a hand and sort the copy. Then I realized that I did have a legitimate need to assign a value to a hand. I made Hands private instead of limited private. Then I realized I might want to make a copy of a deck for a duplicate bridge game so I would need to assign values to decks, too. Then I realized I might not want to wait until I randomly dealt myself a royal flush to see if the royal flush detection algorithm really works, and so I might want to assign particular values to Cards in my hand in a test routine. There went the last limited private type.
I think it is a good idea to use limited private types whenever possible, so I always try them first. If they don't work, it is a simple matter to strike the word limited with a text editor. If I start out with a private type first, I might not realize I don't need to assign a value to it or test it for equality, and I might leave it private when it should be limited private. ÿ