Sunteți pe pagina 1din 28

1

Chapter 1

A Revisionist History of
Programming
or Modeling Reality with Finite, Deterministic State
Machines

1.0 Overview
We present a very abbreviated and incomplete summary of the development of computer pro-
gramming. Purists will abhor the omissions and oversimplifications. Nonetheless, we think
that the revolution being wrought by object-oriented programming (and Java in particular)
can only be appreciated within a historical context.
In section 1.1 we begin, as do all revolutions, with a preamble justifying the overthrow of
the old regime. Most programmers are ensconced somewhere in the span of third or fourth
generation languages. They believe that the next generation of features is just what they need
to write the killer app or to deliver their project on time and within budget. Yet many design-
ers and managers, and most users who pay the bills, know otherwise.
In section 1.2 we make the concept of a finite deterministic state machine our philosophi-
cal foundation. We realize that the academic literature adopts the more general concept of a
Turing machine for the formal development of computing theory. Since engineering as cur-
rently understood can construct only finite machines, we limit ourselves to what is realistic.

1
2 Chapter 1: A Revisionist History of Programming

The thesis of our entire presentation is twofold. First, we view practical computers as
finite state machines; indeed, the underlying computing hardware falls into this category. Sec-
ond, translating the abstract state machine concept into software leads directly and naturally
to the concepts of object-oriented programming.
The main reason programmers didn’t discover object-oriented-programming sooner is
that a singular event occurred as electronic computing was being born. That event was John
von Neumann’s entry into the field. In section 1.3 we briefly introduce his computing insights
and architecture. In section 1.4 we present a simplified example of a von Neumann machine
along with the binary codes to control its operation.
In sections 1.5–1.7, starting with the first level of machine code, we retrace the steps of the
first three generations of software as it traipses further into the maze of the input-process-out-
put model. Our retelling culminates in third generation languages with structured analysis
and design.
In the actual history of programming, the development forked into several different paths
at this point, including early forms of object-oriented programming. Ironically, object-ori-
ented concepts could have been discovered and adopted from the very beginning of program-
ming, were it not for the overwhelming success of von Neumann’s approach. Indeed, the
object-oriented paradigm provides a most direct correspondence between hardware to soft-
ware.
In section 1.8 we present a simplified model of data structure in a third generation lan-
guage. In section 1.9 we discuss why this notion is inadequate and the problems that arise.
In section 1.10, we presume that most readers are familiar with third generation program-
ming, so we exercise poetic license to rewrite history as if third generation led directly to the
concepts of object-oriented programming as embodied in Java. We spare the reader the inter-
vening years of wandering through the wilderness of C++ and other near misses.
Having wed program and data, which were separated in von Neumann’s original creation,
in section 1.11 we provide a brief synopsis of the concepts of object-oriented programming.
We then survey the Java Virtual Machine architecture, which was designed to facilitate dis-
tributed systems running across a network.

1.1 This Is Not Your Parents’ Programming


Computers are useful when they can be made to simulate the characteristics and behavior of
phenomena that are of interest to us. Familiar examples are predicting the trajectory of a
rocket traveling to Mars; maintaining the books and records of a corporation; playing chess
against the world human champion; or playing adventure games in a fantasy world.
In the short history of computing, the capacity of machines to store and process informa-
tion has uncannily followed Moore’s Law, which predicted that computing capacity would
double every 18 months. Hardware development is one of the great technological triumphs of
the twentieth century, a model of efficiency. Yet, the parallel effort to specify the behavior of
these increasingly powerful machines is a struggle that has created a new class of billionaires
but stymied most corporations and individuals.
Why has success in software engineering proved so elusive? Many developers now realize
that software took a major wrong turn early on and has been in a cul-de-sac ever since. The
fundamental problems arise from two misconceptions. First, traditional software develop-
ment artificially separated the attributes (data) from the behavior (processing) of the phenom-
ena it attempted to model. This can be seen most clearly in the expression “Data Processing”
Finite Deterministic State Machines 3
and the input-process-output paradigm. Second, an overly simplistic application of the ana-
lytical technique in software design leads to top-down implementations that are inflexible and
unreliable in practice. 1
The software journey is littered with wrecks that promised a way through: centralized
processing; third generation languages; 4GL; structured analysis and design; expert systems;
interpreters; decentralized processing; client server; and a host of others. The early European
explorers of North America sought the Northwest Passage, believing each new discovery to
be the breakthrough. Alas, all stopped well short of the Pacific. Today, with the benefit of
accurate maps and weather surveys, we see the futility of the effort to cross from the Atlantic
to the Pacific via the north. We also marvel at the irony of the Isthmus of Panama that pro-
vides such a short overland journey between the Atlantic and the Pacific. But in our enlight-
ened hindsight, we often forget when you’re on the surface, you can’t see what’s over a hill or
beyond the horizon.
It is increasingly clear that we need to embark on a new direction for software develop-
ment. This direction is called object-oriented programming. Like any pioneering venture, it
may initially seem strange and difficult but eventually will become obvious. Many of the nav-
igation techniques developed during previous voyages are still useful in the new context, even
if the destination is different.
Java was always possible — it just took 45 years of others’ efforts before James Gosling
and crew found the isthmus. Our approach will be to retrace the development of computing
as it might have been had the object-oriented approach been discovered early on. Eventually
you will think it is obviously the correct way to do things and wonder why no one thought of
this earlier. We will avoid extensive comparisons to other environments except when there are
confusing similarities or differences for those arriving from those backgrounds.

1.2 Finite Deterministic State Machines


If you want to come across as a real geek, the next time someone asks you what you do, say
that you manage finite deterministic state machines. Although nearly all commercially avail-
able computers are just that, surprisingly few people who make a living with computers have
even a basic understanding of how they actually work. We start at the beginning.
To say that something is finite means that we could count its constituent parts and eventu-
ally finish. Thus, the number of humans on earth, the number of atoms in the sun, even Bill
Gate’s bank account are all finite. The natural numbers are not finite, since if I could finish
counting, you could always add one to my last result and get a new number. As of this writ-
ing, it remains an open question in physics whether the universe is finite, although the current
betting of most physicists is on infinite.
So, a finite machine is one that has some definite number of components. The number of
components could be quite large, so large that it could take billions of human lifetimes to
count them, but there is some specific number of them. Some of these components are private
— i.e., internal to the workings of the machine — while others are exposed to the public in
order to provide information about the machine to external users.
A state machine is a machine that can be characterized by the values of some collection of
attributes. At any time, the set of all these values is called the state of the machine. Often a
small number of these attributes characterize the overall state; they are sometimes referred to
as the state variables. The progression from one state to another is called a state transition.
4 Chapter 1: A Revisionist History of Programming

The possible transitions between states can be represented graphically in a state transition
diagram.
Summarizing, a finite state machine is one having a finite number of attributes whose
(finite) set of values characterize it at any time. A finite state machine can theoretically be rep-
resented by a (possibly very large and complicated) state transition diagram. In practice, we
usually look at a subset of the entire diagram to specify or understand the workings of the
machine.
A state machine is deterministic if there is some set of rules — procedures or formulas that
prescribe the maching’s behavior — by which one can transform any given state into the next
state. Put negatively, there is no random element in the behavior of the machine. Whether
human beings or the universe are deterministic has been the subject of endless philosophical
and religious debate, much of it content-free. Classical physics said that the universe was
entirely deterministic; quantum mechanics denies that even any single event is deterministic.
Computers are deterministic by design, but random events do sometimes intrude.
We need to add one more concept to complete our initial idea of computing: time. In addi-
tion to being finite deterministic state machines, computers are also governed by a clock. At
each regular tick of the clock (or multiple thereof), the state of the machine makes a transi-
tion to a new state as determined by the transition rules. The frequency of the clock tick is
called the cycle time of the machine. As of this writing, cycle times of personal computers are
in the 500 MHz range, meaning that roughly 200 million state changes can occur each sec-
ond.
For ease of use, most computers implement access control, in which attribute values can
be set or read from outside the machine in a controlled fashion. This is traditionally called
input/output management but we can also think of knobs, dials, sliders, etc. Computers also
implement some methods for invoking important state transition rules. In the traditional pro-
gramming world, this involved opcodes (operation codes) and programs; we can think of but-
tons and wheels that cause some action to take place.

