Object Oriented Software Development
H. Conrad Cunningham
12 April 2022
Browser Advisory: The HTML version of this textbook requires a browser that supports the display of MathML. A good choice as of April 2022 is a recent version of Firefox from Mozilla.
TODO: Add
For the purposes of this discussion, assume that software development proceeds through the four lifecycle phases shown in the following diagram.
.__________________.
| |
| Analysis |<--:
|__________________| |
| |
| ^
.________V_________. |
| |-->:
| Design | |
|__________________|<--.
| |
| ^
.________V_________. |
| |-->:
| Implementation | |
|__________________|<--:
| |
| |
.________V_________. ^
| | |
| Maintenance |-->:
|__________________|
TODO: Replace the above diagram by a better graphic.
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:
Break the programming task into manageable parts.
Define the relationships among the parts.
Incorporate any required or appropriate pre-existing components.
Keep the design independent of implementation language and hardware details. (We can, however, use knowledge of high-level architectures and language paradigms.)
In the implementation phase, we move from a design specification to a tested executable system. The implementation tasks are to:
Translate the design into working software on appropriate hardware
Use the details of the programming language carefully
In the maintenance phase, we move from a complete “working” system to a modified system. The maintenance tasks are to:
Repair any errors in analysis, design, or implementation that have been found
Adapt to the changes in requirements that have occurred
We observe the following.
In successful software systems, the maintenance phase is more important than implementation.
Often more than half of the development effort will occur during maintenance.
We conclude that we should:
Do initial analysis, design, and implementation very carefully
Design for change!
The type of software development projects familiar to most students can be described as programming in the small. Such projects have the following attributes:
Programs are developed by a single programmer or perhaps a small group of programmers.
All aspects of the project can be understood by a single individual.
The major problem is the development of the algorithms and data structures needed to solve the task at hand.
Programming in the large characterizes projects with the following attributes:
The software system is developed by a large team of programmers, often with considerable specialization.
No single individual can (likely) understand all aspects of the project.
The major problem is the coordination of the diverse aspects of the project–people and software systems.
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.
In contemporary practice, most software engineers approach the design of programs from an object-oriented perspective.
This approach is based on the following assumptions:
Describing large, complex systems as interacting objects make them easier to understand than otherwise.
The behaviors of real world objects tend to be stable over time.
The different kinds of real world objects tend to be stable. (That is, new kinds appear slowly; old kinds disappear slowly.)
Changes tend to be localized to a few objects.
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:
Uses the same basic entities (i.e., objects) throughout the lifecycle
Identifies the basic objects during analysis
Identifies lower-level objects during design, reusing existing object descriptions where appropriate
Implements the objects as software structures (e.g., Java classes).
Maintain the object behaviors.
See the “Object Model” section in Chapter 3 of the textbook Exploring Languages with Interpreters and Functional Programming [6] for discussion of the object-based concepts and terminology.
In this chapter, we do not make strong distinctions among the object-based, class-based, object-oriented, and prototype-based approaches. Analysis and design begins by identifying objects as relatively fuzzy entities. These may later evolve into standalone objects, instances of classes, classes, hierarchies of classes, delegation-based clusters of objects, etc., depending on the specifics of the application and of the technologies used.
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 do the following.
Read all existing documents that potentially describe the requirements for the new system.
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.
Examine system outputs carefully.
This includes the outputs (e.g., reports) generated in the current system and the descriptions of outputs desired from the new system.
Interview users.
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.
Examine documentation of the current system.
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.
Note: The accompanying slide set Using CRC Cards discusses the processes of object-oriented requirements (this subsection) and design (next subsection) using methods built around Class-Responsibility-Collaboration (CRC) cards [[1]; Bellin1997; [15]].
The goals of the design phase are to:
Identify the classes.
What kinds of objects do we have?
Identify the responsibilities (i.e., functionality) of each class.
What does each kind of object do? What information does it hold?
Identify the collaborations of each class (i.e., the relationships among the classes).
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:
to carry out some action—that is, an operation.
to hold some key information—that is, an attribute (part of the state).
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.
You are to build an automated telephone book system for a university. The telephone book should contain entries for each person in the university community—student, professor, and staff member. Users of the directory can look up entries. In addition, the administrator of the telephone book can, after supplying a password, insert new entries, delete existing entries, modify existing entries, print the telephone book to a printer, and print a listing of all students or of all faculty. The entries in a listing are to be arranged in alphabetical order by family name.
To develop an object-oriented design model for this application, as designers we can carry out the following steps:
Identify the candidate classes.
Begin by listing the nouns, noun phrases, and pronoun antecedents from the requirements specification, changing all the plurals to singular.
you (the designer), automated telephone book system, university, telephone book, entry, person, university community, student, professor, staff member, employee, user, directory, administrator, password, printer, listing, faculty, alphabetical order, family name
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:
the “subjects” (i.e., actors) of passive voice sentences. To find these, a designer may need to restate the passive sentence as an equivalent active sentence.
parts of items named. For example, the name, address, and telephone number fields of the entries are not explicitly mentioned, but they are implicit in what is commonly meant by telephone book entries.
known interfaces to the environment of the system. For example, user interface entities such as a menu are implied by the need for some way for a user to interact with the application.
data structures for storing the collection of personal entries that make up the telephone book.
Several, but not necessarily all, of these will become classes in our design.
Identify the candidate responsibilities.
Begin by listing the verbs from the specification. Listing the objects of transitive verbs may also be helpful.
build, contain (entry), look (up entry), supply (password), insert (new entry), delete (existing entry), modify (existing entry, print (telephone book), print (all students), print (all employees), set (telephone number field), get (telephone number field), compare (entries), to be arranged
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.
Eliminate classes and responsibilities that are outside of the system scope.
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:
critical classes (i.e., the “winners”), which we will definitely continue to consider. These are items that directly relate to the main entities of the application.
In the telephone book example, these might include telephone book, entry, student, faculty, staff member, and so forth.
irrelevant candidates (i.e., the “losers”), which we will definitely eliminate at this point. These are items that are clearly outside the system scope.
In the telephone book example, these might include university which we do not need to explicitly model in this application) and printer (which is handled by other software/hardware outside of this application).
undecided candidates (i.e., the “maybes”), which we will review further for categorization. These are items that we may not be able to categorize without first clarifying the system boundaries and definition
In the telephone book example, these might include the automated 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.
Combine synonym classes and synonym operations into single abstractions.
For example, automated 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.
Distinguish attributes from classes.
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:
it does not do anything—i.e., it has no operations.
it cannot change state.
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.
Be wary of adjectives.
Adjectives modify nouns. In our technique, nouns give rise to classes or objects.
An adjective might be superfluous as far as our object analysis is concerned. For example, the adjective “automated” in “automated telephone book” adds no extra information to “telephone book” because the adjective was implicit in the context of the specification anyway.
An adjective may signal the need for an attribute of the
associated class. For example, the phrase “red truck” might imply the
need for a color attribute of a Truck
class—unless, of
course, only one color of truck is needed in the application.
An adjective may signal the need for a subclass. For
example, the phrase “fire truck” would likely call for a subclass
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”.
An adjective might just indicate the need for an
instance of a class. For example, in a context where there is
exactly one “red truck” and where “red” trucks need no different
behaviors than any other trucks, it is sufficient for the “red truck” to
simply be an instance of class 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”.
Consider architectural design issues.
Identify hot spots. Structure classes and collaborations accordingly.
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 what might change if we needed 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
Person
entries would need to be flexible.
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?
Use appropriate design patterns to guide structuring of the system.
TODO: Fix the two patterns hyperlinks below appropriately. Perhaps augment or replace with citations.
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 catalog such as the “Gang of Four” patterns [8], the Siemens patterns [5], or others.
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 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.
Take advantage of existing software frameworks.
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.
Associate the operations with the appropriate classes.
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 an 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.
Names of classes should be singular nouns. In Java and Scala, these names should normally begin with an uppercase letter.
Names of responsibilities (operations) should be verbs or short sequences of words containing one verb. In Java and Scala, these names should normally begin with a lowercase letter.
Names of booleans should indicate meaning of the true value.
Names should be easily recognized and understood by domain experts.
Names should be short.
Names should be pronounceable (read them out loud).
Names should be consistent within the project (perhaps the entire development organization).
Names should be unambiguous, not having multiple interpretations. Use abbreviations with care.
Names should use capitalization and underscores, but avoid digits.
Two classes in a design may be related in one of several ways. The three relationships that are common are:
use or awareness (uses)
aggregation (has-a)—sometimes called containment or composition
inheritance (is-a)—sometimes called generalization, extension, or specialization
Class A uses B when:
an operation of A receives/returns an object of B
an operation of A must examine or create an object of B
an object of A “contains” objects of B (as instance variables)
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
struct
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:
descriptions of each class
descriptions of each operation (i.e., method)
diagrams giving relationships among the classes
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:
Implement and test each class or cluster of closely related classes separately.
Once each cluster is working correctly, integrate it into the program.
Perhaps defer some operations until later—build a prototype rapidly to explore requirements.
If done carefully, incrementally evolve the prototype into a fully functional system.
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.
TODO: Add
TODO: Add
In Fall 2016 and Spring 2017, I adapted this chapter from my previous notes on this topic. These notes—and my approach to object-oriented design and programming in general—were influenced by my study of works by Horstmann [9,10], Budd [3,4], Meyer [12], Thomas [14], Wirfs-Brock [16,17], Beck [1], Bellin [2], the “Gang of Four” [8], Buschmann [5], Fishwick [7], Johnson [11], Schmid [13], and many other sources (lost in the mists of time).
I wrote the first version of these notes in Spring 1996 for my CSci 490 Special Topics course on object-oriented design. This C++-based course used a textbook by Horstmann [9]. I expanded the notes for the first Java-based version of CSci 211 (then titled File Systems) during Fall 1996. This course used a book by Horstmann and Cornell [10, First edition].
I revised the notes incrementally over the next decade for use in my Java-based courses on object orientation, software architecture, and computer simulation.
I partially revised the notes for use in my Scala-based classes beginning in Fall 2008 and Lua-based classes beginning in Fall 2013.
In Fall 2016, I separated out the discussion of the Object Model and merged it into the discussion of programming paradigms from my Functional Programming notes. In the 2016-18 period, this evolved into Chapters 2 and 3 of the 2018 version of my textbook Exploring Languages with Interpreters and Functional Programming.
In 2017, 2018, and 2019, I reformatted these notes to use Pandoc Markdown, improved the presentation in places, and linked it into the other documents
I retired from the full-time faculty in May 2019. As one of my post-retirement projects, I am continuing work on possible textbooks based on the course materials I had developed during my three decades as a faculty member. In January 2022, I began refining the existing content, integrating separately developed materials together, reformatting the documents, constructing a unified bibliography (e.g., using citeproc), and improving my build workflow and use of Pandoc.
I maintain this chapter as text in Pandoc’s dialect of Markdown using embedded LaTeX markup for the mathematical formulas and then translate the document to HTML, PDF, and other forms as needed.
TODO: Add