Previous

Contents

Next

Chapter 1:
Programming concepts

A journey of a thousand miles
must begin with a single step.

— Proverb


1.1 What is a program?
1.2 Readability, maintainability, portability and reusability
1.3 Specifications and implementations
1.4 Abstract data types
1.5 Generics
1.6 Inheritance and polymorphism

1.1 What is a program?

A program is typically defined as a sequence of instructions for a computer to carry out in order to do a particular job, but this is perhaps a rather simplistic definition. You can also look at it another way and say that a program is a model of some aspect of the real world which you can use to mimic the behaviour of the real world. The more accurate the model is the better the results you can expect to get. For example, a program to calculate a company’s payroll is modelling aspects of the real world such as income tax legislation. A program to forecast the weather is modelling atmospheric conditions such as temperature, pressure and humidity. And a program such as the word processor I’m using to write this book is modelling things like letters, words, lines and paragraphs. Inside the computer all these things are represented as patterns of 0s and 1s but programs manipulate them in such a way as to make these patterns behave like the things they are supposed to represent. A program is not just a sequence of instructions; instead, it models objects in the real world in such a way as to mimic the behaviour you would expect from those objects as they interact in the real world. This is the basis of the object-oriented approach to programming, where the program is regarded as a collection of interacting objects rather than just a featureless sequence of instructions.

In this book you’re going to learn how to write programs in Ada, a general purpose programming language originally commissioned by the US Department of Defense. The first standard for the Ada language was finalised in 1983 and was later revised to produce a new updated standard in 1995. The earlier version is now known as Ada 83 and the later version (the one covered in this book) as Ada 95. Ada programs use a somewhat stilted kind of formal English in order to specify the operations you want the computer to perform. You provide some declarations which specify the objects that the program is going to deal with. You also provide a sequence of statements specifying the actions you want performed on those objects and the computer will perform them in order, one after the other. A set of declarations together with a sequence of statements like this makes up a procedure which you invent a name for. For example, you might write a procedure which clears the screen and call it Clear_Screen.

Computers do not understand Ada directly; the text of your procedure (the source code) must first of all be translated (compiled) into an internal form by an Ada compiler. The compiler protects you against your own stupidity by detecting a lot of common errors such as those which arise from typing mistakes, and these must be corrected before you can go any further. Once a procedure has been compiled successfully it is added to your program library. Whenever you want to perform that sequence of statements you can just refer to the procedure by the name you’ve given it. For example, you might write another procedure which needs to clear the screen before displaying something on it. All it would have to do would be to call on the services of the existing Clear_Screen procedure. This means that you don’t have to rewrite things you’ve already written, which reduces the amount of work you have to do both in writing the programs and in testing them to make sure they work properly. You can also make use of procedures written by other people without having to know all the details of how they work.

The next step for turning your procedure from a library unit into a working program is to link it (also referred to as building or binding it). This combines it with any other library units it refers to. In addition to any library units you may have written, the Ada language specification defines a number of standard library units that all Ada systems must provide (e.g. operations like input and output) and it is quite likely that your procedure will have used one or more of these, either directly or indirectly. The linker takes your procedure, any library units it refers to, any others that they refer to and so on, and binds them all together into a single executable program which you can then run.

Of course it is quite possible (almost inevitable, as you will discover!) that the program won’t work the way you expected; the compiler isn’t omniscient, so the fact that your program compiled successfully just means that it’s a valid Ada program. It doesn’t guarantee that it’s the particular Ada program you expected it to be, and it’s up to you to test it thoroughly to make sure it behaves the way you intended it to. You might hope for a program which displays a five times table but end up with a program that displays the number 5 over and over again, in which case it’s back to the drawing board: you’ll need to track down the errors (known as bugs in the trade), correct them, recompile, relink, and try again.

Writing a program which produces the right answers from sensible input is only part of the story. You also need to make sure that it still behaves sensibly with nonsensical input. For example, the program might expect you to type in a number; what happens if you type in your name instead? If the program responds with an error message, ignores the erroneous input and carries on working, fine. If the program crashes (i.e. halts unexpectedly), this is not so good. You may never get the result that the program is supposed to produce. Even worse, the program might get stuck in an infinite loop where it just ends up doing the same thing over and over again until it is forcibly halted.


1.2 Readability, maintainability, portability and reusability