1.3 The von Neumann Architecture


John von Neumann had one of the great minds of the twentieth century. He made significant
contributions to many disciplines, inventing several fields along the way. Any one of his
accomplishments would make the career of most of us. In the mid-1940s he turned his prodi-
gious intellect to the new field of electronic computing. The first computers, constructed of
vacuum tubes, were controlled by hand entering the (state transition) opcodes, which were
then executed sequentially. While working with the pioneers of ENIAC (Electronic Numerical
Integrator and Computer) at the Moore School of the University of Pennsylvania, von Neu-
mann made many profound observations in his 1945 paper “First draft of a Report on the
EDVAC” (Electronic Discrete Variable Automatic Computer). This paper formalized the con-
cept of a stored program computer. Here are some of the more important of von Neumann’s
observations:
• specifying a storage location by its sequential position allowed data to be addressed;
• opcodes could be placed into and executed from storage;
• a sequence of bits could be interpreted as data, an address, or an opcode;
• treating opcodes as data allowed instruction sequences to be modified during execution;
The von Neumann Architecture 5
• introducing test and conditional branch opcodes permitted control logic to be imple-
mented;
• introducing a transfer of control (i.e., branch or jump) opcode provided program segmen- 1
tation, and
• once programs are segmented, specialized routines could be written once and reused.
In order to implement his concepts, von Neumann proposed the first virtual computer
model in his 1946 paper “Preliminary Discussion of the Logical Design of an Electronic Com-
puting Machine.” A virtual machine is a model of a computer that specifies all its functional-
ity as a finite state machine, without a corresponding implementation of the machine in
hardware. In order to solve scientific computational problems arising from his work in other
fields, von Neumann designed and built a new hardware architecture, completed in 1951 at
the Institute for Advanced Study (IAS) in Princeton. It was so overwhelmingly successful that
shortly afterward most new computers were based on his design. Most computers are still
built around some form of this basic architecture even today. Virtual machines are now com-
monly implemented in software running on von Neumann architecture hardware.
The basic concepts of the von Neumann architecture are so ingrained in the history of
computing that they seem almost self-evident. They were not, but the concepts are simple and
powerful enough that the rest of us mere mortals can grasp them. Because it underlies so
much of what we do, we think that all programmers should have a basic understanding of
how the architecture works. One of von Neumann’s key observations was that programs and
data could be stored together instead of separately, as they had been. Ironically, it is the per-
vasiveness of this machine architecture and the resultant style of programming that led to the
dead end in software engineering. It is worth understanding the architecture in order to see
how and why this happened.
The most basic form of von Neumann architecture separates the functions of computation
into four specialized units. The memory (M in Figure 1.1 — also known as the data store,
storage, random access memory, or RAM) holds the data and the numerically encoded
opcodes that are to be processed. The central arithmetical unit (CA in Figure 1.1) performs
addition, subtraction, etc. The input/output channels (I/O as shown in Figure 1.1) transfer
data from/to the external environment. Finally, the central control unit (CC in Figure 1.1)
coordinates the other units and manages the execution sequence of the opcodes. The four
units can be represented schematically as shown in Figure 1.1.

Figure 1.1 Basic von Neumann architecture

CA CC I/O world

M
6 Chapter 1: A Revisionist History of Programming

The basic operation of this architecture is as follows. First, a sequence of binary digits rep-
resenting data and opcodes is hand assembled by humans and fed into the input device. In the
earliest days, this was via binary switches, but punch cards soon became the standard method
of input. The data is transferred from the input device to storage.
Next, the control unit executes the loaded opcodes. It begins by addressing a predeter-
mined location in memory, loading the opcode at that location into its instruction unit, and
then executing it. The opcode might load data from memory into a register; or it might per-
form an arithmetic operation on the data in two registers placing the result in a third; or it
might store data from a register to memory. Based on the result of this execution, the control
unit determines the address of the next instruction, loads it, and executes it. This process
repeats until the end of the instruction sequence or until an opcode aborts (e.g., divide by 0).
During this process, certain instructions place data onto the output unit, enabling people
monitoring the computer to inspect the results. Paper tape was the early standard for output
display.

1.4 A Baby von Neumann Machine


Let’s describe how a very simple von Neumann machine might work. First we make the
observation that by using base 2, all numbers can be represented as sequences of the binary
digits (bits) 0 and 1. This is very convenient for electrical engineers, who can design the mem-
ory, registers, and all components of the computer using simple electrical circuits.
Although strings of 0s and 1s may be very convenient for circuits, they are real drag for
humans. To make things easier for those of us who are binary challenged, we switch to base
16 (hexadecimal, or hex for short) whose representation consists of the digits 0 through 9
and the uppercase or lowercase letters A through F. We group each four bits into a hex digit
and we declare that 8 bits (a byte), represented by two hex digits, is the smallest unit of data
that we will process. A byte can hold 28 = 256 distinct values.
Our simple machine begins with three registers and a very short list of 1- and 2-byte
opcodes.

Instruction
Opcode Opcode Opcode
State Change
Binary Hex Mnemonic
Description
00000000 00 NOP no operation
00000001yyyyyyyy 01YY LD 00,YY load data YY into register
00
00000010yyyyyyyy 02YY LD 01,YY load data YY into register
01
00000011 03 ADD 00,01,02 add data in register 00 to
data in register 01 and
place result in register 02
00000100 04 MUL 00,01,02 multiply data in register 00
by data in register 01 and
place result in register 02
Our First Machine Language Programming Steps 7

Instruction
Opcode Opcode Opcode
State Change 1
Binary Hex Mnemonic
Description
00000101yyyyyyyy 05YY STM 02,YY store data in register 02 to
memory location YY
000000110yyyyyyyy 06YY IN YY copy data from input
device to memory location
YY
000000111yyyyyyyy 07YY OUT YY copy data from memory
location YY to output
device
111111111 FF END end execution
Here yyyyyyyy stands for an arbitrary binary string and YY its corresponding hex representa-
tion. The collection of binary opcodes is the machine language for our virtual machine. We
cheat a bit according to the binary purist, and create the instruction mnemonics next to the
opcodes to help us remember what the opcodes stand for. We will extend this initial instruc-
tion set in the next section.
We arrange that when we press the start button, our virtual machine will read the pro-
gram opcodes we feed it and copy them into contiguous storage starting at location 100. The
control unit will fetch the opcode value it finds at location 100 and execute it. After executing
this opcode, the control unit fetches the next opcode, executes it, and so on. It continues this
cycle until it encounters the opcode ‘FF’, when it stops execution.

1.5 Our First Machine Language Programming


Steps
Let’s construct a program sequence to add the numbers 2 and 3.

Opcode Opcode Instruction


Hex Mnemonic State Change Description
0102 LD 00,02 load data value 02 into register 00
0203 LD 01,03 load data value 03 into register 01
03 ADD 00,01,02 add data in register 00 to data in register 01 and place
result in register 02
0500 STM 02,00 store data in register 02 to memory location 00
0700 OUT 00 copy data value at location 00 to output device
FF END end execution
By assembling these instructions into a sequence, voilà, we have our first machine language
program:
010202030305000700FF
8 Chapter 1: A Revisionist History of Programming

or, for bit heads,


