home
***
CD-ROM
|
disk
|
FTP
|
other
***
search
/
Mega CD-ROM 1
/
megacd_rom_1.zip
/
megacd_rom_1
/
MAGAZINE
/
MSJOURNA
/
MSJV4_2B.ZIP
/
PROJECT.ZOO
/
pdesign.txt
< prev
next >
Wrap
Text File
|
1988-10-07
|
30KB
|
727 lines
Object-Oriented Programming in the Real World
by Zack Urlocker, The Whitewater Group
Introduction
Object-oriented programming is a popular theme. Although
object-oriented programming languages have been around for
years on expensive dedicated hardware, the availability of
efficient object-oriented languages on PCs has created a much
wider audience.
Many articles have been published on the virtues of OOP, but
nothing demonstrates the use of encapsulation and inheritance
better than a real example. In this article I will describe
how I designed a critical path project manager, PC-Project, in
Actor, an object-oriented language for the PC.
The application was easier to develop and understand using an
object-oriented approach. The development took about two and a
half man-weeks, though it was spaced out over a longer period.
This is about one half to three-quarters of the time I estimate
it would have taken me in C even though I have more experience
in C than in Actor. In addition, there is much higher degree
of code reuse than there would have been in C. Even the
complexities of developing a windowing user-interface were
easier to tame with an object-oriented language.
The project manager is similar to commercially available
products such as SuperProject or MacProject, but on a smaller
scale. It allows you to define a project as a group of related
tasks and then compute the critical path of the project, it's
total cost and so on. The critical path is the sequence of
activities that must be completed in the scheduled time to meet
the project deadline. Some activities that are done in
parallel may not be on the critical path, and thus they have
additional "slack time". Since Actor is a development
environment for Microsoft Windows, PC-Project also runs under
Windows making it easy to use.
A standalone version of PC-Project that runs under Microsoft
Windows and full source code are available for downloading from
many BBSs or directly from the author.
What is object-oriented programming?
Unlike traditional languages where data are separate from the
operations you perform on them, object-oriented languages tie
data and functionality into modules known as objects. Each
object has a set of attributes (data) and operations. Objects
include things found in traditional languages such as arrays,
strings, numbers, characters, files and so on. Actor includes
Object-Oriented Programming in the Real World 1
a rich set of predefined objects such as stacks, queues, sets,
dictionaries, windows, dialog boxes and others.
Object-oriented design
When designing an application in a procedural language such as
C or Pascal you might begin by designing the data structures
and then determining the functions that are required. In
object-oriented languages, the data and functionality are
combined in objects, so we don't consider the program in terms
of routines that manipulate passive data, but rather as
collection of active objects that perform operations on
themselves. Therefore, we need to determine which objects will
make up the system. The easiest way to do this is to develop a
logical model of the system we are trying to create. In cases
where there is no clear logical model, we can determine the
objects based on the user interface. For example, a
spreadsheet might include a spreadsheet window and cells as
various types of objects.
One approach to the project manager is to have a project object
which is a network of nodes. Each node is an activity: either
a task or a milestone. Tasks are activities which consume time
and resources. For example, developing software is a task that
will take time and money. A milestone is used to mark the
completion of some tasks. For example, you may have a
milestone to indicate that the specs are written for a product
and it is time to begin programming and writing the user
manual. The milestone itself doesn't take any time or have any
cost; it just marks the completion of a phase in the project.
Since our application will run under Microsoft Windows we will
also need to create a project window object that will be able
to draw a diagram of the network, handle menu commands and so
on.
As much as possible, we want to preserve the functional
divisions found in the logical model. For example, the project
object shouldn't be concerned with details of how the total
cost of a task is calculated; let the task worry about it! The
only thing the project needs to know is a way of updating its
total if the cost of a task changes.
Good object-oriented design encourages the creation of abstract
data objects that clearly define public protocol and a hide
implementation details. Although this approach is not required
by Actor, it is strongly encouraged and can minimize
maintenance headaches. The object-oriented design approach
requires more work "up front", but generally this pays off in
the long run by providing better encapsulation than the
traditional procedural approach.
Figure 1 lists some of the objects that will be used in PC-
Project.
Object-Oriented Programming in the Real World 2
Figure 1. Objects in PC-Project.
Network a generic network of nodes with a start and end
Node a generic node capable of connecting itself
Project a network that knows the critical path method
Activity a node with an earlyStart and lateFinish
Milestone an activity that uses no time or resources
Task an activity that has time, resources and cost
PERTTask a Task where the time is estimated by PERT
Resource used by a task; has a name and cost
ProjWindow a window that can display a network diagram
GanttWindow a window that can display a Gantt chart
Messages and methods
An object is capable of responding to messages that are sent to
it by other objects, by itself, or by the system. For example,
you can print an object by sending it a print message.
Examples of message sends are shown in Figure 2.
Figure 2. Examples of message sends.
print(A); /* all objects know how to print */
draw(rect, hDC); /* draw a rect in the display */
cost := calcCost(P); /* whatever P is, get its cost */
reSize(aWindow, wp, lp); /* the system tells us to resize */
Message sends are similar to function calls in procedural
languages. However, there are some important differences.
First of all, the first parameter is the receiver of the
message; the other parameters are arguments. Secondly, the
message makes no assumptions about the "type" of the receiver
or the arguments; they are simply objects. This provides us
with a great deal of flexibility since we can have different
objects respond to the same message in their own way. This is
known as polymorphism, meaning "many behaviors". For example,
we might have a calcCost message that can be sent to either a
project or a task and it will calculate the cost using the
appropriate algorithm.
The implementation of a message for a class of object is called
a method, and it corresponds to a function definition in a
procedural language. Figure 3 shows a sample method definition
for calcCost for the Task class.
Object-Oriented Programming in the Real World 3
Figure 3. A sample method definition.
/* This method defines the calcCost message for the Task
class. Self refers to the task that receives the message.
Time, resources and cost are instance variables defined
for the Tasks.
The cost is the fixedCost plus the variable cost of each
resource used by the task. If the cost changes, send a
message to the network to update its cost. */
Def calcCost(self | oldCost)
{
oldCost := cost; /* store our old cost as a temp */
cost := fixedCost; /* starting value for the cost */
do(resources, /* loop through the resources */
{using(res) /* res is the loop variable */
cost := cost + getFixedCost(res)
+ getTime(self) * getVariableCost(res);
});
if cost <> oldCost then
updateCost(network, cost - oldCost);
endif;
^cost; /* return the new cost */
}
Actor source code looks a lot like Pascal or C. Most
programmers find this makes learning Actor quite easy. The
method definition begins with the Def keyword followed by the
name of the method, the receiver (always referred to as self),
any arguments, and after a vertical bar, any local variables.
The do message is defined for all collection classes and allows
us to loop through the resources array referring to each
element of the array in turn. If later we decide that the
resources should be implemented as a lookup table or a set,
calcCost will still work correctly since all of these
collections understand a do message.
Actor is an interactive environment and so as soon as we write
a method we can test it out. Methods are written in an editor
known as a browser. As soon as a method is written, it is
immediately compiled and can be tested.
Classes of objects
Every object belongs to a class. A class is like a data type
in procedural languages, but much more powerful. Classes
define the data that make up objects and the messages that
objects understand. For example, a stack class (actually
called OrderedCollection) includes instance variables
firstElement and lastElement and responds to messages such as
push and pop. Instance variables are attributes of an object
and are like fields in a record structure in C or Pascal. We
can create new classes and define methods using the browser.
Object-Oriented Programming in the Real World 4
Rather than access the private instance variables of a class
directly using the "dot notation" (e.g. stack.firstElement) we
will encapsulate the data by using messages only. That way we
reduce the dependencies on the implementation.
For example, if we need to get the time required of an activity
x we should send a getTime(x) message rather than refer
directly to the instance variable x.time. It doesn matter if x
is a Milestone, a Task, a PERTTask or even a Project; as long
as it knows how to respond to a getTime message. This can make
the critical path recalculation algorithm easier to write since
we eliminate special cases for Milestones; we just define it's
getTime method to return zero. This results in significant
code savings.
Inheritance
What really makes classes powerful is the use of inheritance.
We can create descendant classes that build upon the
functionality already found in the system. For example, the
OrderedCollection class mentioned earlier descends from the
Array class. Similarly, we can create descendants of classes
like Window and Dialog to build the user interface to PC-
Project without having to know the more than 400 Microsoft
Windows function calls. Automatically our descendant classes
will include much of the generic windowing behavior that users
expect in a Windows application.
Inheritance allows us to re-use code in cases where something
is "a little different" from an object that already exists.
For example, I defined the classes Network and Node to provide
generic network connection capabilities. We can create a new
network, connect some nodes to it, disconnect nodes and so on.
Not very exciting, but it enables us to factor out a fairly
easy part of the application, test it and then forget about it.
Since there is nothing inherantly network-like or node-like in
the system already, these classes descend directly from class
Object.
Rather than keep track of the connections in the network, the
node themselves maintain a set of input nodes and output nodes
as instance variables inputs and outputs. The network, then,
just has to maintain the start and end nodes. The network is
actually implemented as a doubly-linked list, but this will be
considered "private" information and will be encapsulated with
connect and disconnect methods.
The connect method is implemented as addOutputs and addInputs
messages since both the nodes must know about the connection
that is made. These two steps could easily have been done in
the connect method, but by having each node manage its own
instance variables we are more flexible and can accomodate
networks made up of other networks more easily.
Object-Oriented Programming in the Real World 5
Figure 4. The connect method.
Public protocol :
connect(N1, N2); /* e.g. N1 -> N2 */
Private implementation:
/* Connect self->aNode by updating self's outputs and
aNode's inputs. Also, aNode should know it's network
and the position should be set. */
Def connect(self, aNode)
{ addInputs(aNode, self);
addOutputs(self, aNode);
setNetwork(aNode, network);
setPosn(aNode, self);
}
/* To connect node n1->n2, n1's outputs must contain n2. */
Def addOutputs(self, aNode)
{ add(outputs, aNode);
}
/* To connect n1->n2, n2's inputs must contain n1. */
Def addInputs(self, aNode)
{ add(inputs, aNode);
}
The setPosn method is another example of private protocol.
This method is used to update the onscreen position of the
node. The disconnect method is written in a similar fashion.
Interactive tools
Once we've defined the network and node classes we can create a
network and examine it to make sure it's as we expect. Actor
encourages an interactive programming style and has a rich set
of tools to make it easy to test and debug code one piece at a
time.
If any of our methods should cause a runtime error a source
code debugger will automatically pop up and we can fix the code
on the fly and then resume execution. Any time we want to
examine an object we can call up an inspector and see the
values of the instance variables. We can also inspect the
instance variables of an object from within another inspector
and trace through an entire network easily.
Actor also has a code profiler that can be used to determine
which methods are executed most to aid us in optimizing our
application. We can then selectively use early binding
techniques to speed up critical code by eliminating the message
Object-Oriented Programming in the Real World 6
send overhead. With the proper use of early binding in
critical areas we can improve performance by about 25 to 30%.
The project manager classes
Once we're sure that our generic Network and Node classes are
working properly we can define the Project, Activity and other
classes listed in Figure 1.
The Project class will descend directly from the Network class
and include additional functionality related to the
application. For example, we need to be able to recalculate
the critical path of the project.
The Activity class is a formal class that will group behavior
that is common between its descendants Milestone and Task. We
won't ever create Activities themselves (except for testing),
instead they will always be either a Milestone or a Task.
Alternatively, we could have had Task descend from Milestone,
but then we would end up inheriting inappropriate behavior.
Similarly, we could define a class called PERTTask that
descends from Task that uses the Project Evaluation and Review
Technique (PERT) for estimating time. The only methods we
would have to write would be those related to calculating time;
everything else would be inherited.
By using inheritance we reduce the amount of code we need to
write and the amount of testing that needs to be done. Once a
method is written for the Activity class we know it will work
for any descendant classes. Figure 6 shows a class tree of the
classes in PC-Project. Note that all classes descend from the
Object class.
Figure 6. Class tree diagram.
Object
|
-----------------------------------------------------
| | | | |
Window Dialog Network Node Resource
| | | |
ProjWindow PDialog Project Activity
| |
MStoneDialog --------
| | |
TaskDialog Milestone Task
| |
PERTDialog PERTTask
Object-Oriented Programming in the Real World 7
The recalc algorithm
The user can set automatic recalculation to recalculate the
critical path any time a change is made; or alternatively he or
she turn off automatic recalculation, make several changes, and
then recalculate the entire network.
The critical path recalc algorithm must make two passes: a
forward pass, recalc1, is used to compute the earlyStart times
of activities; a backward pass, recalc2, computes the
lateFinish time and the slack. When an activity's time
changes, it sends a recalc1 message forward to all of its
outputs, who send it to their outputs and so on.
Like today's popular spreadsheets, the project manager uses a
minimal recalc algorithm so that only parts of the project that
actually change are recalculated. The recalc1 message is only
passed on if the earlyStart or time changes, otherwise the
outputs don't need to be recalculated.
Figure 7 shows the critical path recalculation algorithm.
Figure 7. The critical path recalculation algorithm.
Network class methods:
/* Recalculate the entire network. Invalidate all nodes
so that a full recalc will be forced. Then do both the
forward and backwards recalc passes. The forward pass
must be completed before doing the backwards pass. */
Def recalc(self)
{ invalidate(start, new(Set,10)); /* clear all nodes */
cost := 0; /* recalc cost */
recalc1(start, true, true); /* force entire recalc */
recalc2(self); /* backwards pass */
}
/* Do the backwards pass only starting from the end node.
The end node is always critical. */
Def recalc2(self)
{ recalc2(end);
end.critical := true;
}
Object-Oriented Programming in the Real World 8
Activity class methods:
/* Recalculate the network from this node onwards.
This requires forcing a forward recalc1 and a
backwards recalc2 from the end of the net. */
Def recalc(self)
{ recalc1(self,true,nil); /* force forward recalc1 */
recalc2(network); /* do backwards recalc2 */
}
/* Recalculate an activity's earlyStart. If the user has
set an earlyStart, use it instead of recalculating.
Send a message to the node's outputs to recalculate only
if a forced recalc is required, or if earlyStart changes.
formula: ES = max(ES(i) + time(i)) for all inputs i
arguments:
timeChanged: force a recalc1 if the time has changed
costRequired: force a calcCost(self) if true
*/
Def recalc1(self, timeChanged, costRequired |oldEarlyStart)
{ if (costRequired)
calcCost(self);
endif;
oldEarlyStart := earlyStart; /* temporary */
if (userEarlyStart) /* user over ride */
earlyStart := userEarlyStart;
else
earlyStart := 0; /* find largest ES */
do(inputs,
{using(input)
earlyStart := max(earlyStart,
getEarlyStart(input) + getTime(input));
});
endif;
/* Recalculate outputs if earlyStart changed OR if time
changed. Don't force it to beyond the next level. */
if timeChanged or (earlyStart <> oldEarlyStart)
do(outputs,
{using(output) recalc1(output, nil, costRequired);
});
endif;
}
Object-Oriented Programming in the Real World 9
/* Recalculate an activity's lateFinish then its slack and
determine if its critical.
If the user has set a lateFinish, use it instead of
recalculating. LateFinish recalc goes backwards from a
node to its inputs. Always propogate recalc2 since a
change to the time of a node will not change lateFinish,
but it can change slack and critical, which are only
known on the backwards pass.
formula: LF = min(LF(i) - time(i)) for all outputs i
*/
Def recalc2(self)
{
if userLateFinish
lateFinish := userLateFinish; /* user override */
else
lateFinish := MAXINT; /* find smallest LF */
do(outputs,
{using(output)
lateFinish := min(lateFinish,
getLateFinish(output) - getTime(output));
});
endif;
calcSlack(self);
calcCritical(self);
/* Continue sending the recalc2 message. */
do(inputs,
{using(input)
recalc2(input);
});
}
Windows objects
Ultimately we'd like our project manager to run in a window
with pulldown menus and have some sort of graphical
representation of the project. Since Actor is a Microsoft
Windows development environment, this is quite easy.
Microsoft Windows is somewhat object-oriented itself. When the
user clicks on the mouse or presses a key, MS-Windows will send
a message to the appropriate window. Because of the object-
oriented approach, programming Microsoft Windows is much easier
in Actor than in a procedural language like C. Actor has
predefined classes for windows, dialog boxes, scroll bars etc.
We will define a new window class, called ProjWindow, which
inherits the "default" behavior needed in Windows applications.
Note that ProjWindow and Project are unrelated in the class
tree. ProjWindow does not descend from Project since it is not
project-like. Rather, a ProjWindow simply contains a project
object as an instance variable.
Object-Oriented Programming in the Real World 10
By having two distinct classes we clearly separate the user
interface issues from the recalculating of the critical path
handled by the project and its activities. This makes it easy
for us to have different types of windows display projects in
different ways. Our ProjWindow can display a network diagram
of the activities while a GanttWindow will display a time line
graph known as a Gantt chart. In both cases, the windows will
send messages to the project to select particular activities,
edit them, recalculate the critical path and so on.
Conclusion
Object-oriented programming encourages the creation of abstract
data objects with a well-defined public protocol and a private
implementation. The result is a program that is easier to
develop and maintain. PC-Project is a good demonstration of
object-oriented principles put into action. And with it's
graphical user-interface, fast recalculation, file load and
save capabilities, it's a useful application in itself.
About the author
Zack Urlocker is Manager of Developer Relations at The
Whitewater Group, the developers of Actor. Urlocker has a
master's degree in Computer Science from the University of
Waterloo, Ontario, Canada where his research was in the area
programming environments. PC-Project and source code are
available on disk from the author for $5 shipping to the United
States; $10 elsewhere.
Object-Oriented Programming in the Real World 11