Writing a program which does what it’s supposed to do is only part of the story. Once you’ve got the program working someone will usually think of some extra feature that would be incredibly useful or some improvement to what it already does that will make it easier to use. These sorts of changes come under the heading of program maintenance. Most real-world programs are hundreds of thousands of lines long and will be in use for about five years before being replaced. Over a five year period, maintenance will typically cost about four times as much as developing the original program and will involve writing as many lines of code as there were in the original, either as additions to or replacements for lines in the original. More programmers are employed maintaining existing code than writing new code. How easy is it going to be to make maintenance changes to your program? What if it’s someone else who has to make the changes rather than you, the original author?

Obviously readability is a major factor in determining how easy it is to maintain a program. You need to be able to read and understand what the program does in order to change it. As I said earlier, Ada programs are written using a somewhat stilted formal English. Writing a program is in many ways just like writing an essay. It can be well- presented and well-structured or it can be a tangled rambling mess. If you want other people to be able to understand what it’s all about (or even yourself in six months’ time!) you need to make an effort to present the program so that its structure and meaning is easy to understand. If you don’t, you’ll end up thinking ‘what on earth does this bit do?’ and wasting a lot of time trying to understand what’s going on.

Maintainability is a measure of how easy it is to change a program. Ideally a single change should involve changing just one part of the program. If you have to alter several parts of the program to effect the change there’s a risk that you’ll forget to change some of them or that you’ll make a mistake in some places but not in others. For example, consider a payroll program that can handle up to 100 employees. If the number of employees expands, the program will need changing to handle (say) 200 employees. If there is a single line in the program that says that the number of employees is 100 and the rest of the program refers to ‘the number of employees’ rather than using the number 100 then there will be no problem. If, however, the value 100 appears as a ‘magic number’ throughout the program, every single occurrence of 100 will need changing to 200. The chances of missing one are fairly high. Worse, some places where 100 is used might be involved in calculating percentages, or it might refer to the number of pennies in a pound, and we might accidentally end up with 200 pence to the pound or percentages calculated wrongly. And what about the places where the magic number 99 appears? Should 99 be changed to 199 throughout, or rather, to ‘the number of employees - 1’?

Another situation that arises quite often is the need to make the program work on several different systems. A program should ideally be portable from one system to another; all you do is recompile it and, hey presto, it works! Unfortunately life is rarely that simple; there are usually differences from one system to another such as the size of the largest possible number that can be handled or the capabilities of the display screen. Although you can avoid assuming that your program is running on a system which has particular characteristics it will sometimes be impossible to eliminate system-specific dependencies. About the only thing you’ll be able to to do in such situations is to gather together all the system-specific parts of the program so that it’s easy to locate them and change them.

After writing a few programs you normally discover that a lot of the time you’re doing the same old thing that you did before, at least in places. Input and output; sorting collections of data into order; trying to find things in a collection; there are dozens of common features shared by different programs. After a while a sensation of d‚j… vu comes upon you and you realise you’ve done it all before. The second time you do it better and/or faster than the first time. The third time you do it better and/or faster than the second. The fourth time you start yawning. How easy is it to reuse what you’ve already written and rewritten? Ada provides some mechanisms which allow you to write software which is flexible enough that you can reuse it fairly easily in a variety of different situations. Learning to make the most of these mechanisms is always useful but it becomes a necessity as soon as programs start to go above a few thousand lines of code in size.


1.3 Specifications and implementations

Ada’s reuse mechanisms are based on separating specifications from implementations. All you need to use an existing procedure is a specification which gives its name and describes what it does; you don’t need to know anything about the implementation, i.e. how it works. This is a bit like driving a car; you don’t need to know how an engine or a gearbox works in order to drive, you just have to know what the various controls do. You can take the analogy further: the people who build the car don’t need to know how to manufacture the individual components like pistons and fuel pumps that go into it. They just need to know how to assemble these components to produce the car as a finished product. One of the most important aspects of becoming a proficient Ada programmer is learning what components are already available and how to assemble them to provide larger components or finished products.

This separation also enhances maintainability and portability. As long as all you rely on when you use something is a specification of what it does, it is easy to change how it does it. This can be done behind the scenes with no visible effects other than the need to take any existing programs which rely on the specification and relink them so that they use the new implementation. Portability is also aided by the fact that you can say ‘this procedure clears the screen’ as a specification but then provide different implementations for different systems with different types of screen.


1.4 Abstract data types