000000010000001000000010000000110000001100000101000000000000011100000 0001 1111111
Now all we have to do is feed this sequence into our virtual machine and press the Start
button. Some lights may blink and shortly the value 05 will appear on the output device.
While this example may seem trivial, the concepts are not. The idea that the behavior of a
general purpose machine can be dynamically specified to solve specific tasks without physical
modification was one of the great advances in human technology.
How can we make this program more useful? We will raise similar questions repeatedly
throughout this text, since the answers have driven many advances in software engineering.
The most obvious limitation is that to add numbers other than 2 and 3, we must reassemble
the opcode sequence. For example, to add 4 and 5, we would have to replace the first two
opcodes with:

Op Code Op Code Instruction


Hex Mnemonic State Change Description
0104 LD 00,04 load data value 04 into register 00
0205 LD 01,05 load data value 05 into register 01
resulting in the new program sequence
010402050305000700FF
The problem arises because the data values are tightly bound to the instructions; in fact,
the data is part of the instructions. This requires the program sequence to be reassembled
each time the data values change.
Suppose we had instructions to load data from an arbitrary memory location into regis-
ters, analogous to the instruction to store data from a register 02 into a memory location.
Then we could read the data from the input device into memory, then from memory into the
register. Each time we wanted to rerun the program with different data values we’d only need
to change the data and not the program. This was exactly what von Neumann wanted,
because his interest in building the computer was to solve applied mathematics problems in
physics and engineering. Most of these problems involved computing the solutions to differ-
ential equations given certain initial conditions. Once he had programmed the computation
algorithm, he could feed the initial conditions as data values. This allowed him to examine
many different scenarios without reprogramming.
We add two new opcodes to our virtual machine

Opcode Opcode Opcode Instruction


Binary Hex Mnemonic State Change Description
00001000yyyyyyyy 08YY LDM 00,YY load data from memory loca-
tion YY into register 00
00001001yyyyyyyy 09YY LDM 01,YY load data from memory loca-
tion YY into register 01
A Second Generation in Programming: Assembler Language 9
Our new and improved program is,

Opcode Opcode Instruction 1


Hex Mnemonic State Change Description
0600 IN 00 copy data value from input device to location 00
0601 IN 01 copy data value from input device to location 01
0800 LDM 00,00 load data value from location 00 into register 00
0901 LDM 01,01 load data value from location 01 into register 01
03 ADD 00,01,02 add data in register 00 to data in register 01, place result
in register 02
0502 STM 02,02 store data in register 02 to memory location 02
0702 OUT 02 copy data value at location 02 to output device
FF END end execution
If we assemble these instructions into a sequence, voila, we have our second machine lan-
guage program.
06000601080009010305020702FF
To load programs into our new virtual machine we load the program and append the data
after the final program instruction. Our revised program with data looks like:
06000601080009010305020702FF0203
We load this sequence into our machine and press the Start button. Lights blink and 05
appears on the output device.
In order to modify the program to compute ‘5 plus 6’, we need only change the data
sequence.
06000601080009010305020702FF0506
This time 0B appears on the output device.
With the addition of only a handful of instructions, this machine is theoretically powerful
enough to solve virtually any algorithmic problem. In practice, programming and execution
may take a very long time, but it can be done with a sufficiently powerful machine.

1.6 A Second Generation in Programming:


Assembler Language
The process of hand assembling machine codes is how initial computers were actually pro-
grammed. At the Institute for Advanced Study, graduate students hand assembled the
opcodes for von Neumann’s machine. When one of the graduate students wrote a program to
assemble the machine code automatically, von Neumann considered it a waste of valuable
computing time to do clerical work. When the FORTRAN concept was presented to von
Neumann, he dismissed it as merely an implementation of one of Turing’s ideas and couldn’t
understand why anyone needed more than machine language. Here was the father of modern
10 Chapter 1: A Revisionist History of Programming

computing illustrating the first erroneous programmer bias: real programmers don’t need
productivity tools or readable programs.
In fact, most of us are not capable of dividing eight-digit numbers in our minds at age six
or computing 40-digit sums mentally faster than the early computers, as could von Neumann.
It is easier for us to read the mnemonic codes than the corresponding hex codes. This realiza-
tion leads to the first step above machine language. It would be more productive for program-
mers to write programs using the mnemonic codes and have these translated into machine
code. How could this be done?
We first determine a binary encoding to represent characters. Then we choose a character
representation for each opcode. Next we hand assemble a machine language program that
reads character representations of opcodes and assembles them into machine codes. This
enables us to write our application program using the character opcodes and feed it into this
assembler program, which will then assemble the corresponding machine code. This
approach shows the beauty of interpreting binary streams as either data or opcodes; the out-
put produced by the assembler program can be fed back into the machine as data, interpreted
as machine opcodes, and executed.
Surprise! Since our mnemonics serve perfectly as character representations of the opcodes,
they serve as an assembler language for our virtual machine. To program in our new assem-
bler language, we first construct the sequence of mnemonic codes providing the desired pro-
gram function. After loading and executing the assembler program, we feed it our character
stream of mnemonic codes followed by the input data. Finally we feed the resulting machine
code output back into the machine to be executed. In terms of our previous table diagrams,
we start with the second column and the assembler produces the first column. Here is our
two-function program in assembler.
IN 00 ; copy data value from input device to location 00
IN 01 ; copy data value from input device to location 01
LDM 00,00 ; load data value from location 00 into register 00
LDM 01,01 ; load data value from location 01 into register 01
ADD 00,01,02 ; add data in register 00 to data in register 01 and place
result in reg 02
STM 02,02 ; store data in register 02 to memory location 02
OUT 02 ; copy data value at location 02 to output device
END ; end execution
We agree to use the semicolon character ‘;’ as a statement terminator. If we further arrange
that the compiler ignore any characters from the terminator to the end of line, we can use this
“dead space” for comments. Our program can be written more compactly as:
IN 00;IN 01;LDM 00,00;LDM 01,01;ADD 00,01,02;STM 02,02;END;0203
The assembler translates it to the equivalent machine code sequence as:
06000601080009010305020702FF0203
Although this assembler program won’t win a Pultitzer prize, it is certainly more readable
for most of us than the corresponding machine code. We decide to write all new programs in
assembler language. Unfortunately, we’re still stuck with the machine code for the assembler
A Third-Generation Programming Language 11
itself. But wait! Why not rewrite the code sequence for the assembler in our new language?
The result is a source program that we can feed into the original assembler, which will (amaz-
ingly) generate its own opcode sequence as output. Even the assembler itself can be written in 1
assembler language! Hence all machine language programs can be (re)written into assembler,
and only machines need read machine code.
This process of programmatically pulling ourselves up by our own bootstraps seems
miraculous but is common in the development of computing. The seeming magic occurs in
the two-stage process. First, write a preliminary version of a next-level program-generating
tool in the existing language. Once the new program is fully functional, reverse engineer it
using itself. The result is functionally equivalent to, and can replace, the preliminary version.
This can be quite tricky to do in practice, particularly when correcting errors and making
design changes during the process.
The addition of “branch,” “test,” “branch conditional” and other instructions to our
assembler language allows very sophisticated assembler programs to be constructed. In par-
ticular, it is possible to package commonly used code sequences so that they do not have to be
rewritten for each program. The idea is to create a block from the opcode sequences for a
general algorithm or procedure. Examples of such a block include I/O routines, mathematical
calculations such as exponentiation, and program initialization and cleanup. Using the
“branch” instruction with a destination address of this block, we can “call” to the corre-
sponding block from any location in our program, eliminating the need for duplicate code
in-line. Such a block is called by names such as “subroutine”, “procedure”, and “function.”
There are many common steps involved in the execution of programs including loading
the program, allocating storage, and reading and writing data from I/O devices. These rou-
tines are collected together into one (large) program called an operating system (OS), which
controls the various system resources and functions, including loading and executing pro-
grams. The OS is loaded first, when the computer is turned on. We then feed our program
opcode sequence into the OS as input data. The OS, in turn, sets up our opcodes and data
bytes and executes the program. It also provides a set of system subroutines, such as I/O, that
our program can call. When execution completes, the OS returns the machine to its initial
state (i.e., as it was before our program was loaded) by performing cleanup procedures. A
well-designed OS relieves the average program from handling mundane hardware-related
tasks and frees it to focus on solving its particular problem. A poorly designed OS will allow
user programs to place the machine in an inconsistent state, leading to a crash.

