.__________________. | | | Analysis |<--: |__________________| | | | | ^ .________V_________. | | |-->: | Design | | |__________________|<--. | | | ^ .________V_________. | | |-->: | Implementation | | |__________________|<--: | | | | .________V_________. ^ | | | | Maintenance |-->: |__________________|
In the analysis phase we move from a vague description of the problem to be solved to a precise and unambiguous requirements specification.
The requirements specification might be a precise, but informal description written in careful natural language text and diagrams. Alternatively, the specification might be a formal description written in a mathematically precise language. Or the requirements specification might be something in between these.
The requirements specification should be:
In the design phase, we move from a requirements specification to a design specification. The design specification gives the system structure. The design tasks are to:
In the implementation phase, we move from a design specification to a tested executable system. The implementation tasks are to:
In the maintenance phase, we move from a complete "working" system to a modified system. The maintenance tasks are to:
Observations:
Conclusions:
The type of software development projects familiar to most students can be described as programming in the small. Such projects have the following attributes:
Programming in the large characterizes projects with the following attributes:
The techniques of object-oriented design and programming are useful in both programming in the small and programming in the large situations. However, some of the techniques are best appreciated when the difficulties of programming in the large are understood.
We will approach the design of programs from an object-oriented perspective.
Assumptions:
Assumption 1 simplifies analysis, design, and implementation--makes them more reliable.
Assumptions 2 and 3 support reuse of code, prototyping, and incremental development.
Assumption 4 supports design for change.
The object-oriented approach:
Our object model includes four components:
Objects are characterized by:
An object is a separately identifiable entity that has a set of operations and a state that records the effects of the operations. That is, an object is essentially the same as an abstract data structure as we have discussed previously.
The various components of the state are sometimes called the attributes of the object.
As an example, consider an object for a student desk in a simulation of a classroom. The relevant state might be attributes like location, orientation, person using, items in basket, items on top, etc. The relevant operations might be state-changing operations (mutators) such as "move the desk", "seat student", or "remove from basket" or might be state-observing operations (accessors) such as "is occupied" or "report items on desktop".
A language is object-based if it supports objects as a language feature.
Object-based languages include Ada, Modula, Clu, C++, Java, and Smalltalk. Pascal (without module extensions), Algol, Fortran. and C are not inherently object-based.
A class is a template for creating objects.
A class description includes definitions of
As an example, again consider a simulation of a classroom. There
might be a class of StudentDesk
objects from which
specific instances can be created as needed.
An object-based language is class-based if the concept of class occurs as a language feature and every object has a class.
Class-based languages include Clu, C++, Java, Smalltalk, and Ada 95. Ada 83 and Modula are not class-based.
A class C inherits from class P if C's objects form a subset of P's objects.
The importance of inheritance is that it encourages sharing and reuse of both design information and program code. The shared state and operations can be described and implemented in base classes and shared among the subclasses.
As an example, again consider the student desks in a simulation of
a classroom. The StudentDesk
class might be derived
(i.e., inherit) from a class Desk
, which in turn might be
derived from a class Furniture
. In diagrams, it is the
convention to draw arrows from the subclass to the superclass.
Furniture <-- Desk <-- StudentDesk
The simulation might also include a ComputerDesk
class
that also derives from Desk
.
Furniture <-- Desk <-- ComputerDesk
In Java, we can express the above inheritance relationships using the
extends
keyword as follows:
class Furniture // extends Object by default { ... } class Desk extends Furniture { ... } class StudentDesk extends Desk { ... } class ComputerDesk extends Desk { ... }
Both StudentDesk
and ComputerDesk
objects will need operations to simulate a move
of the
entity in physical space. The move
operation can thus be
implemented in the Desk
class and shared by objects of
both classes. Invocation of operations to move
either a
StudentDesk
or a ComputerDesk
will be bound
to the general move
in the Desk
class.
The StudentDesk
class might inherit from a
Chair
class as well as the Desk
class.
Furniture <-- Chair <-- StudentDesk
Some languages support multiple inheritance as shown above for
StudentDesk
(e.g., C++, Eiffel). Other languages only support a
single inheritance hierarchy.
Because multiple inheritance is both difficult to use correctly and to
implement in a compiler, the designers of Java did not include a
multiple inheritance of classes as feature. Java has a single
inheritance hierarchy with a top-level class named Object
from which all other classes derive (directly or indirectly).
class StudentDesk extends Desk, Chair // NOT VALID in Java { ... }
To see some of the problems in implementing multiple inheritance,
consider the above example. Class StudentDesk
inherits
from class Furniture
through two different paths. Do the
data fields of the class Furniture
occur once or twice?
What happens if the intermediate classes Desk
and
Chair
have conflicting definitions for a data field or
operation with the same name?
The difficulties with multiple inheritance are greatly decreased if we restrict ourselves to inheritance of class interfaces (i.e., the signatures of a set of operations) rather than a supporting the inheritance of the class implementations (i.e., the instance data fields and operation implementations). Since interface inheritance can be very useful in design and programming, the Java designers introduced a separate mechanism for that type of inheritance.
The Java interface
construct can be used to define an
interface for classes separately from the classes themselves. A Java
interface
may inherit from (i.e., extend
)
zero or more other interface
definitions.
interface Location3D { ... } interface HumanHolder { ... } interface Seat extends Location3D, HumanHolder { ... }
A Java class
may inherit from (i.e.,
implement
) zero or more interfaces as well as inherit
from (i.e., extend
) exactly one other class
.
interface BookHolder { ... } interface BookBasket extends Location3D, BookHolder { ... } class StudentDesk extends Desk implements Seat, BookBasket { ... }
This definition requires the StudentDesk
class to provide
actual implementations for all the operations from the
Location3D
, HumanHolder
,
BookHolder
, Seat
, and
BookBasket
interfaces. The Location3D
operations will, of course, need to be implemented in such a way
that they make sense as part of both the HumanHolder
and
BookHolder
abstractions.
The concept of polymorphism (literally "many forms") means the ability to hide different implementations behind a common interface.
Polymorphism appears in several forms.
void move () { ... } void move (Location l) { ... } void move (Desk d) { ... }
The choice among the alternatives can be done statically at the time the program is compiled.
Java supports overloading of method calls, but does not support user-defined overloading of operator symbols. C++ supports both.
Java does not currently support parameterized class definitions. The
template
facility in C++ and the generic
facility in Ada are examples of parametric polymorphism. There are
experimental extensions to Java that do support parametric
polymorphism.
This form of polymorphism is carried out at run time. Given an object (i.e., class instance) to which an operation is applied, the system will first search for an implementation of the operation associated with the object's class. If no implementation is found in that class, the system will check the superclass, and so forth up the hierarchy until an appropriate implementation is found. Implementations of the operation may appear at several levels of the hierarchy.
The combination of dynamic binding with a well-chosen inheritance hierarchy allows the possibility of an instance of one subclass being substituted for an instance of a different subclass during execution. Of course, this can only be done when none of the extended operations of the subclass are being used.
As an example, again consider the simulation of a classroom. As in
our discussion of inheritance, suppose that the
StudentDesk
and ComputerDesk
classes are
derived from the Desk
class and that a general
move
operation is implemented as a part of the
Desk
class. This could be expressed in Java as follows:
class Desk extends Furniture { ... public void move(...) ... } class StudentDesk extends Desk { ... // no move(...) operation here ... } class ComputerDesk extends Desk { ... // no move(...) operation here ... }
As we noted before, invocation of operations to move
either a StudentDesk
or a ComputerDesk
instance will be bound to the general move
in the
>Desk
class.
Extending the example, suppose that we need a special version of the
move
operation for ComputerDesk
objects.
For instance, we need to make sure that the computer is shut down and
the power is disconnected before the entity is moved.
To do this, we can define this special version of the
move
operation and associate it with the
ComputerDesk
class. Now a call to move
a
ComputerDesk
object will be bound to the special
move
operation, but a call to move
a
StudentDesk
object will still be bound to the general
move
operation in the Desk
class.
The definition of move
in the ComputerDesk
class is said to override the definition in the
Desk
class.
In Java, this can be expressed as follows:
class Desk extends Furniture { ... public void move(...) ... } class StudentDesk extends Desk { ... // no move(...) operation here ... } class ComputerDesk extends Desk { ... public void move(...) ... }
A class-based language is object-oriented if class hierarchies can be incrementally defined by an inheritance mechanism and the language supports polymorphism by inheritance along these class hierarchies.
Object-oriented languages include C++, Java, Smalltalk, and Ada 95. The language Clu is class-based, but does not include an inheritance facility.
Other object-oriented languages include Objective C, Object Pascal, Eiffel, and Oberon 2.
The task of the analysis phase is to define the problem and write a clear, consistent, complete, and precise description of the system to be constructed. The analysts must elicit the requirements for the system from the clients and communicate the requirements to the system designers, who have the task of designing the new system.
The analysts must determine the scope of the system: What must the system accomplish? And, perhaps just as importantly, what behaviors are clearly outside the system in its environment?
In approaching the requirements analysis for a new system, the analysts should:
This includes all documents that directly address the requirements as well as documents that may indirectly address them. The indirect sources include such items as memos, meeting minutes, problem reports from the (manual or automated) system being replaced, and employee, supplier, or customer complaints and suggestions. The indirect sources might also include information about how recent or expected future changes in the business, regulatory, social, or technological environment may impact the requirements for the system.
This includes the outputs (e.g., reports) generated in the current system and the descriptions of outputs desired from the new system.
The analysts should talk to a wide range of experienced users of the current system and its outputs and take careful notes. The experienced users are the ones who know how the system is "really" being used in practice -- what its strengths and weaknesses are, what works well and what must be worked around, who uses what aspect of the system or its outputs, etc.
The users of the system may have many different relationships to the system and, hence, will likely have different perspectives on how it works. The users might include a wide range of employees of the organization (clerical personnel, computer operators, managers, technical professionals, factory workers, salespersons, etc.) and might also include customers or suppliers to the client organization.
The analysts should review the official documentation on the current system (whether automated or manual) as well as any unofficial or personal notes users or maintainers of the system have.
Good analysts are good detectives! Among the mass of detail, the analysts must find the clues that allow them to solve the mystery of what system the client needs. It is important that analysts keep a complete record of the information they have gathered and their reasoning on any "conclusions" they reach about that information.
The result of the analysts' work is a document called the requirements specification. Typically this will be a natural language (e.g., English) document. The writers of this document should use great care, using the language in a clear, consistent, and precise manner. The writers are establishing the vocabulary for communication with the clients and among the designers, implementers, and testers of the system.
The goals of the design phase are to:
What kinds of objects do we have?
What does each kind of object do? What information does it hold?
Does one kind of object use another in some way? Is it a special case of another kind?
The actual design process is iterative. Elaboration of a class may lead to identification of additional classes or changes to those already identified.
Classes should be crisply defined. It should be as easy as possible to determine what class an object belongs in. Coming up with a good categorization is often difficult in complex situations.
The responsibilities of a class are of two types:
Responsibilities should be defined precisely. Ambiguity and imprecision will likely cause problems later in the design and implementation.
The collaboration relationships among classes should not be excessively complex. A collaboration between two classes means that one class depends upon the other in some way. A change in one class may necessitate a change in the other.
The information gathered in the design phase is the basis for the implementation. A good design makes the implementation easier and faster and the resulting product more reliable.
The basic methods for identifying classes and responsibilities are as follows.
To develop an object-oriented design model for this application, as designers we can carry out the following steps:
Begin by listing the nouns, noun phrases, and pronoun antecedents from the requirements specification, changing all the plurals to singular.
Other classes may be be implicit in the specification or may emerge as the design of individual classes proceed and the designer's knowledge of the application domain increases. For example, the design may need classes corresponding to:
Several, but not necessarily all, of these will become classes in our design.
Begin by listing the verbs from the specification. Listing the objects of transitive verbs may also be helpful.
Transitive verbs become operations that respond to inputs to the object. All of the above verbs except "to be arranged" are transitive and, hence, will likely give rise to operations.
Intransitive verbs typically specify attributes of classes. For example, "to be arranged in alphabetical order" in the above example denotes an ordering property of the entries in a listing. It is not an operation. Of course, in some cases, this kind of attribute might require a "sort" operation.
At this point, we may want to narrow the list of candidate classes by quickly dividing them into three categories based on their relevance to the system scope:
In the telephone book example, these might include telephone book, entry, student, faculty, staff member, and so forth.
In the telephone book example, these might include university (for which we do not need to explicitly model in this application) and printer (which is handled by other software/hardware outside of this application).
In the telephone book example, these might include the computerized telephone book system, an item that definitely will be built (it is the whole system) but for which we may not need an explicit class.
For example, computerized telephone book, telephone
book, and directory can be combined into a single
PhoneBook
class.
Similarly, entry and person can be combined into a
single Person
class.
In addition, all the print
operations probably could be
combined into a single print
operation with the differing
functionalities specified by the parameter.
Some candidate classes may turn out to represent information held by other classes instead of being classes themselves.
A candidate class may be an attribute (i.e., a responsibility) of another class rather than itself a class if:
For example, we might choose to make the entity name an
immutable string and make it an attribute of a class
Person
rather than have it a separate class.
Adjectives modify nouns. In our technique, nouns give rise to classes or objects.
Truck
class--unless, of
course, only one color of truck is needed in the application.
FireTruck
of class Truck
if other kinds of
trucks are needed in the application. A "fire truck" has several
behaviors and attributes that differ from the generic concept of a
"truck".
Truck
.
Prepositional phrases may modify nouns in the requirements specification. Such phrases may lead to subclasses, objects, or instances as described above for adjectives.
Adverbs may modify adjectives in the requirements specification. They provide other information about the adjective that should be considered in the analysis. For example, a reference to a "brilliantly red truck" might signal the need for another attribute, subclass, or instance that is different from a plain "red truck" or from a "dull red truck".
A hot spot is a portion of the system that is likely to change from one system variant to another.
To readily support change, encapsulate the variable aspects within components and design the interfaces of and the relationships among system components so that change to the architecture is seldom necessary.
This technique enables convenient reuse of the relatively static, overall system architecture and common code. It makes change easier to implement at hot spots.
In the telephone book example, we might consider aspects of the application that likely will need to change over time to meet the needs of the identified client. The hot spots identified might be how the telephone book information is stored, how the printed listings are formatted, what the user interface looks like, what exact information is stored in entries, what kinds of computing platforms the application executes on, what types of printers are used, etc. The detailed design decisions relative to these issues should be encapsulated within single components and documented well.
Sometimes we need to approach the design of an application as the
design of a whole product line rather than the design of a
single product.
For example, we might consider is what might change if we needed
wanted to modify the application to handle the needs of other
organizations. In the telephone book example, what would need to
change if we wanted to make it useful to other universities? to
government agencies? to private nonprofit service agencies? to
businesses? to institutions in other countries (i.e.,
internationalization)? Clearly, the categorization of student, staff,
and professor would need to be flexible. The handling of the name and
address data and of other aspects of the
Similarly, we can consider what might need to change if we wanted to
expand the application from just maintaining telephone book
information to other types of membership applications. Can we
separate the application-specific behaviors from more general
behaviors? Can we make it so that it is easy to design new specific
behaviors and plug them in to the overall application structure?
A design pattern is a
design structure that has been successfully used in a similar
context--i.e., a reusable design.
Design patterns may be distillations of the development organization's
experience -- or may be well-known general patterns selected from a
book such as Design Patterns: Elements of Reusable
Object-Oriented Software by the Gang of Four (Gamma, Helm,
Johnson, and Vlissides) (Addison-Wesley, 1995).
The use of the design pattern may require the addition of new classes
to the design or the modification of core classes.
An example of a high-level pattern is the Pipes and Filters pattern.
In Unix for instance, a filter is a program that reads a stream of
bytes from its standard input and writes a transformed stream to its
standard output. These programs can be chained together with the
output of one filter becoming the input of the next filter in the
sequence via the pipe mechanism. Larger systems can thus be
constructed from simple components that otherwise operate
independently of one another.
An example of a lower level pattern is the Iterator. This pattern
defines general mechanisms for stepping through container data
structures element by element. For instance, a Java object that
implements the
A framework is a collection of classes--some abstract, some
concrete--that captures the architecture and basic operation of an
application system. Systems are created by extending the given
classes to add the specialized behaviors.
Frameworks are "upside down libraries" -- system control resides in
framework code that calls "down" to user-supplied code.
Examples of software frameworks include graphical user interface
toolkits like the Java AWT and some discrete event simulation
packages.
To fit an application into a framework may require addition or
modification of core classes.
Person
entries
would need to be flexible.
Enumeration
interface is returned by the
elements()
of a Vector
object; successive
calls of the nextElement()
method of the
Enumeration
object returns successive elements of the
Vector
object.
For example, in the telephone book example associate
lookup
, insert
, delete
, and
modify
entry operations with the PhoneBook
class, associate compare
, setPhoneNumber
and
getPhoneNumber
with the Person
class, etc.
Sometimes there might be a choice on where to associate a operation.
For example, the "insert person into telephone book" operation could
be a operation of Person
(that is, insert this
person into the argument telephone book) or a operation of
PhoneBook
(that is, insert the argument person in
this telephone book).
Which is better? Associating the operation with Person
would require the Person
class to have access to the
internal representation details of the PhoneBook
.
However, associating the operation with PhoneBook
would
not require the PhoneBook
to know about the internal
details of the Person
class--except to be able to
compare
two entries. Thus making insert
an
operation of PhoneBook
and compare
an
operation of Person
would best maintain the encapsulation
of data.
During design we should not be concerned with the minute details of the implementation. However, it is appropriate to consider whether there is a "reasonable" implementation. In fact, it is better to make sure there are two different possible implementations to ensure flexibility and adaptability.
It is good to have more than one person involved in a design. A second designer can review the first's work more objectively and ask difficult questions -- and vice versa.
The selection of names for classes and operations is an important task. Give it sufficient time and thought.
Two classes in a design may be related in one of several ways. The three relationships that are common are:
Class A uses B when:
If objects of A can carry out all operations without any awareness of B, then A does not use B.
From the telephone book example, PhoneBook
uses
Person
objects (that is, it inserts, deletes, modifies,
etc., the entries). Person
objects do not use
PhoneBook
objects.
If class A uses class B, then a change to class B (particularly to its public interface) may necessitate changes to class A. The following principle will make modification of a design and implementation easier.
Class A uses class B for aggregation if objects of A contain objects of B.
Note: Object A contains (has an) object B if A has an instance variable that somehow designates object B--a Java reference to B, the index of B, the key of B, etc.
Aggregation is a one-to-N relationship; one object might contain several others.
Note: Aggregation is a special case of the use relationship. If asked to identify the aggregation and use relationships, identify an aggregation relationship as such rather than as a use relationship.
For example, objects of the Person
class in the telephone
book example contain (has) name
,
address
, and phoneNumber
objects.
The Pascal record
and C structure
are
aggregations; they contain other data fields.
They are not, however, objects.
Class D inherits from class B if all objects of D are also objects of B.
As noted previously,
In the telephone book example, we design Professor
to
inherit from Employee
; similarly, we design
Staff
to inherit from Employee
.
We make Employee
itself inherit from Person
;
similarly, we design Student
to inherit from
Person
.
Inheritance can lead to powerful and extensible designs. However, use and aggregation are more common than inheritance.
The outputs of the object-oriented design phase include:
The purpose of the implementation phase is to code, test, and integrate the classes into the desired application system.
Object-oriented development involves an integration of testing with implementation:
Note: If we seek to evolve a prototype into a full program, we should never hesitate to reopen the analysis or design if new insight is discovered.
Some of the material here is based on the presentation in the following books:
The first version of these lecture notes were written for use in the CSCI 490 section on object-oriented design and programming using C++ in the 1996 spring semester. The notes were subsequently expanded and put on the Web for the first Java-based version of CSCI 211 (File Systems) during the 1996 fall semester. They were further modified for use in the CSCI 581 (Object-Oriented Design and Programming) classes in the 1997 fall and 1999 spring semesters,in the CSCI 405 (Computer Simulation) class in the 2000 spring semester, and the ENGR 691 (Software Architecture) class in the 2000 fall semester.
UP to ENGR 691 Lecture Notes root document?