It is quite common for collections of related procedures to be useful in a wide variety of situations. To avoid programmers having to reinvent the wheel every time they sit down to write a program, a collection of related procedures, data type declarations and the like can be put together into a package and added to the library as a single unit. This is useful for defining classes of objects as described earlier. For example, packs of playing cards are a class of objects which could be defined by a package. The package could contain some declarations for data types like Card and Pack together with a set of procedures defining the things you can reasonably expect to do with a pack of cards (shuffle the pack, deal a card, replace a card at the bottom of the pack, and so on). If you hide the implementation details such as the internal representation of a card or a pack of cards, users are only able to do things that are provided for in the package specification and things which you would not reasonably expect them to be able to do (such as manufacturing extra aces at will or dealing cards from the bottom of the pack) are prohibited.

What you end up with is an abstract data type. Abstract data types provide no information about their internal implementation; instead, all you know is the set of permissible values of the type and the operations that can be performed on them. This is just like the way that numbers are handled in a computer. You don’t usually have access to the internal representation of numbers in the computer’s memory; all you know about is the visible representation of numbers like 3.1416 and the fact that you can perform operations like addition and subtraction on them. Ada packages provide the mechanism by which programmers can define their own abstract data types.


1.5 Generics

One of the things that hinders reuse in most programming languages is that algorithms (methods of solving a problem) are usually intertwined with the type of data that they deal with. For example, a procedure which sorts a list of numbers into ascending order will normally need changing if you want to sort a list of names instead. Even if the changes are straightforward, this means that the source code for the procedure must be available for it to be changed. Commercially available software is not generally supplied in source form so that trade secrets can be preserved and copyright enforced, so this can mean reinventing the wheel yet again.

Ada allows you to separate algorithms from the data they deal with by allowing you to write generic library units. For example, in order to sort a collection of data all you really need to know is how to compare data items with each other to discover if they need reordering and how to move them to the correct position in the collection if they are out of order. The same algorithm can be used regardless of whether you’re sorting numbers or names as long as these conditions are met. Ada allows you to write a generic sorting procedure which, given a data type which satisfies the requirements of the algorithm, will sort a collection of items of that type. All you have to do to be able to use it to sort items of a particular type is to specify the item type to be used and the method for comparing two items. You can use the same algorithm over and over again for different types of data without the need to modify it in any way.


1.6 Inheritance and polymorphism

Another obstacle to reuse and maintenance is when you have some existing code that almost but not quite meets your requirements. The ideal solution is to use the existing code but extend it and modify its behaviour where necessary to fit your requirements. For example, you might want a program which can deal with different types of bank accounts such as current accounts and savings accounts. Using conventional programming techniques it will need major surgery whenever a new type of bank account is introduced that behaves slightly differently from other account types, such as an interest-bearing bank account. Object-oriented programming introduces the notion of inheritance to overcome this problem. Using inheritance all you have to do is to say that this new type of bank account is the same as that old type of account but with the following differences. The new bank account inherits all the characteristics of the existing account type, modifying or extending those characteristics where necessary. A minimum of new code needs to be written, which speeds up development and simplifies testing and debugging.

A related problem is dealing with those situations where the different types of account behave differently. Normally this involves checking the type of account at each point in the program where different behaviour is possible and then choosing the behaviour appropriate to that account type. A new account type will therefore involve a number of modifications at different places in the program with the associated risk of missing a change or changing something incorrectly. Inheritance guarantees that all bank accounts will share certain common characteristics such as the ability to deposit and withdraw money. New types of bank account will either inherit the existing behaviour or provide a replacement for it. In either case it will still be possible to perform those operations. Polymorphism reflects the ability to provide different implementations of the same operation for different types and to select the correct behaviour automatically for the actual type of object being dealt with. Rather than seeing a program as a set of procedures which manipulate data, you can look at it as a set of objects which you can ask to perform particular operations. Each object responds to a request to perform a particular operation by executing the version of the operation which is appropriate for it. You don’t have to check what type of account you’re dealing with when you say ‘deposit some money’; instead, you tell the account that you want to deposit some money and it does it in the way that’s appropriate for the type of account that it is. As a result, you won’t need to change any existing code when you introduce a new type of account.



Previous

Contents

Next

This file is part of Ada 95: The Craft of Object-Oriented Programming by John English.
Copyright © John English 2000. All rights reserved.
Permission is given to redistribute this work for non-profit educational use only, provided that all the constituent files are distributed without change.
$Revision: 1.2 $
$Date: 2001/11/17 12:00:00 $