1.7 A Third-Generation Programming Language


For all its convenience, assembler doesn’t allow us to solve any problem that we couldn’t
solve in machine code. This fact is easily forgotten, but to confirm its truth we need only
remember that all assembler statements are translated into machine code. This closeness to
the underlying machine code makes assembler very powerful. There are a surprising number
of programs still written in assembler even today, including significant portions of some oper-
ating systems.
Assembler presents a considerable advance over machine language in terms of human effi-
ciency, von Neumann and other macho geeks not withstanding. Assembler is much easier to
write and read, which makes assembler programs more maintainable. With appropriate use
of comments in the source code file, it is sometimes even possible to read, understand, and
reuse programs written by someone else. This comes in handy on multi-programmer projects.
12 Chapter 1: A Revisionist History of Programming

The desire for increased efficiency on the human side of the process led to the next innovation
in programming languages.
While assembler looks more like a human language than machine code, it hasn’t won any
literary prizes. Software developers realized that they could extend the power of the assem-
bler to allow program statements that are more readable than simple opcode mnemonics.
Memory addresses are replaced by variables; test and branch instructions become
“if-then-else” statements; code sequences that are often branched become sub-routines which
are “called” with passed data parameters and return values. Such a souped-up assembler is
called a compiler. Early compilers translated a series of statements from the new format into
assembler language, which was then assembled to produce machine code. Two of the first
compilers/languages were COBOL (Common Business Oriented Language), which was cre-
ated to help programming in business applications, and FORTRAN (FORmula TRANsla-
tion), which was used for scientific computing. Both are still quite popular among
programmers of a certain generation.
Let’s take a cut at our own higher-level language. In order to achieve our first objective —
to replace memory locations with named variables — the compiler must associate a name
with a data type and a chunk of storage. To do this, the compiler will keep an internal map of
the memory associated with each name. This map also tracks what type of data is stored at
each location, so the compiler can figure out how many bytes to retrieve or store, as well as
how to interpret the data. Our first statements might look something like:
int operand1;
int operand2;
int result;
As before, we use the semicolon as a statement terminator to delineate statements to the
compiler and us. The three previous statements each tell the compiler to reserve enough stor-
age for an integer value and associate it with the specified name. For example, we can read
the first statement as, “Reserve space for an integer and associate the name ‘operand1’ with
that storage.”
Under the covers, our compiler creates a table and adds a new entry for each statement. A
row contains the (hex) representation of the characters for the name, a (hex) address for the
starting location in memory, a (hex) code standing for the “integer” data type, and a (hex)
length. This enables the compiler to generate the appropriate instructions to load or store
data at the associated address when it encounters a reference to the name. Borrowing termi-
nology from mathematics, we say that we have declared a variable with the given name.
The value of the variable is the resulting value of the data at the associated location when
the hex string of the appropriate length is interpreted as the specified data type. How do we
load specific values into storage? Again borrowing from mathematics, we say that we assign a
value to the variable. Unfortunately, many languages use the equals sign ‘=’ to represent the
assignment. This is not an accurate analogy and we think the best choice is that of APL (A
Programming Language — no joke) which uses the symbol, ‘‹ ’ that clearly shows the direc-
tion of assignment. However, when in Rome:
operand1 = 2;
operand2 = 3;
A Third-Generation Programming Language 13
The first statement tells the compiler, “Generate the instructions to load the (integer) value
2 into the storage location associated with the name ‘operand1’.” This style of interpreting
statements becomes reasonably straightforward if we read from right to left, “take the value 2 1
and store it in (the location associated with the name) ‘operand1’.”
Next we want to add (the values in) the operands and place the result in (the location
associated with) the result. We’ll do this with the single statement:
result = operand1 + operand2;
The compiler and we read this as follows. “Retrieve the (integer) value from (the location
associated with the name) ‘operand2’; add it to the (integer) value retrieved from (the loca-
tion associated with the name) ‘operand1’; store the sum (as an integer) into the (location
associated with the name) ‘result’.” The compiler allows us to drop the parenthetical parts
because it understands and implicitly fills them in.
We now come to one of the crowning achievements of third-generation languages. A func-
tion is modeled on the eponymous mathematical construct and has the same syntax; it is an
improved version of the assembler subroutine construct. If we are clever enough, we can
design our compiler to allow us to pass values to, and return a value from, a program func-
tion in much the same as a mathematical function.
We first declare the function to the compiler, so it knows the name of the function as well
as the names and data types of input parameters (if any) and the return value (if any). In our
language, the data type of the return value will be listed to the left of the function name, by
analogy to the way we declared variables. As with a mathematical function, the parameters
will be listed to the right of the function name within parentheses, separated by commas. For
example, following is a function that takes no input parameters but returns a value.
int getinput( )
This can be read as “declare a function named ‘getinput’ that takes no input parameters
and returns a value of integer type.” Another example that takes one input parameter and
doesn’t return a value is:
void setoutput( int value )
This is read, “Declare a function named ‘setoutput’ that takes a single input parameter of
data type integer named ‘value’ and returns nothing.” Another example is:
int add( int op1, int op2 )
This is read as, “Declare a function named ‘add’ that takes two input parameters of inte-
ger type named ‘op1’ and ‘op2’ and returns a sum of integer type.
Next, we define the function by listing the instructions of the subroutine block inside a
matched pair of braces.
int add ( int op1, int op2 ) {
return op1 + op2;
};
This says, “The definition of the function add() consists of a single statement that adds the
values of the two passed parameters and passes back the result as its return value.”
14 Chapter 1: A Revisionist History of Programming

The compiler associates the function name with the subroutine block and allows us to use
this name as shorthand for jumping to the subroutine block. At the point in our program
where we want to execute the subroutine block, we invoke or call the function by listing its
name and substituting actual values for the appropriate input parameters. The compiler gen-
erates code to associate the actual values with the parameter names. The usual way to pass
parameters is by constructing a stack and pushing them on the stack, although the IBM main-
frame architecture is a notable exception. For example, we can invoke the add() function
from another program.
int result;

result = add( 2, 3 );
This says, “Execute the subroutine block defined in add, substituting the value 2 for the
first parameter and the value 3 for the second parameter, and assign the resulting return value
of the execution to the variable result.”
As an exercise, define and declare a multiply() function analogous to add(). With it,
along with a few auxiliary I/O routines, we can write the third-generation version of our pro-
gram.
int operand1;
int operand2;
int result;

operand1 = getinput();
operand2 = getinput();

result = add( operand1, operand2 );


setoutput( result );

result = multiply( operand1, operand2 );


setoutput( result );
If our compiler is good, it will generate machine code very close to that produced by the
assembler in our previous section. There will probably be some additional overhead in the
instructions to pass the parameters to the functions. This is a small price to pay for the reus-
ability of the function code, especially when the function is more complex than our trivial
example.
Only an iconoclast would deny that this program is significantly easier to read and write
than its assembler language or machine language equivalents. Moreover, we can improve the
language by adding logical constructs such as ‘if’ and ‘else’ to improve on the assembler ‘test’
and ‘branch conditional’. This makes the control flow more explicit and intuitive. Inciden-
tally, our third-generation language is actually part of the C language, developed by Ker-
nighan and Ritchie in 1977.
Using these and other constructs, it is possible to apply the analytical approach to pro-
gram design. The starting point is to decompose the target phenomenon into input-pro-
Data Structure in Our 3GL 15
cess-output. The process is then successively broken down into smaller processes, each of
which is implemented as a subprogram. This principle underlies structured analysis and
design, and leads to a hierarchical program structure. The idea is to design routines with spe- 1
cific functionality and factor out common processes. In this way, very little is duplicated.
Related routines can be collected into libraries to be linked into the main program at compile
or run time.

