Copyright (C) 2016, H. Conrad Cunningham
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 |-->:
|__________________|
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.
In contemporary practice, most software engineers 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:
See the the Fundamental Concepts notes section on the Object-Oriented Paradigm.
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:
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.
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.
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.
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.
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:
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.
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 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 nouns. In Java and Scala, these names should normally begin with an uppercase letter.
Names of 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 evocative in the context of the problem.
Names should be short.
Names should be pronounceable (read them out loud).
Names should be consistent within the project.
Names should not have 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:
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:
I wrote the first version of these lecture notes for use in the CSci 490 section on object-oriented design and programming using C++ in the 1996 spring semester. I subsequently expanded the note and put on the Web for the first Java-based version of CSci 211 (File Systems) during the 1996 fall semester. I further modified the notes 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 and 2002 spring semesters.
I updated these notes for use in the Scala-based Multiparadigm Programming in Scala class in Fall 2008 and Spring 2012 and Software Families class in Fall 2011.
I updated this document some for the Lua-based Fall 2013 offering of CSci 658 Software Language Engineering and the Lua component in the Fall 2014 offering of CSci 450 Organization of Programming Languages. For the Fall 2016 offering of CSci 450, I moved the discussion of the Object Model to the document discussing programming paradigms and refocussed this document on analysis and design.