1.8 Data Structure in Our 3GL


One problem of programming in second- or third-generation languages is that data items are
often viewed as isolated entities. In our example, the two ‘operand’ variables and the ‘result’
variable are associated with each other only by proximity of declaration. Passing variables
independently opens the possibility that the wrong variable gets passed or assigned; the state
of our system can become inconsistent. It would be better to wrap related attributes and pass
them in a package.
Most higher-level languages provide the ability to collect “atoms” such as integers, real
numbers, and characters into a more complex form called a record or structure. Program
design can then more closely reflect the organization of the process being modeled. For exam-
ple, in our 3GL, the code to define and declare a structure looks like:
struct two_function_calculator {
int operand1;
int operand2;
int result;
};
This can be read, “A data structure named ‘two_fuction_calculator’ comprises an integer
named ‘operand1’, an integer named ‘operand2’ and an integer named ‘result’.”
It is instructive to think of a structure as a template for laying out data elements. It tells
the compiler the names and data types of the elements for the layout. This template can be
viewed as an extension of the simple data types that our machine language can interpret.
Remember, a primitive data type is just an interpretation of a contiguous sequence of bits. A
structure allows us to define composite patterns that the compiler can interpret.
In order to use a structure template to reserve actual storage, we declare a variable with
this new type, just as we did for the atomic data types.
struct two_function_calculator calculator;
In order to access the elements of the structure, we use a notation in which the structure
name is followed by a dot ‘.’ and then the element name. So, for example:
caculator.operand1 = 2;

temp = calculator.operand1;
The first statement is read, “Assign the value 2 to the operand1 element of the calculator
structure.” The second is “Retrieve the value of the operand1 element of the structure calcu-
lator and assign it to the variable temp.”
16 Chapter 1: A Revisionist History of Programming

We rewrite our program in this new view as:


struct two_function_calculator {
int operand1;
int operand2;
int result;
};

add( struct two_function_calculator calc ) {


calc.result = calc.operand1 + calc.operand2;
}

multiply( struct two_function_calculator calc ) {


calc.result = calc.operand1 * calc.operand2;
}

struct two_function_calculatur calculator;

calculator.operand1 = getinput();
calculator.operand2 = getinput();

add( calculator );
setoutput( calculator.result );

multiply( calculator );
setoutput( calculator.result );
We can develop complex data forms by iterating the process of collecting data into struc-
tures; put otherwise, we nest data structures. For example, we can define a new structure that
contains integers as well as a calculator structure.
struct simple_scenario {
struct two_function_calculator calculator;
int rounding;
};
The “dot notation” extends easily to cover the compound case.
struct simple_scenario scenario;

scenario.rounding = 0;
scenario.calculator.operand1 = 2;
scenario.calculator.operand2 = 3;
Trouble in Structured City 17

add( scenario.calculator );
1
setoutput( scenario.calculator.result );

multiply( scenario.calculator );
setoutput( scenario.calculator.result );
Very sophisticated processes can be modeled using this technique.

1.9 Trouble in Structured City


Our thumbnail sketch of programming history is, of course, overly simplistic. We have omit-
ted significant topics and substantial issues and developments. Nonetheless, it does present a
reasonably accurate picture. Third-generation languages allowed software development to
move out of its narrow academic beginning into the commercial and public realms. The
march to automate manual processes was on. Corporations created departments with
pseudo-academic names like Electronic Data Processing, Management Information Science,
or Information Technologies to acquire and manage this new tool. Companies dedicated to
software development emerged and made their founders wealthy. Affordable personal com-
puters brought computing into the home. As hardware price performance doubled every two
years, massive gains in productivity due to computer applications were predicted.
The programming constructs outlined in previous sections make it possible to apply the
analytical techniques of process engineering to program design. The starting point is to
decompose the overall operation into input-process-output. A specification for the functional-
ity of the process portion is written and then broken down into successively smaller pro-
cesses, each of which is implemented as a subprogram. The goal is to design routines with
specific functionality and factor out common processes so that duplication is minimized.
Related routines can be collected into libraries that can be linked into the main program at
compile or run time. A hierarchical program structure results.
Similarly, the data representing an entity can be decomposed into successively smaller data
units. This top-down approach to data decomposition results in complex data structures that
eventually resolve into primitive types. It closely parallels the structured approach to program
design but is simpler because it doesn’t involve dynamic behavior.
Carefully following the prescriptions of structured analysis and design results in
well-structured programs with reusability of code. It was promised, and expected, that soft-
ware design and implementation should show roughly the same progress as hardware engi-
neering, yet this hasn’t happened. In practice, software projects are frequently behind
schedule and over budget. Programs often lack robustness and flexibility. Maintenance is dif-
ficult and time-consuming. And most damning, changes or extensions to the design specifica-
tion cause major disruption in the development cycle, often resulting in failure to deliver a
finished product on time, or at all.
The reasons for the lackluster success of software engineering are beginning to be under-
stood. Our analysis begins by rewinding to the beginning of our story. The original goal of
von Neumann in building his machine at IAS was to solve differential equations from applied
mathematics. In this scenario, the programs constitute the central feature while the data are
merely the initial conditions of the differential equations. From the birth of electronic com-
18 Chapter 1: A Revisionist History of Programming

puting, data was the neglected stepchild, viewed merely as input and output to the program.
As the field emerged, the main directions of research were in writing programs, creating lan-
guages, and designing assemblers and compilers. The subordination of data to program is
reflected in the name given to the discipline, “programming.” Most attention was paid to
simulating the dynamic behavior of a system in code, while the was often an afterthought.
Here is the first wrong turn taken in the journey of software engineering development: the
data and the program were separated and data was subordinated to program. Ironically, one
of von Neumann’s initial observations was that the opcodes could be placed into and exe-
cuted from the storage in which the data resided. His further observation — that opcodes
could be treated as data and modified during program execution — resulted in the concept of
self-modified code, which was theoretically useful in his development of automata theory. It
leads to enormously flexible programs, but programmers quickly realized that dynamic mod-
ification of opcodes led to programs that were virtually impossible to verify or correct. The
program ate its own trail of breadcrumbs and erased its own tracks! Consequently, the stan-
dard procedure is to load program opcodes into a portion of storage that is write-protected in
order to prohibit deliberate or accidental modification of instructions. This code segment is
separate from the data segment in which storage was regularly modified.
One serious problem with separating and subordinating the data is that the data structure
is often jammed into the program structure. When the reuse of program code is paramount,
subroutines are written to accept input in the most general form, i.e. separate parameters of
atomic type. In order to use these pre-packaged routines, programs often keep variables to
shadow the parameters as well as variables to synchronize state between the main program
and the various subroutines. It is difficult to collect these variables into a natural structure.
The resulting “global” variables are artifacts of the cobbling together of the subroutines. The
fidelity between the program and the real-world process it seeks to represent is degraded by
this plumbing code. Program structure can become so arcane as to be undecipherable to any-
one other than the author; sometimes even this is a stretch.
The second major problem is a result of the (mis)application of the structured analysis and
design process. In applying the analytical approach to representing a real-world process on a
computer, structured analysis and design makes a major implicit assumption: the structure of
the process being represented is essentially stable. We are unlikely to be successful in reducing
a target entity into components and then retarget each constituent if it is changing during the
analysis. The rapidly mutating retroviruses (e.g., HIV) have proved particularly difficult for
modern medicine, partially for this reason.
In the everyday world of program development, the stability assumption is rarely realized.
Design specifications are often subject to change from a number of sources. The phenomenon
under study can change. It could become necessary to model a larger or smaller portion of the
phenomenon. Our understanding could change. The choice of modeling tools or language
could change. Most likely of all, capricious users and programmers change their minds.
Changes to the specifications of a top-down system design cause effects that amplify down
the design hierarchy. This is understandable since each iteration of the approach reduces an
entity into smaller constituents. If the entity changes, these components need to change —
many will be completely invalid. Injure the trunk and all the branches suffer, perhaps die.
Another problem with classical programming is that the opcodes and atomic data types
are tightly coupled with the machine architecture and consequently are machine-dependent.
Code developed for one machine will not run on another. Even passing data from one
Back to the Future: Object-Oriented 19
machine to another is problematic due to different interpretations of so basic a concept as
byte order. Although languages such as C abstract the basic instructions and data types, there
are still problems with different word sizes. In addition, the utilities provided by the underly- 1
ing operating system differ in function and calling convention. Hence, even if code reusability
is achieved on a single platform, reusability across platforms is a Herculean task.
These and other problems surfaced in isolation over many years. Collectively they became
major obstacles in the 1980s when the levels of sophistication of hardware, software, and
networking made distributed systems feasible. The advent of the client-server paradigm broke
the stalemate between centralized and decentralized systems. The commercial acceptance of
Unix and C brought the first reality of multi-platform systems. Data sharing and program
reuse became paramount as systems became more complex.
It has become apparent that structured analysis and design don’t scale well in a changing,
distributed, heterogeneous environment. Many of the difficulties have a common theme: shar-
ing. It is relatively straightforward for a single programmer to design and implement an appli-
cation that receives its data from a single source and has a single user in one environment.
Things become much more difficult, however, when there is a team of programmers, multiple
data sources, many users, or a heterogeneous environment for development and deployment.
Combining these factors results in an exponential increase in complexity.
The difficulties arising from sharing are often synchronization problems. A variety of tech-
niques has evolved to manage synchronization. Source code management librarians require
modules to be checked out and in, preventing developers from stepping on each others’ code.
Databases provide locking to prevent multiple update requests from conflicting. Re-entrant
code and thread locking prevent multiple processes from tripping over each other. Transac-
tion management subsystems allow multiple users without conflicts. Unfortunately, these
techniques are complicated and are often grafted onto or forced into the development pro-
cess.

1.10 Back to the Future: Object-Oriented


Many of the problems in traditional structured analysis and design can be solved by adopting
an approach that combines data and programming in a more unified way, called Object-Ori-
ented Programming (OOP). The corresponding process of designing programs is called
Object Oriented Analysis and Design (OOAD). As much as the word is overused by market-
ing suits, object-oriented represents a new paradigm. For many who are accustomed to pro-
gramming in traditional environments, object-oriented seems difficult or unnatural at first. In
fact, object-oriented is actually easier once we let go of our programming preconceptions.
Some call it a holistic approach to programming; they are probably wearing tie-dyed shirts,
bell-bottoms, and colored glasses.
It is a great irony that the object-oriented approach could have been developed from the
beginning of programming. But von Neumann’s ability to construct his machine and solve
important computational problems in the development of the hydrogen bomb prevailed in
the days of the cold war. This pattern has been exhibited repeatedly in the history of comput-
ing. The best technology rarely wins the first time out. Instead, technology that is good
enough — but has the right political or marketing support — prevails.
Many approaches to object-oriented (OO) take the pedagogical approach of comparing it
to current languages. For example, since Java evolved from C and C++, it seems natural to
20 Chapter 1: A Revisionist History of Programming

contrast it to these languages. We will instead retrace our revisionist history and develop OO
from first principles.
Recall that computers are state machines. A (finite) collection of attribute values (from a
finite range) characterizes the machine at any time. A (finite) set of rules determines the tran-
sition of the machine from one state to the next. Often a limited number of attributes charac-
terize the most important states of the machine; these are called state variables. Some
attributes are internal to the working of the machine and are called private. Other attributes
are exposed to the external world and are called public.
One way to visualize this is to think of attributes as displays on the machine. The private
ones are inside, available only to the mechanic. The public ones are right on the front panel;
in most science fiction films these are blinking colored lights. It’s more convenient to use dials
and gauges. Some of these gauges, such as a power meter, only display data; these are called
read-only. Others, such as a clock, can be set as well; these are called read-write.
The way most everyday machines work is that there are controls such switches, buttons or
knobs that effect the state transitions in which we are interested. For example, you press
down the gas pedal of a car and it goes faster. What is actually happening is that mechanical
and electronic linkages are feeding more gas into the engine. In a computer, programs effect
the state transitions.
Our premise is that a computer is useful when it can reproduce or simulate the behavior of
some phenomenon that interests us. In other words, we want to can simulate some behavior
on the computer. With our understanding that a computer is a state machine, we must model
an external entity on a finite deterministic state machine. The easiest way to do this, it would
seem, is to map features of the entity onto the features of our machine. This amounts to mod-
eling the entity as a finite state machine.
How do we model an entity as a state machine? First, identify the attributes of the entity
and determine which are public, private, read-only, and read-write. These attributes are
mapped onto data types in the computer. Then we characterize the dynamic behavior of the
entity in terms of state transitions. These state transitions are implemented as functions. In
our new approach, we call the model an object, a data variable a field, and a function a
method. Note that both fields and methods are intrinsic to the object; this is called encapsula-
tion.
In practice, the creation of objects is a two-step process. First, declare to the compiler a
template or blueprint for the object, called a class. A class instructs the compiler about the
form of the fields and methods but, like the blueprint of a building, is neither the building nor
the construction crew. Once the class has been declared, we tell the compiler to construct an
object using the blueprint. Just as many houses can be built from a blueprint, so can many
objects be built from a class. At least in the case of object construction, we are guaranteed
that the general contractor constructs every object exactly according to the specification.
After construction, we can customize the object by setting attributes; we can also invoke its
dynamic behavior by calling methods with appropriate parameters.
Back to the Future: Object-Oriented 21
We return to our previous example to see how the two-function calculator looks using the
object-oriented approach. In fact, it’s similar to the third-generation structure, with some
important differences. First, we define the class. 1
class Calculator {
int operand1;
int operand2;
int result;

void add() {
result = operand1 + operand2;
}

void multiply() {
result = operand1 * operand2;
}
}
This tells the compiler that we are defining a new class (i.e., object blueprint) whose name
is ‘Calculator’. For clarity we agree on the convention that class names start with an initial
capital and the names of objects, fields, and methods start with a lowercase letter.
Next, we tell the compiler about the fields — how to reserve storage and interpret it as
data — for the object attributes. In our case, we have three integers named ‘operand1’,
‘operand2’ and ‘result’. We have simplified things temporarily by ignoring whether they
should be private or public.
Finally, we tell the compiler about the methods. We have two, named ‘add’ and ‘multiply’,
that do just what you’d expect. The add() method retrieves the values of the operand1 and
operand2 attributes, adds them and places the sum in the result attribute. The void keyword
preceding the method name tells the compiler that the method returns no value. Similarly,
multiply() places the product of the operand1 and operand2 attributes into the result
attribute.
Now we declare and construct a Calculator object with the following code:
Calculator calculator;

calculator = new Calculator();


The first statement isn’t too surprising, being completely analogous to declaring a variable
of type int. It tells the compiler that the name ‘calculator’ (note lower case initial letter) will
be associated with an object constructed from the class Calculator. The next statement may
look a little funky at first but just read it in our usual fashion from right-to-left. The expres-
sion Calculator() uses the name of the class with function syntax. What could this mean?
The key is the word ‘new’. The combination of new followed by a class name with function
syntax tells the compiler to construct a new object of the given class. Not so mysterious.
22 Chapter 1: A Revisionist History of Programming

Finally, the assignment statement tells the compiler to associate the newly constructed (pun
intended) object with the name ‘calculator’.
At this point we can use the object:
Calculator calculator;
calculator = new Calculator();

calculator.operand1 = 2;
calculator.operand2 = 3;

calculator.add();
System.out.println( calculator.result );

calculator.multiply();
System.out.println( calculator.result );
The two statements with equals signs tell the compiler to assign the values 2 and 3 to the
attributes operand1 and operand2 of the object calculator. The next statement invokes the
add() method of the object calculator. In pure object-oriented parlance, we send a message
to the object calculator telling it to add itself. The next statement invokes a Java system out-
put routine to print to the console a line containing the value contained in the result
attribute of the object calculator. Similarly, we tell the object to multiply itself and then we
output its result.
The object-oriented code we have created is actual Java code that will execute. To run the
code, let’s start JBuilder, an Integrated Development Environment (IDE) for Java from Bor-
land, that makes developing Java programs easy. Here are the steps. Pull down the File menu
in JBuilder. Click on the New Project… item. In the Project Wizard dialog box, change the
File entry so that it ends with ‘\tutorial110a\ Tutorial110a.jpr’ and change the Title entry to
read ‘Tutorial 1.10.a’. You can also type your name in the Author entry if you wish. Now
click on the Finish button and JBuilder creates the project file.
Pull down the File menu again and click on the New... item. In the New… window, double
click on the Class icon. In the New Java File dialog box that appears, change the Class Name
entry to ‘Application’ and click the OK button. JBuilder adds a file named ‘Application.java’
to the project; it can be seen in the Navigator pane in the upper-left corner. The source code is
displayed in the Source pane.
Now enter the following source code into the Source pane:

Tutorial 1.10.a
class Calculator {
int operand1;
int operand2;
int result;

void add() {
Back to the Future: Object-Oriented 23

result = operand1 + operand2;


}
1
void multiply() {
result = operand1 * operand2;
}
}

public class Application {

public Application() {
Calculator calculator;
calculator = new Calculator();

calculator.operand1 = 2;
calculator.operand2 = 3;

calculator.add();
System.out.println( calculator.result );

calculator.multiply();
System.out.println( calculator.result );
}

static public void main(String[] args) {


Application application = new Application();
}
}
To compile and execute the program, pull down the Run menu and locate the Run item that
has a green triangle at its left, then click it. The problem is that our application completes and
the console window closes before we can see it! To remedy this, pull down the Project menu
and select the Properties… item by clicking on it. In the Tutorial110a.jpr Properties dialog
that appears, select the Run/Debug tab. In the Console I/O group, click Close console win-
dow on exit to deselect it, and click the OK button.
Now run the application once more by clicking on the Run menu item or by pressing the
F9 key. We can see the output on the console followed by the message “Press Ctrl+C to termi-
nate application…”. Our object-oriented calculator works! Press the Ctrl+C key combination
to close the console window.
We won’t explain the wrapper code in detail, but we’ll make a few notes for the curious.
Our Application class is special to Java because it contains a unique method named ‘main’
24 Chapter 1: A Revisionist History of Programming

with a particular signature. At run time, control is initially transferred to this main() method.
Normally executed Java programs have a class with such a main() method.

1.11 A Philosophy of Object-Oriented Programming


The scientific method is one of the most powerful tools of human intellectual endeavor. It
posits that real world phenomena can be represented by models that make objectively verifi-
able predictions about real world events. There is a profound difference between the scientific
method and other predictive models — such as spiritualism, self-proclaimed sports experts,
ultra-high-end audio, economics, and astrology. The scientific method requires the results of
predictions be repeatable as well as independently verifiable by objective means. Unless it can
be corrected, a model whose predictions disagree with reality must be tossed onto the junk
heap. If it doesn’t agree with existing and new data, it’s a nonstarter. If it doesn’t correctly
predict something we didn’t already know, it’s useless. There can be no exceptions.
Analysis is one of the cornerstones of scientific thought. It breaks down a complex process
or problem into an equivalent collection of smaller ones. For the technique to work, two cri-
teria must be satisfied. First, each of the subproblems must be simpler than the original one.
Equally importantly, each of the subproblems must be independent or uncoupled from the
others. Repeated application of the technique leads to a hierarchical organization structure.
In fact, anywhere there is a hierarchical structure, it was likely produced by the analytical
approach.
It is easy to see the appeal of the analytic technique. Properly applied, it leads to localiza-
tion of problem resolution and a high leverage of knowledge. Given our fundamental urge to
impose order on our surroundings, hierarchical structure has become the de facto organiza-
tion in armies, governments, corporations, religious bodies, etc. For example, Napoleon’s
invention of the concept of a bureaucracy to run his state initially resulted in a dramatic
advance in productivity. Each function was specialized into a bureau, which was in turn bro-
ken down into more specialized bureaus. The specialization of function led to higher produc-
tivity since individuals were required to learn and perform only a few tasks well instead of
many tasks poorly.
The analytic technique is a top-down approach that begins with a well-specified entity and
results in several well-specified entities that are simpler than the original one. The key is in
finding an optimal decomposition that meets the two criteria. The success of analysis is not
guaranteed by merely reducing a problem into subproblems.
Difficulties quickly arise from oversimplification and misapplication of the analytic tech-
nique. The fundamental quandary is that often only the first criterion is met. In mundane
applications, it is rarely the case that the subcomponents are independent or even loosely cou-
pled. For example, the term “bureaucracy” now has a negative connotation because depart-
ments don’t always perform the functions expected but do interact in complex ways — office
politics. Armies must put soldiers through boot camp to train recruits not to think for them-
selves but to obey orders. Corporate protocol is a stylized ritual, eventually requiring
repeated meetings stressing the need for “team play.”
This is also the case in software development. To be successful, we must first correctly
understand all relevant aspects of the real-world phenomenon being modeled. Not just the
obvious attributes and normal behavior, but every possible interaction must be considered.
This knowledge is usually codified into a set of design specifications that are the first step of
the reductive portion of system development. Second, we must develop a model that accu-
A Philosophy of Object-Oriented Programming 25
rately represents all specified attributes and behavior patterns. Third, we must transfer the
model into a design for a set of classes, with associated variables, methods, events and event
handlers. This step also includes the design of GUIs, reports, databases, etc. Fourth, we must 1
implement all these entities. Then we must verify each of the components against its design
specifications.
At this point, most programmers consider the hard part of the development process to be
completed. They began system development with high hopes and imminent delivery dead-
lines. But the individual components must still be assembled into modules and their interac-
tion must be verified. In particular, the interoperability of the parts must verified: the modules
must be connected, often across a network, and the overall application logic tested.
This is where many software projects fail. Why? Analysis extracts its due from sloppy
users. Only in these final steps do many designers discover that they have not modeled the
phenomena correctly or decomposed it cleanly. The symptom is component interaction,
which is more complex than anticipated. The interaction of the various components is the
denouement for improperly designed systems. When the components are not independent, or
decoupled, cobbling them together into a functioning whole may be impossible.
Synthesis, the process of combining parts to create a more complex entity, is the inverse of
analysis. Synthesis is bottom-up and is more open-ended than a top-down decomposition. A
major difficulty in synthesis is dealing with the interactions of the parts. In general, the com-
plexity of interaction rises exponentially with the number of entities. Thus, it is extremely
important to ensure a limited number of well-specified interactions for synthesis.
When analytic reduction is poorly applied in software design, it produces coupled compo-
nents. The resulting system integration task proves to be an unanticipated and difficult syn-
thesis. Toss in users who routinely change their minds and add functionality at the last
minute, and the exponential complexity quickly becomes unmanageable. This is how a
twelve-month project can be 18 months behind schedule after eleven months.
An essential element of object-oriented design, called encapsulation, ensures that all
aspects of an object’s design are collected together in a single place. Encapsulation is aug-
mented by data hiding, in which the internals of an object’s implementation are not visible to
other objects. All access to an object’s state is through get/set methods that we call accessors.
Another aid to decoupling in Java is an event model that allows objects to communicate state
changes to each other. An event is the association of a state change in one object, called the
trigger, and a method in another object, called the handler.
An object that obeys Java’s rules of encapsulation, data hiding, and events is called a Jav-
aBean — the trite name obscures a deep concept. The great advances in computer hardware
that followed the discovery of solid state transistors derived not from transistors, which were
merely smaller, cheaper, and more efficient versions of vacuum tubes. The real revolution was
the introduction of integrated circuits. An integrated circuit models on a single chip the
behavior of a circuit containing scores, hundreds, thousands, or even millions of individual
components. The user of the chip need know nothing about the internal implementation of
the chip; the input/output specifications are sufficient.
A more complex circuit can be synthesized using combinations of discrete components,
including existing integrated circuits. This new circuit can be tested and adjusted until its
behavior is exactly right. Then it can be modeled, in turn, as a new integrated chip. Each new
generation represents a higher level of complexity of behavior, but the complexity of design
remains manageable because the interactions in the synthesis process are well specified: each
26 Chapter 1: A Revisionist History of Programming

chip is characterized by its input/output specifications. Chips can be bought off the shelf and
combined in predictable fashion, with no knowledge of design or manufacturing techniques.
A JavaBean is a software component, the analogue of a hardware integrated circuit. Com-
plexity of behavior of a collection of discrete components (the fields and methods) is bundled
together into a single object. By following encapsulation, data hiding, and other principles of
object-oriented design, off-the-shelf objects can be synthesized into user-designed programs of
increased richness. Using the event model to connect objects corresponds to connecting chips
using their I/O specs.

1.12 The Java Virtual Machine


Programs in traditional languages are compiled into native machine code that is loaded and
executed by the operating system. Recall our acquaintance in section 1.3 with the concept of
a virtual machine as a software model of a physical machine design. Java programs are com-
piled into opcodes that are loaded and executed by the Java Virtual Machine (JVM). This
implies that the JVM must be implemented on each platform on which Java runs. In order to
understand the nature of Java, it is worth examining the JVM model of computing in more
detail.
To provide architecture-neutral, portable, and secure code, Java decouples the develop-
ment and compilation environment from the run-time environment. Java programs can be
developed in any ASCII editor and compiled by any Java compiler. The result of the compila-
tion is not native hardware instructions for any particular platform. Rather, every Java com-
piler produces byte codes — abstracted machine instructions that are hardware-independent.
A given source file will produce the same byte code output regardless of the editor, develop-
ment tool, compiler, operating system, or hardware platform on which it is compiled. More
specifically, every Java program comprises a collection of source files, each containing defini-
tions of classes and objects. The result of compiling a Java source unit is a file containing the
byte code representation of its class(es), unsurprisingly called a class file. Restating our previ-
ous remark, a Java class file is independent of the environment in which it is produced.
Before discussing the Java run-time environment, we make a few more observations on
the compiler environment. Java is a strongly typed language. This means that every data ele-
ment in a program must be declared from a list of data types known to the compiler. It also
means that a complete signature (i.e., the data types of all parameters and the return value)
must also be specified to the compiler for each method. Strong typing allows the compiler to
catch a broad class of errors resulting from incorrect or incompatible data conversions. Many
such errors are obvious, but others are insidious.
Programmers from loosely typed environments, such as APL and LISP, may consider
strong typing a programming straightjacket. Managers would probably respond, “If the
jacket fits, wear it.” We prefer to think of strong typing as a contract of discipline between
each developer and the compiler, drawn up and signed in calm consideration. Later, in the
heat of the development battle, when it is all too tempting to cheat, the compiler acts as ref-
eree and enforces the rules of engagement.
Some decisions that are usually made by the compiler in traditional languages are deferred
to run time in Java. One example is memory layout, which differs according to byte order
and other hardware and operating system characteristics. Because Java has no pointers and
memory allocation is performed completely “under the covers,” a program cannot deliber-
ately or accidentally address storage outside permitted boundaries. This restrains some cher-
The Java Virtual Machine 27
ished programming tricks, but also eliminates a common source of program bugs and security
leaks. Another deferred decision is code optimization. You can’t optimize machine code until
you know the specific characteristics of the machine. This and other factors currently make it 1
difficult for Java to achieve the performance levels of native compiled languages.
The class files resulting from compilation are delivered as input to the Java run-time envi-
ronment (JRE). This can occur directly from the compiler, via the file system, or over the net-
work. Implicit in this is that the run-time environment may be in a different physical location
or on a different platform than the compiler. The first stage of the run-time environment is the
class loader, which directs the loading and interpretation of the byte code class representa-
tions. The class loader invokes the byte code verifier, which considers every incoming class
file as untrusted, due to its unknown origin. The byte code verifier passes the byte codes
through security checks to prove that the code has been produced by a legitimate Java com-
piler and has no violations that could corrupt the Java interpreter.
Once a class file passes verification, it can be safely passed into the run-time environment.
When the class loader encounters references to other class files that it does not currently have
in storage, it loads the required class files and resolves the references. This dynamic loading is
as close as Java comes to the “link” step of traditional compiled languages. When the
required class files are all present and accounted for, the references have all been resolved and
the code is ready for execution. Of course, the required class files must be available to the
JVM or else it will not be able to resolve the references.
The simplest way for the JVM to execute the application is to pass the fully resolved byte
codes through a Java interpreter. An interpreter dynamically reads byte codes and converts
them into appropriate instructions for the underlying platform. An alternative is to pass the
byte codes through a code generator that produces native machine code that is then executed.
A hybrid approach is a just in time compiler (JIT), in which optimized machine code is sub-
stituted for the byte codes after the initial interpretation. The area of JVM performance opti-
mization is currently a very hot topic in the Java world.
Finally, the code is executed in the run-time environment. It is important to realize that in
a pure Java system, the application makes no direct access to the underlying operating system
or hardware. The JVM acts as an intermediary for all resource allocation and use. In fact, it is
possible to implement the JVM directly onto the hardware platform, without any underlying
operating system. This notion of a Java OS is finding practical use for “Java on a chip” appli-
cations to control microchips in appliances, security devices, cars, etc. Effectively, the JVM
makes the OS recede into the background (or into oblivion) from the developer’s point of
view. This makes certain people very uncomfortable, particularly those who rely on propri-
etary OS features for their advantage; coincidentally, there is a disproportionate concentra-
tion of discomfort in the Seattle area.
It is relatively easy, but not encouraged by Sun’s marketing suits, to call programs in other
languages from Java. To do so, you can declare a proxy method using the special modifier
native; it has no implementation block. Then use the Java Native Interface (JNI) to connect
the proxy method to the foreign code in an external file. You can invoke the proxy method in
the usual Java fashion. Conversely, it is relatively difficult to call a Java program from
another language. This entails running the JVM, and is not for the faint of heart.
There are advantages to integrating Java with other programming languages — for exam-
ple, the reuse of existing code. It also allows performance-critical routines to be written in a
high-performance language such as C or C++ until Java performance becomes comparable.
28 Chapter 1: A Revisionist History of Programming

On the downside, this makes the resulting code nonportable and lessens the benefits of Java,
particularly in a distributed system or a network environment.

S-ar putea să vă placă și