Design - OOD - CL1 Flashcards
Abstraction
The Meaning of Abstraction
Abstraction is one of the fundamental ways that we as humans cope with complexity. Dahl, Dijkstra, and Hoare suggest that “abstraction arises from a recognition of similarities between certain objects, situations, or processes in the real world, and the decision to concentrate upon these similarities and to ignore for the
time being the differences”. Shaw defines an abstraction as “a simplified description, or specification, of a system that emphasizes some of the system’s details or properties while suppressing others. A good abstraction is one that emphasizes details that are significant to the reader or user and suppresses details that are, at least for the moment, immaterial or diversionary”. Berzins, Gray,
and Naumann recommend that “a concept qualifies as an abstraction only if it can be described, understood, and analyzed independently of the mechanism that will
eventually be used to realize it”. Combining these different viewpoints, we define an abstraction as follows:
An abstraction denotes the essential characteristics of an object that distinguish it from all other kinds of objects and thus provide crisply defined conceptual boundaries, relative to the perspective of the viewer.
An abstraction focuses on the outside view of an object and so serves to separate an object’s essential behavior from its implementation. Abelson and Sussman call
this behavior/implementation division an abstraction barrier achieved by applying the principle of least commitment, through which the interface of an object provides its essential behavior, and nothing more. We like to use an additional principle that we call the principle of least astonishment, through which an abstraction captures the entire behavior of some object, no more and no less, and offers no surprises or side effects that go beyond the scope of the abstraction.
Deciding on the right set of abstractions for a given domain is the central problem in object-oriented design. From the most to the least useful, these kinds of abstractions include the following:
■ Entity abstraction - An object that represents a useful model of a problem domain or solution domain entity
■ Action abstraction - An object that provides a generalized set of operations, all of which perform the same kind of function
■ Virtual machine abstraction - An object that groups operations that are all used by some superior level of control, or operations that all use some junior-level set
of operations
■ Coincidental abstraction - An object that packages a set of operations that have no relation to each other
A key abstraction is a class or object that forms part of the vocabulary of the problem domain. The primary value of identifying such abstractions is that they give boundaries to our problem; they highlight the things that are in the system and therefore relevant to our design, and they suppress the things that are outside the system and therefore superfluous.
Identifying Key Abstractions
The identification of key abstractions is highly domain-specific.
As we mentioned earlier, the identification of key abstractions involves two processes: discovery and invention. Through discovery, we come to recognize the
abstractions used by domain experts; if the domain expert talks about it, the abstraction is usually important. Through invention, we create new classes and objects that are not necessarily part of the problem domain but are useful artifacts in the design or implementation. For example, a customer using an automated teller speaks in terms of accounts, deposits, and withdrawals; these words are part of the vocabulary of the problem domain. A developer of such a system uses these same abstractions but must also introduce new ones, such as databases, screen managers, lists, queues, and so on. These key abstractions are artifacts of the particular design, not of the problem domain.
Naming Key Abstractions
Naming things properly—so that they reflect their semantics—is often treated
lightly by most developers yet is important in capturing the essence of the
abstractions we are describing. Software should be written as carefully as English
prose, with consideration given to the reader as well as to the computer [58].
Consider for a moment all the names we may need just to identify a single object:
We have the name of the object itself, the name of its class, and the name of the
module in which that class is declared. Multiply this by thousands of objects and
possibly hundreds of classes, and you have a very real problem.
We offer the following suggestions.
■ Objects should be named with proper noun phrases, such as the Sensor or just simply shape.
■ Classes should be named with common noun phrases, such as Sensor or Shape.
■ The names chosen should reflect the names used and recognized by the domain experts, whenever possible.
■ Modifier operations should be named with active verb phrases, such as draw or moveLeft.
■ Selector operations should imply a query or be named with verbs of the form “to be,” such as extentOf or isOpen.
■ The use of underscores and styles of capitalization are largely matters of
personal taste. No matter which cosmetic style you use, at least have your
programs be self-consistent.
Encapsulation
The Meaning of Encapsulation
Abstraction and encapsulation are complementary concepts: Abstraction focuses on the observable behavior of an object, whereas encapsulation focuses on the implementation that gives rise to this behavior. Encapsulation is most often achieved through information hiding (not just data hiding), which is the process of hiding all the secrets of an object that do not contribute to its essential characteristics; typically, the structure of an object is hidden, as well as the implementation of its methods. “No part of a complex system should depend on the internal details of any other part”. Whereas abstraction “helps people to think about what they are doing,” encapsulation “allows program changes to be reliably made with limited effort”.
Encapsulation provides explicit barriers among different abstractions and thus leads to a clear separation of concerns. For example, consider again the structure
of a plant. To understand how photosynthesis works at a high level of abstraction,
we can ignore details such as the responsibilities of plant roots or the chemistry of
cell walls. Similarly, in designing a database application, it is standard practice to
write programs so that they don’t care about the physical representation of data
but depend only on a schema that denotes the data’s logical view [52]. In both of
these cases, objects at one level of abstraction are shielded from implementation
details at lower levels of abstraction.
“For abstraction to work, implementations must be encapsulated” [53]. In prac-
tice, this means that each class must have two parts: an interface and an imple-
mentation. The interface of a class captures only its outside view, encompassing
our abstraction of the behavior common to all instances of the class. The imple-
mentation of a class comprises the representation of the abstraction as well as the
mechanisms that achieve the desired behavior. The interface of a class is the one
place where we assert all of the assumptions that a client may make about any
instances of the class; the implementation encapsulates details about which no
client may make assumptions.
To summarize, we define encapsulation as follows:
Encapsulation is the process of compartmentalizing the elements of an abstrac-
tion that constitute its structure and behavior; encapsulation serves to separate the
contractual interface of an abstraction and its implementation.
Britton and Parnas call these encapsulated elements the “secrets” of an abstrac-
tion [54].
Examples of Encapsulation
To illustrate the principle of encapsulation, let’s return to the problem of the
Hydroponics Gardening System. Another key abstraction in this problem domain
is that of a heater. A heater is at a fairly low level of abstraction, and thus we
might decide that there are only three meaningful operations that we can perform
on this object: turn it on, turn it off, and find out if it is running.
All a client needs to know about the class Heater is its available interface (i.e.,
the responsibilities that it may execute at the client’s request—see Figure 2–9).
Turning to the inside view of the Heater, we have an entirely different perspec-
tive. Suppose that our system engineers have decided to locate the computers that
control each greenhouse away from the building (perhaps to avoid the harsh envi-
ronment) and to connect each computer to its sensors and actuators via serial
lines. One reasonable implementation for the Heater class might be to use an
electromechanical relay that controls the power going to each physical heater,
with the relays in turn commanded by messages sent along these serial lines. For
example, to turn on a heater, we might transmit a special command string, fol-
lowed by a number identifying the specific heater, followed by another number
used to signal turning the heater on.
Suppose that for whatever reason our system engineers choose to use memory-
mapped I/O instead of serial communication lines. We would not need to change
the interface of the Heater, yet the implementation would be very different. The
client would not see any change at all as the client sees only the Heater inter-
face. This is the key point of encapsulation. In fact, the client should not care what
the implementation is, as long as it receives the service it needs from the Heater.
Let’s next consider the implementation of the class GrowingPlan. As we men-
tioned earlier, a growing plan is essentially a time/action mapping. Perhaps the
most reasonable representation for this abstraction would be a dictionary of time/
action pairs, using an open hash table. We need not store an action for every hour,
because things don’t change that quickly. Rather, we can store actions only for
when they change, and have the implementation extrapolate between times.
In this manner, our implementation encapsulates two secrets: the use of an open
hash table (which is distinctly a part of the vocabulary of the solution domain, not
the problem domain) and the use of extrapolation to reduce our storage require-
ments (otherwise we would have to store many more time/action pairs over the
duration of a growing season). No client of this abstraction need ever know about
these implementation decisions because they do not materially affect the out-
wardly observable behavior of the class.
Intelligent encapsulation localizes design decisions that are likely to change. As a
system evolves, its developers might discover that, in actual use, certain opera-
tions take longer than is acceptable or that some objects consume more space than
is available. In such situations, the representation of an object is often changed so
that more efficient algorithms can be applied or so that one can optimize for space
by calculating rather than storing certain data. This ability to change the represen-
tation of an abstraction without disturbing any of its clients is the essential benefit
of encapsulation.
Hiding is a relative concept: What is hidden at one level of abstraction may repre-
sent the outside view at another level of abstraction. The underlying representa-
tion of an object can be revealed, but in most cases only if the creator of the
abstraction explicitly exposes the implementation, and then only if the client is
willing to accept the resulting additional complexity. Thus, encapsulation cannot
stop a developer from doing stupid things; as Stroustrup points out, “Hiding is for
the prevention of accidents, not the prevention of fraud” [56]. Of course, no pro-
gramming language prevents a human from literally seeing the implementation of
a class, although an operating system might deny access to a particular file that
contains the implementation of a class.
Inheritance vs. Aggregation
The Meaning of Hierarchy
Abstraction is a good thing, but in all except the most trivial applications, we may find many more different abstractions than we can comprehend at one time.
Encapsulation helps manage this complexity by hiding the inside view of our abstractions. Modularity helps also, by giving us a way to cluster logically related abstractions. Still, this is not enough. A set of abstractions often forms a hierarchy, and by identifying these hierarchies in our design, we greatly simplify our understanding of the problem.
We define hierarchy as follows:
Hierarchy is a ranking or ordering of abstractions.
The two most important hierarchies in a complex system are its class structure
(the “is a” hierarchy) and its object structure (the “part of” hierarchy).
Examples of Hierarchy: Single Inheritance
Inheritance is the most important “is a” hierarchy, and as we noted earlier, it is an
essential element of object-oriented systems. Basically, inheritance defines a rela-
tionship among classes, wherein one class shares the structure or behavior defined
in one or more classes (denoting single inheritance and multiple inheritance,
respectively). Inheritance thus represents a hierarchy of abstractions, in which a
subclass inherits from one or more superclasses. Typically, a subclass augments
or redefines the existing structure and behavior of its superclasses.
Semantically, inheritance denotes an “is a” relationship. For example, a bear “is
a” kind of mammal, a house “is a” kind of tangible asset, and a quick sort “is a”
particular kind of sorting algorithm. Inheritance thus implies a generalization/
specialization hierarchy, wherein a subclass specializes the more general structure
or behavior of its superclasses. Indeed, this is the litmus test for inheritance: If B
is not a kind of A, then B should not inherit from A.
Consider the different kinds of growing plans we might use in the Hydroponics
Gardening System. An earlier section described our abstraction of a very general-
ized growing plan. Different kinds of crops, however, demand specialized grow-
ing plans. For example, the growing plan for all fruits is generally the same but
is quite different from the plan for all vegetables, or for all floral crops. Because
of this clustering of abstractions, it is reasonable to define a standard fruit-
growing plan that encapsulates the behavior common to all fruits, such as the
knowledge of when to pollinate or when to harvest the fruit. We can assert that
FruitGrowingPlan “is a” kind of GrowingPlan.
In this case, FruitGrowingPlan is more specialized, and GrowingPlan
is more general. The same could be said for GrainGrowingPlan or
VegetableGrowingPlan, that is, GrainGrowingPlan “is a” kind of
GrowingPlan, and VegetableGrowingPlan “is a” kind of GrowingPlan.
Here, GrowingPlan is the more general superclass, and the others are special-
ized subclasses.
As we evolve our inheritance hierarchy, the structure and behavior that are com-
mon for different classes will tend to migrate to common superclasses. This is
why we often speak of inheritance as being a generalization/specialization hierar-
chy. Superclasses represent generalized abstractions, and subclasses represent
specializations in which fields and methods from the superclass are added, modi-
fied, or even hidden. In this manner, inheritance lets us state our abstractions with
an economy of expression. Indeed, neglecting the “is a” hierarchies that exist can
lead to bloated, inelegant designs. “Without inheritance, every class would be a
free-standing unit, each developed from the ground up. Different classes would
bear no relationship with one another, since the developer of each provides meth-
ods in whatever manner he chooses. Any consistency across classes is the result
of discipline on the part of the programmers. Inheritance makes it possible to
define new software in the same way we introduce any concept to a newcomer, by
comparing it with something that is already familiar” [64].
There is a healthy tension among the principles of abstraction, encapsulation, and
hierarchy. “Data abstraction attempts to provide an opaque barrier behind which
methods and state are hidden; inheritance requires opening this interface to some
extent and may allow state as well as methods to be accessed without abstraction”
[65]. For a given class, there are usually two kinds of clients: objects that invoke
operations on instances of the class and subclasses that inherit from the class.
Liskov therefore notes that, with inheritance, encapsulation can be violated in one
of three ways: “The subclass might access an instance variable of its superclass,
call a private operation of its superclass, or refer directly to superclasses of its
superclass” [66]. Different programming languages trade off support for encapsu-
lation and inheritance in different ways. C++ and Java offer great flexibility.
Specifically, the interface of a class may have three parts: private parts, which
declare members that are accessible only to the class itself; protected parts, which
declare members that are accessible only to the class and its subclasses; and pub-
lic parts, which are accessible to all clients.
Examples of Hierarchy: Multiple Inheritance
The previous example illustrated the use of single inheritance: the subclass
FruitGrowingPlan had exactly one superclass, the class GrowingPlan.
For certain abstractions, it is useful to provide inheritance from multiple superclasses.
For example, suppose that we choose to define a class representing a kind of
plant. Our analysis of the problem domain might suggest that flowering plants
and fruits and vegetables have specialized properties that are relevant to our appli-
cation. For example, given a flowering plant, its expected time to flower and time
to seed might be important to us. Similarly, the time to harvest might be an impor-
tant part of our abstraction of all fruits and vegetables. One way we could capture
our design decisions would be to make two new classes, a Flower class and a
FruitVegetable class, both subclasses of the class Plant. However, what if
we need to model a plant that both flowered and produced fruit? For example,
florists commonly use blossoms from apple, cherry, and plum trees. For this
abstraction, we would need to invent a third class, FlowerFruitVegetable,
that duplicated information from the Flower and FruitVegetable classes.
A better way to express our abstractions and thereby avoid this redundancy is to
use multiple inheritance. First, we invent classes that independently capture the
properties unique to flowering plants and to fruits and vegetables. These two classes
have no superclass; they stand alone. These are called mixin classes because they
are meant to be mixed together with other classes to produce new subclasses.
For example, we can define a Rose class (see Figure 2–10) that inherits from
both Plant and FlowerMixin. Instances of the subclass Rose thus include
the structure and behavior from the class Plant together with the structure and
behavior from the class FlowerMixin.
Similarly, a Carrot class could be as shown in Figure 2–11. In both cases, we
form the subclass by inheriting from two superclasses.
Now, suppose we want to declare a class for a plant such as the cherry tree that
has both flowers and fruit. This would be conceptualized as shown in Figure 2–12.
Multiple inheritance is conceptually straightforward, but it does introduce some
practical complexities for programming languages. Languages must address two
issues: clashes among names from different superclasses and repeated inherit-
ance. Clashes will occur when two or more superclasses provide a field or opera-
tion with the same name or signature as a peer superclass.
Repeated inheritance occurs when two or more peer superclasses share a common
superclass. In such a situation, the inheritance lattice will be diamond-shaped, so
the question arises, does the leaf class (i.e., subclass) have one copy or multiple
copies of the structure of the shared superclass? (See Figure 2–13.) Some lan-
guages prohibit repeated inheritance, some unilaterally choose one approach, and
others, such as C++, permit the programmer to decide. In C++, virtual base
classes are used to denote a sharing of repeated structures, whereas nonvirtual
base classes result in duplicate copies appearing in the subclass (with explicit
qualification required to distinguish among the copies).
Multiple inheritance is often overused. For example, cotton candy is a kind of
candy, but it is distinctly not a kind of cotton. Again, the litmus test for inherit-
ance applies: If B is not a kind of A, then B should not inherit from A. Ill-formed
multiple inheritance lattices should be reduced to a single superclass plus aggre-
gation of the other classes by the subclass, where possible.
Examples of Hierarchy: Aggregation
Whereas these “is a” hierarchies denote generalization/specialization relation-
ships, “part of” hierarchies describe aggregation relationships. For example, con-
sider the abstraction of a garden. We can contend that a garden consists of a
collection of plants together with a growing plan. In other words, plants are “part
of” the garden, and the growing plan is “part of” the garden. This “part of” rela-
tionship is known as aggregation.
Aggregation is not a concept unique to object-oriented development or object-
oriented programming languages. Indeed, any language that supports record-like
structures supports aggregation. However, the combination of inheritance with
aggregation is powerful: Aggregation permits the physical grouping of logically
related structures, and inheritance allows these common groups to be easily
reused among different abstractions.
When dealing with hierarchies such as these, we often speak of levels of abstrac-
tion, a concept first described by Dijkstra [67]. In terms of its “is a” hierarchy, a
high-level abstraction is generalized, and a low-level abstraction is specialized.
Therefore, we say that a Flower class is at a higher level of abstraction than a
Plant class. In terms of its “part of” hierarchy, a class is at a higher level of
abstraction than any of the classes that make up its implementation. Thus, the class
Garden is at a higher level of abstraction than the type Plant, on which it builds.
Aggregation raises the issue of ownership. Our abstraction of a garden permits
different plants to be raised in a garden over time, but replacing a plant does not
change the identity of the garden as a whole, nor does removing a garden neces-
sarily destroy all of its plants (they are likely just transplanted). In other words,
the lifetime of a garden and its plants are independent. In contrast, we have
decided that a GrowingPlan object is intrinsically associated with a Garden
object and does not exist independently. Therefore, when we create an instance of
Garden, we also create an instance of GrowingPlan; when we destroy the
Garden object, we in turn destroy the GrowingPlan instance.
Modularity
The Meaning of Modularity
“The act of partitioning a program into individual components can reduce its
complexity to some degree. . . . Although partitioning a program is helpful for this
reason, a more powerful justification for partitioning a program is that it creates a
number of well-defined, documented boundaries within the program. These
boundaries, or interfaces, are invaluable in the comprehension of the program”
[57]. In some languages, such as Smalltalk, there is no concept of a module, so
the class forms the only physical unit of decomposition. Java has packages that
contain classes. In many other languages, including Object Pascal, C++, and Ada,
the module is a separate language construct and therefore warrants a separate set
of design decisions. In these languages, classes and objects form the logical structure
of a system; we place these abstractions in modules to produce the system’s phys-
ical architecture. Especially for larger applications, in which we may have many
hundreds of classes, the use of modules is essential to help manage complexity.
“Modularization consists of dividing a program into modules which can be com-
piled separately, but which have connections with other modules. We will use the
definition of Parnas: ‘The connections between modules are the assumptions
which the modules make about each other’” [58]. Most languages that support the
module as a separate concept also distinguish between the interface of a module
and its implementation. Thus, it is fair to say that modularity and encapsulation
go hand in hand.
Deciding on the right set of modules for a given problem is almost as hard a prob-
lem as deciding on the right set of abstractions. Zelkowitz is absolutely right
when he states that “because the solution may not be known when the design
stage starts, decomposition into smaller modules may be quite difficult. For older
applications (such as compiler writing), this process may become standard, but
for new ones (such as defense systems or spacecraft control), it may be quite dif-
ficult” [59].
Modules serve as the physical containers in which we declare the classes and
objects of our logical design. This is no different than the situation faced by the
electrical engineer designing a computer motherboard. NAND, NOR, and NOT
gates might be used to construct the necessary logic, but these gates must be
physically packaged in standard integrated circuits. Lacking any such standard
software parts, the software engineer has considerably more degrees of freedom—
as if the electrical engineer had a silicon foundry at his or her disposal.
For tiny problems, the developer might decide to declare every class and object in
the same package. For anything but the most trivial software, a better solution is
to group logically related classes and objects in the same module and to expose
only those elements that other modules absolutely must see. This kind of modu-
larization is a good thing, but it can be taken to extremes. For example, consider
an application that runs on a distributed set of processors and uses a message-
passing mechanism to coordinate the activities of different programs. In a large
system, such as a command and control system, it is common to have several hun-
dred or even a few thousand kinds of messages. A naive strategy might be to
define each message class in its own module. As it turns out, this is a singularly
poor design decision. Not only does it create a documentation nightmare, but it
makes it terribly difficult for any users to find the classes they need. Furthermore,
when decisions change, hundreds of modules must be modified or recompiled.
This example shows how information hiding can backfire [60]. Arbitrary modu-
larization is sometimes worse than no modularization at all.
In traditional structured design, modularization is primarily concerned with the
meaningful grouping of subprograms, using the criteria of coupling and cohesion.
In object-oriented design, the problem is subtly different: The task is to decide
where to physically package the classes and objects, which are distinctly different
from subprograms.
Our experience indicates that there are several useful technical as well as non-
technical guidelines that can help us achieve an intelligent modularization of
classes and objects. As Britton and Parnas have observed, “The overall goal of the
decomposition into modules is the reduction of software cost by allowing mod-
ules to be designed and revised independently. . . . Each module’s structure should
be simple enough that it can be understood fully; it should be possible to change
the implementation of other modules without knowledge of the implementation
of other modules and without affecting the behavior of other modules; [and] the
ease of making a change in the design should bear a reasonable relationship to the
likelihood of the change being needed” [61]. There is a pragmatic edge to these
guidelines. In practice, the cost of recompiling the body of a module is relatively
small: Only that unit need be recompiled and the application relinked. However,
the cost of recompiling the interface of a module is relatively high. Especially
with strongly typed languages, one must recompile the module interface, its body,
all other modules that depend on this interface, the modules that depend on these
modules, and so on. Thus, for very large programs (assuming that our develop-
ment environment does not support incremental compilation), a change in a single
module interface might result in much longer compilation time. Obviously, a
development manager cannot often afford to allow a massive “big bang” recompi-
lation to happen too frequently. For this reason, a module’s interface should be as
narrow as possible, yet still satisfy the needs of the other modules that use it. Our
style is to hide as much as we can in the implementation of a module. Incremen-
tally shifting declarations from a module’s implementation to its interface is far
less painful and destabilizing than ripping out extraneous interface code.
The developer must therefore balance two competing technical concerns: the
desire to encapsulate abstractions and the need to make certain abstractions visi-
ble to other modules. “System details that are likely to change independently
should be the secrets of separate modules; the only assumptions that should
appear between modules are those that are considered unlikely to change. Every
data structure is private to one module; it may be directly accessed by one or more
programs within the module but not by programs outside the module. Any other
program that requires information stored in a module’s data structures must
obtain it by calling module programs” [62]. In other words, strive to build mod-
ules that are cohesive (by grouping logically related abstractions) and loosely
coupled (by minimizing the dependencies among modules). From this perspec-
tive, we may define modularity as follows:
Modularity is the property of a system that has been decomposed into a set of
cohesive and loosely coupled modules.
Thus, the principles of abstraction, encapsulation, and modularity are synergistic.
An object provides a crisp boundary around a single abstraction, and both encap-
sulation and modularity provide barriers around this abstraction.
Two additional technical issues can affect modularization decisions. First, since
modules usually serve as the elementary and indivisible units of software that can
be reused across applications, a developer might choose to package classes and
objects into modules in a way that makes their reuse convenient. Second, many
compilers generate object code in segments, one for each module. Therefore,
there may be practical limits on the size of individual modules. With regard to the
dynamics of subprogram calls, the placement of declarations within modules can
greatly affect the locality of reference and thus the paging behavior of a virtual
memory system. Poor locality happens when subprogram calls occur across seg-
ments and lead to cache misses and page thrashing that ultimately slow down the
whole system.
Several competing nontechnical needs may also affect modularization decisions.
Typically, work assignments in a development team are given on a module-by-
module basis, so the boundaries of modules may be established to minimize the
interfaces among different parts of the development organization. Senior design-
ers are usually given responsibility for module interfaces, and more junior devel-
opers complete their implementation. On a larger scale, the same situation applies
with subcontractor relationships. Abstractions may be packaged so as to quickly
stabilize the module interfaces as agreed upon among the various companies.
Changing such interfaces usually involves much wailing and gnashing of teeth—
not to mention a vast amount of paperwork—so this factor often leads to conser-
vatively designed interfaces. Speaking of paperwork, modules also usually serve
as the unit of documentation and configuration management. Having ten modules
where one would do sometimes means ten times the paperwork, and so, unfortu-
nately, sometimes the documentation requirements drive the module design deci-
sions (usually in the most negative way). Security may also be an issue. Most
code may be considered unclassified, but other code that might be classified
secret or higher is best placed in separate modules.
Juggling these different requirements is difficult, but don’t lose sight of the most
important point: Finding the right classes and objects and then organizing them
into separate modules are largely independent design decisions. The identification
of classes and objects is part of the logical design of the system, but the identifica-
tion of modules is part of the system’s physical design. One cannot make all the
logical design decisions before making all the physical ones, or vice versa; rather,
these design decisions happen iteratively.
Examples of Modularity
Let’s look at modularity in the Hydroponics Gardening System. Suppose we
decide to use a commercially available workstation where the user can control the
system’s operation. At this workstation, an operator could create new growing
plans, modify old ones, and follow the progress of currently active ones. Since one of our key abstractions here is that of a growing plan, we might therefore create a
module whose purpose is to collect all of the classes associated with individual
growing plans (e.g., FruitGrowingPlan, GrainGrowingPlan). The
implementations of these GrowingPlan classes would appear in the implemen-
tation of this module. We might also define a module whose purpose is to collect
all of the code associated with all user interface functions.
Our design will probably include many other modules. Ultimately, we must
define some main program from which we can invoke this application. In object-
oriented design, defining this main program is often the least important decision,
whereas in traditional structured design, the main program serves as the root, the
keystone that holds everything else together. We suggest that the object-oriented
view is more natural, for, as Meyer observes, “Practical software systems are
more appropriately described as offering a number of services. Defining these
systems by single functions is usually possible, but yields rather artificial
answers. . . . Real systems have no top” [63].
Polymorphism
Polymorphism is a condition that exists when the features of dynamic typing and
inheritance interact. Polymorphism represents a concept in type theory in which a
single name (such as a variable declaration) may denote objects of many different
classes that are related by some common superclass. Any object denoted by this
name is therefore able to respond to some common set of operations [74]. The
opposite of polymorphism is monomorphism, which is found in all languages that
are both strongly and statically typed.
Polymorphism is perhaps the most powerful feature of object-oriented program-
ming languages next to their support for abstraction, and it is what distinguishes
object-oriented programming from more traditional programming with abstract
data types. As we will see in the following chapters, polymorphism is also a cen-
tral concept in object-oriented design.
Polymorphism
For the class TelemetryData, the function transmit may transmit the iden-
tifier of the telemetry stream and its timestamp. But the same function for the
class ElectricalData may invoke the TelemetryData transmit func-
tion and also transmit its voltage and current values.
This behavior is due to polymorphism. In a generalization, such operations are
called polymorphic. Polymorphism is a concept in type theory wherein a name
may denote instances of many different classes as long as they are related by
some common superclass. Any object denoted by this name is thus able to
respond to some common set of operations in different ways. With polymor-
phism, an operation can be implemented differently by the classes in the hierar-
chy. In this manner, a subclass can extend the capabilities of its superclass or
override the parent’s operation, as ElectricalData does in Example 3–5.
The concept of polymorphism was first described by Strachey [29], who spoke
of ad hoc polymorphism, by which symbols such as + could be defined to mean
different things. We call this concept overloading. In C++, one may declare
functions having the same names, as long as their invocations can be distin-
guished by their signatures, consisting of the number and types of their arguments
(in C++, unlike Ada, the type of a function’s returned value is not considered in
overload resolution). By contrast, Java does not permit overloaded operators.
Strachey also spoke of parametric polymorphism, which today we simply call
polymorphism.
Without polymorphism, the developer ends up writing code consisting of large
case or switch statements. 6 Without it, we cannot create a hierarchy of classes for
the various kinds of telemetry data; rather, we have to define a single, monolithic
variant record encompassing the properties associated with all the kinds of data.
To distinguish one variant from another, we have to examine the tag associated
with the record.
To add another kind of telemetry data, we would have to modify the variant
record and add it to every case statement that operated on instances of this record.
This is particularly error-prone and, furthermore, adds instability to the design.
In the presence of inheritance, there is no need for a monolithic type since we
may separate different kinds of abstractions. As Kaplan and Johnson note, “Poly-
morphism is most useful when there are many classes with the same protocols”
[30]. With polymorphism, large case statements are unnecessary because each
object implicitly knows its own type.
Inheritance without polymorphism is possible, but it is certainly not very useful.
Polymorphism and late binding go hand in hand. In the presence of polymor-
phism, the binding of a method to a name is not determined until execution. In
C++, the developer may control whether a member function uses early or late
binding. Specifically, if the method is declared as virtual, then late binding is
employed, and the function is considered to be polymorphic. If this virtual decla-
ration is omitted, then the method uses early binding and thus can be resolved at
the time of compilation. Java simply performs late binding without the need for
an explicit declaration such as virtual. How an implementation selects a par-
ticular method for execution is described in the sidebar, Invoking a Method.
Invoking a Method
In traditional programming languages, invoking a subprogram is a completely
static activity. In Pascal, for example, for a statement that calls the subprogram
P, a compiler will typically generate code that creates a new stack frame,
places the proper arguments on the stack, and then changes the flow of
control to begin executing the code associated with P. However, in languages
that support some form of polymorphism, such as Smalltalk and C++, invok-
ing an operation may require a dynamic activity because the class of the
object being operated on may not be known until runtime. Matters are even
more interesting when we add inheritance to the situation. The semantics of
invoking an operation in the presence of inheritance without polymorphism
is largely the same as for a simple static subprogram call, but in the pres-
ence of polymorphism, we must use a much more sophisticated technique.
Consider the class hierarchy in Figure 3–10, which shows the base class
DisplayItem along with three subclasses named Circle, Triangle, and
Rectangle. Rectangle also has one subclass, named SolidRectangle.
In the class DisplayItem, suppose that we define the instance variable
theCenter (denoting the coordinates for the center of the displayed item),
along with the following operations as in our earlier example:
■ draw - Draw the item
■ move - Move the item
■ location - Return the location of the item.
The operation location is common to all subclasses and therefore need
not be redefined, but we expect the operations draw and move to be rede-
fined since only the subclasses know how to draw and move themselves.
The class Circle must include the instance variable theRadius and
appropriate operations to set and retrieve its value. For this subclass,
the redefined operation draw draws a circle of the given radius, centered
on theCenter. Similarly, the class Rectangle must include the instance
variables theHeight and theWidth, along with appropriate operations to
set and retrieve their values. For this subclass, the operation draw
draws a rectangle with the given height and width, again centered on
theCenter. The subclass SolidRectangle inherits all characteristics of
the class Rectangle but again redefines the behavior of the operation draw.
Specifically, the implementation of draw for the class SolidRectangle
first calls draw as defined in its superclass Rectangle (to draw the outline
of the rectangle) and then fills in the shape. The invocation of draw
demands polymorphic behavior.
Suppose now that we have some client object that wishes to draw all of the
subclasses. In this situation, the compiler cannot statically generate code
to invoke the proper draw operation because the class of the object being
operated on is not known until runtime. Let’s consider how various object-
oriented programming languages deal with this situation.
Because Smalltalk is a typeless language, method dispatch is completely
dynamic. When the client sends the message draw to an item found in the
list, here is what happens.
■ The item object looks up the message in its class’s message dictionary.
■ If the message is found, the code for that locally defined method is invoked.
■ If the message is not found, the search for the method continues in
the superclass.
This process continues up the superclass hierarchy until the message is
found or until we reach the topmost base class, Object, without finding the
message. In the latter case, Smalltalk ultimately passes the message
doesNotUnderstand to signal an error.
The key to this algorithm is the message dictionary, which is part of each
class’s representation and is therefore hidden from the client. This dictio-
nary is created when the class is created and contains all the methods to
which instances of this class may respond. Searching for the message is
time-consuming; method lookup in Smalltalk takes about 1.5 times as long
as a simple subprogram call. All production-quality Smalltalk implementa-
tions optimize method dispatch by supplying a cached message dictionary,
so that commonly passed messages may be invoked quickly. Caching typi-
cally improves performance by 20% to 30% [31].
The operation draw defined in the subclass SolidRectangle poses a
special case. We said that its implementation of draw first calls draw as defined in the superclass Rectangle. In Smalltalk, we specify a super-
class method by using the keyword super. Then, when we pass the mes-
sage draw to super, Smalltalk uses the same method-dispatch algorithm
as mentioned earlier, except that the search begins in the superclass of the
object instead of its class.
Studies by Deutsch suggest that polymorphism is not needed about 85% of
the time, so message passing can often be reduced to simple procedure
calls [32]. Duff notes that in such cases, the developer often makes implicit
assumptions that permit an early binding of the object’s class [33]. Unfortu-
nately, typeless languages such as Smalltalk have no convenient means for
communicating these implicit assumptions to the compiler.
More strongly typed languages such as C++ do let the developer assert such
information. Because we want to avoid method dispatch wherever possible
but must still allow for the occurrence of polymorphic dispatch, invoking a
method in these languages proceeds a little differently than in Smalltalk.
In C++, the developer can decide whether a particular operation is to be
bound late by declaring it to be virtual; all other methods are considered
to be bound early, and thus the compiler can statically resolve the method
call to a simple subprogram call.
To handle virtual member functions, most C++ implementations use the
concept of a vtable, which is defined for each object requiring polymorphic
dispatch, when the object is created (and thus when the class of the object
is fixed). This table typically consists of a list of pointers to virtual functions.
For example, if we create an object of the class Rectangle, then the
vtable will have an entry for the virtual function draw, pointing to the closest
implementation of draw. If, for example, the class DisplayItem included
the virtual function Rotate, which was not redefined in the class Rectan-
gle, then the vtable entry for Rotate would point to the implementation of
Rotate in the class DisplayItem. In this manner, runtime searching is
eliminated: Referring to a virtual member function of an object is just an
indirect reference through the appropriate pointer, which immediately
invokes the correct code without searching [34].
Types vs. Classes
A type, in an object-oriented system, summarizes the common features of a set of objects with the same characteristics. It corresponds to the notion of an abstract data type. It has two parts: the interface and the implementation (or implementations). Only the interface part is visible to the users of the type, the implementation of the object is seen only by the type designer. The interface consists of a list of operations together with their signatures (i.e., the type of the input parameters and the type of the result).
The type implementation consists of a data part and an operation part. In the data part, one describes the internal structure of the object’s data. Depending on the power of the system, the structure of this data part can be more or less complex. The operation part consists of procedures which implement the operations of the interface part.
In programming languages, types are tools to increase programmer productivity, by insuring program correctness. By forcing the user to declare the types of the variables and expressions he/she manipulates, the system reasons about the correctness of programs based on this typing information. If the type system is designed carefully, the system can do the type checking at compile-time, otherwise some of it might have to be deferred at compile time. Thus types are mainly used at compile time to check the correctness of the programs. In general, in type-based systems, a type is not a first class citizen and has a special status and cannot be modified at run-time.
The notion of class is different from that of type. Its specification is the same as that of a type, but it is more of a run-time notion. It contains two aspects: an object factory and an object warehouse. The object factory can be used to create new objects, by performing the operation new on the class, or by cloning some prototype object representative of the class. The object warehouse means that attached to the class is its extension, i.e., the set of objects that are instances of the class. The user can manipulate the warehouse by applying operations on all elements of the class. Classes are not used for checking the correctness of a program but rather to create and manipulate objects. In most systems that employ the class mechanism, classes are first class citizens and, as such, can be manipulated at run-time, i.e., updated or passed as parameters. In most cases, while providing the system with increased flexibility and uniformity, this renders compile-time type checking impossible.
Of course, there are strong similarities between classes and types, the names have been used with both meanings and the differences can be subtle in some systems.
We do not feel that we should choose one of these two approaches and we consider the choice between the two should be left to the designer of the system (see Section 4.3). We require, however, that the system should offer some form of data structuring mechanism, be it classes or types. Thus the classical notion of database schema will be replaced by that of a set of classes or a set of types.
We do not, however, feel that is necessary for the system to automatically maintain the extent of a type (i.e., the set of objects of a given type in the database) or, if the extent of a type is maintained, for the system to make it accessible to the user. Consider, for example, the rectangle type, which can be used in many databases by multiple users. It does not make sense to talk about the set of all rectangles maintained by the system or to perform operations on them. We think it is more realistic to ask each user to maintain and manipulate its own set of rectangles. On the other hand, in the case of a type such as employee, it might be nice for the system to automatically maintain the employee extent.
Object-oriented programs are made up of objects. An object packages both data and
the procedures that operate on that data. The procedures are typically called methods
or operations. An object performs an operation when it receives a request (or message)
from a client.
Requests are the only way to get an object to execute an operation. Operations are
the only way to change an object’s internal data. Because of these restrictions, the
object’s internal state is said to be encapsulated; it cannot be accessed directly, and its
representation is invisible from outside the object.
The hard part about object-oriented design is decomposing a system into objects. The
task is difficult because many factors come into play: encapsulation, granularity, depen-
dency, flexibility, performance, evolution, reusability, and on and on. They all influence
the decomposition, often in conflicting ways.
Object-oriented design methodologies favor many different approaches. You can write
a problem statement, single out the nouns and verbs, and create corresponding classes
and operations. Or you can focus on the collaborations and responsibilities in your
system. Or you can model the real world and translate the objects found during analysis
into design. There will always be disagreement on which approach is best.
Many objects in a design come from the analysis model. But object-oriented designs
often end up with classes that have no counterparts in the real world. Some of these are
low-level classes like arrays. Others are much higher-level. For example, the Compos-
ite (163) pattern introduces an abstraction for treating objects uniformly that doesn’t
have a physical counterpart. Strict modeling of the real world leads to a system that
reflects today’s realities but not necessarily tomorrow’s. The abstractions that emerge
during design are key to making a design flexible.
Specifying Object Interfaces
Every operation declared by an object specifies the operation’s name, the objects it
takes as parameters, and the operation’s return value. This is known as the operation’s
signature. The set of all signatures defined by an object’s operations is called the
interface to the object. An object’s interface characterizes the complete set of requests
that can be sent to the object. Any request that matches a signature in the object’s
interface may be sent to the object.
A type is a name used to denote a particular interface. We speak of an object as having
the type “Window” if it accepts all requests for the operations defined in the interface
named “Window.” An object may have many types, and widely different objects can
share a type. Part of an object’s interface may be characterized by one type, and other
parts by other types. Two objects of the same type need only share parts of their
interfaces. Interfaces can contain other interfaces as subsets. We say that a type is a
subtype of another if its interface contains the interface of its supertype. Often we
speak of a subtype inheriting the interface of its supertype.
Interfaces are fundamental in object-oriented systems. Objects are known only through
their interfaces. There is no way to know anything about an object or to ask it to do
anything without going through its interface. An object’s interface says nothing about
its implementation—different objects are free to implement requests differently. That means two objects having completely different implementations can have identical
interfaces.
When a request is sent to an object, the particular operation that’s performed depends on
both the request and the receiving object. Different objects that support identical requests
may have different implementations of the operations that fulfill these requests. The
run-time association of a request to an object and one of its operations is known as
dynamic binding.
Dynamic binding means that issuing a request doesn’t commit you to a particular
implementation until run-time. Consequently, you can write programs that expect an
object with a particular interface, knowing that any object that has the correct interface
will accept the request. Moreover, dynamic binding lets you substitute objects that
have identical interfaces for each other at run-time. This substitutability is known as
polymorphism, and it’s a key concept in object-oriented systems. It lets a client object
make few assumptions about other objects beyond supporting a particular interface.
Polymorphism simplifies the definitions of clients, decouples objects from each other,
and lets them vary their relationships to each other at run-time.
Design patterns help you define interfaces by identifying their key elements and the
kinds of data that get sent across an interface. A design pattern might also tell you what
not to put in the interface. The Memento (283) pattern is a good example. It describes
how to encapsulate and save the internal state of an object so that the object can be
restored to that state later. The pattern stipulates that Memento objects must define two
interfaces: a restricted one that lets clients hold and copy mementos, and a privileged
one that only the original object can use to store and retrieve state in the memento.
Design patterns also specify relationships between interfaces. In particular, they often
require some classes to have similar interfaces, or they place constraints on the interfaces
of some classes. For example, both Decorator (175) and Proxy (207) require the interfaces
of Decorator and Proxy objects to be identical to the decorated and proxied objects. In
Visitor (331), the Visitor interface must reflect all classes of objects that visitors can visit.
Specifying Object Implementations So far we've said little about how we actually define an object. An object's imple- mentation is defined by its class. The class specifies the object's internal data and representation and defines the operations the object can perform. Our OMT-based notation (summarized in Appendix B) depicts a class as a rectangle with the class name in bold. Operations appear in normal type below the class name. Any data that the class defines comes after the operations. Return types and instance variable types are optional, since we don't assume a statically typed implementation language. Objects are created by instantiating a class. The object is said to be an instance of the class. The process of instantiating a class allocates storage for the object's internal data (made up of instance variables) and associates the operations with these data. Many similar instances of an object can be created by instantiating a class. A dashed arrowhead line indicates a class that instantiates objects of another class. The arrow points to the class of the instantiated objects. New classes can be defined in terms of existing classes using class inheritance. When a subclass inherits from a parent class, it includes the definitions of all the data and operations that the parent class defines. Objects that are instances of the subclass will contain all data defined by the subclass and its parent classes, and they'll be able to perform all operations defined by this subclass and its parents. An abstract class is one whose main purpose is to define a common interface for its subclasses. An abstract class will defer some or all of its implementation to operations defined in subclasses; hence an abstract class cannot be instantiated. The operations that an abstract class declares but doesn't implement are called abstract operations. Classes that aren't abstract are called concrete classes. Subclasses can refine and redefine behaviors of their parent classes. More specifically, a class may override an operation defined by its parent class. Overriding gives subclasses a chance to handle requests instead of their parent classes. Class inheritance lets you define classes simply by extending other classes, making it easy to define families of objects having related functionality. The names of abstract classes appear in slanted type to distinguish them from concrete classes. Slanted type is also used to denote abstract operations. A diagram may include pseudocode for an operation's implementation; if so, the code will appear in a dog- eared box connected by a dashed line to the operation it implements. A mixin class is a class that's intended to provide an optional interface or functionality to other classes. It's similar to an abstract class in that it's not intended to be instantiated.
Class versus Interface Inheritance It's important to understand the difference between an object's class and its type. An object's class defines how the object is implemented. The class defines the object's internal state and the implementation of its operations. In contrast, an object's type only refers to its interface—the set of requests to which it can respond. An object can have many types, and objects of different classes can have the same type. Of course, there's a close relationship between class and type. Because a class defines the operations an object can perform, it also defines the object's type. When we say that an object is an instance of a class, we imply that the object supports the interface defined by the class. Languages like C++ and Eiffel use classes to specify both an object's type and its imple- mentation. Smalltalk programs do not declare the types of variables; consequently, the compiler does not check that the types of objects assigned to a variable are subtypes of the variable's type. Sending a message requires checking that the class of the receiver implements the message, but it doesn't require checking that the receiver is an instance of a particular class. It's also important to understand the difference between class inheritance and interface inheritance (or subtyping). Class inheritance defines an object's implementation in terms of another object's implementation. In short, it's a mechanism for code and representation sharing. In contrast, interface inheritance (or subtyping) describes when an object can be used in place of another. It's easy to confuse these two concepts, because many languages don't make the dis- tinction explicit. In languages like C++ and Eiffel, inheritance means both interface and implementation inheritance. The standard way to inherit an interface in C++ is to inherit publicly from a class that has (pure) virtual member functions. Pure inter- face inheritance can be approximated in C++ by inheriting publicly from pure abstract classes. Pure implementation or class inheritance can be approximated with private inheritance. In Smalltalk, inheritance means just implementation inheritance. You can assign instances of any class to a variable as long as those instances support the opera- tion performed on the value of the variable. Although most programming languages don't support the distinction between inter- face and implementation inheritance, people make the distinction in practice. Smalltalk programmers usually act as if subclasses were subtypes (though there are some well- known exceptions [Coo92]); C++ programmers manipulate objects through types de- fined by abstract classes. Many of the design patterns depend on this distinction. For example, objects in a Chain of Responsibility (223) must have a common type, but usually they don't share a com- mon implementation. In the Composite (163) pattern, Component defines a common interface, but Composite often defines a common implementation. Command (233), Observer (293), State (305), and Strategy (315) are often implemented with abstract classes that are pure interfaces.
Putting Reuse Mechanisms to Work
Most people can understand concepts like objects, interfaces, classes, and inheritance.
The challenge lies in applying them to build flexible, reusable software, and design
patterns can show you how.
Inheritance versus Composition
The two most common techniques for reusing functionality in object-oriented systems
are class inheritance and object composition. As we’ve explained, class inheritance lets you define the implementation of one class in terms of another’s. Reuse by subclassing
is often referred to as white-box reuse. The term “white-box” refers to visibility: With
inheritance, the internals of parent classes are often visible to subclasses.
Object composition is an alternative to class inheritance. Here, new functionality is
obtained by assembling or composing objects to get more complex functionality. Object
composition requires that the objects being composed have well-defined interfaces.
This style of reuse is called black-box reuse, because no internal details of objects are
visible. Objects appear only as “black boxes.”
Inheritance and composition each have their advantages and disadvantages. Class
inheritance is defined statically at compile-time and is straightforward to use, since
it’s supported directly by the programming language. Class inheritance also makes it
easier to modify the implementation being reused. When a subclass overrides some
but not all operations, it can affect the operations it inherits as well, assuming they call
the overridden operations.
But class inheritance has some disadvantages, too. First, you can’t change the imple-
mentations inherited from parent classes at run-time, because inheritance is defined
at compile-time. Second, and generally worse, parent classes often define at least part
of their subclasses’ physical representation. Because inheritance exposes a subclass to
details of its parent’s implementation, it’s often said that “inheritance breaks encap-
sulation” [Sny86]. The implementation of a subclass becomes so bound up with the
implementation of its parent class that any change in the parent’s implementation will
force the subclass to change.
Implementation dependencies can cause problems when you’re trying to reuse a sub-
class. Should any aspect of the inherited implementation not be appropriate for new
problem domains, the parent class must be rewritten or replaced by something more
appropriate. This dependency limits flexibility and ultimately reusability. One cure
for this is to inherit only from abstract classes, since they usually provide little or no
implementation.
Object composition is defined dynamically at run-time through objects acquiring refer-
ences to other objects. Composition requires objects to respect each others’ interfaces,
which in turn requires carefully designed interfaces that don’t stop you from using
one object with many others. But there is a payoff. Because objects are accessed solely
through their interfaces, we don’t break encapsulation. Any object can be replaced at
run-time by another as long as it has the same type. Moreover, because an object’s im-
plementation will be written in terms of object interfaces, there are substantially fewer
implementation dependencies.
Object composition has another effect on system design. Favoring object composition
over class inheritance helps you keep each class encapsulated and focused on one task.
Your classes and class hierarchies will remain small and will be less likely to grow into
unmanageable monsters. On the other hand, a design based on object composition will
have more objects (if fewer classes), and the system’s behavior will depend on their
interrelationships instead of being defined in one class.
That leads us to our second principle of object-oriented design:
Favor object composition over class inheritance.
Ideally, you shouldn’t have to create new components to achieve reuse. You should
be able to get all the functionality you need just by assembling existing components
through object composition. But this is rarely the case, because the set of available
components is never quite rich enough in practice. Reuse by inheritance makes it easier
to make new components that can be composed with old ones. Inheritance and object
composition thus work together.
Nevertheless, our experience is that designers overuse inheritance as a reuse technique,
and designs are often made more reusable (and simpler) by depending more on ob-
ject composition. You’ll see object composition applied again and again in the design
patterns.
Abstraction Qualities (cohesion, coupling, etc)
Measuring the Quality of an Abstraction
How can one know if a given class or object is well designed? We suggest five
meaningful metrics:
1. Coupling
2. Cohesion
3. Sufficiency
4. Completeness
5. Primitiveness
Coupling is a notion borrowed from structured design, but with a liberal interpre-
tation it also applies to object-oriented design. Stevens, Myers, and Constantine
define coupling as “the measure of the strength of association established by a
connection from one module to another. Strong coupling complicates a system
since a module is harder to understand, change, or correct by itself if it is highly
interrelated with other modules. Complexity can be reduced by designing systems
with the weakest possible coupling between modules” [52]. A counterexample to
good coupling is given by Page-Jones in his description of a modular stereo sys-
tem in which the power supply is located in one of the speaker cabinets [53].
Coupling with regard to modules still applies to object-oriented analysis and
design, but coupling with regard to classes and objects is equally important. How-
ever, there is tension between the concepts of coupling and inheritance because
inheritance introduces significant coupling. On the one hand, weakly coupled
classes are desirable; on the other hand, inheritance—which tightly couples
superclasses and their subclasses—helps us to exploit the commonality among
abstractions.
The idea of cohesion also comes from structured design. Simply stated, cohesion
measures the degree of connectivity among the elements of a single module (and
for object-oriented design, a single class or object). The least desirable form of
cohesion is coincidental cohesion, in which entirely unrelated abstractions are
thrown into the same class or module. For example, consider a class comprising
the abstractions of dogs and spacecraft, whose behaviors are quite unrelated. The
most desirable form of cohesion is functional cohesion, in which the elements of
a class or module all work together to provide some well-bounded behavior.
Thus, the class Dog is functionally cohesive if its semantics embrace the behavior
of a dog, the whole dog, and nothing but the dog.
Closely related to the ideas of coupling and cohesion are the criteria that a class or
module should be sufficient, complete, and primitive. By sufficient, we mean that
the class or module captures enough characteristics of the abstraction to permit
meaningful and efficient interaction. To do otherwise renders the component use-
less. For example, if we are designing the class Set, it is wise to include an oper-
ation that removes an item from the set, but our wisdom is futile if we neglect an
operation that adds an item. In practice, violations of this characteristic are
detected very early; such shortcomings rise up almost every time we build a client
that must use this abstraction.
By complete, we mean that the interface of the class or module captures all of the
meaningful characteristics of the abstraction. Whereas sufficiency implies a mini-
mal interface, a complete interface is one that covers all aspects of the abstraction.
A complete class or module is thus one whose interface is general enough to be
commonly usable to any client. Completeness is a subjective matter, and it can be
overdone. Providing all meaningful operations for a particular abstraction over-
whelms the user and is generally unnecessary since many high-level operations
can be composed from low-level ones. For this reason, we also suggest that
classes and modules be primitive.
Primitive operations are those that can be efficiently implemented only if given
access to the underlying representation of the abstraction. Thus, adding an item to
a set is primitive because to implement this operation Add, the underlying repre-
sentation must be visible. On the other hand, an operation that adds four items to
a set is not primitive because it can be implemented just as efficiently on the more
primitive Add operation, without having access to the underlying representation.
Of course, efficiency is also a subjective measure. An operation is indisputably
primitive if we can implement it only through access to the underlying representa-
tion. An operation that could be implemented on top of existing primitive opera-
tions, but at the cost of significantly more computational resources, is also a
candidate for inclusion as a primitive operation.
Separation of concerns principle
In computer science, separation of concerns (SoC) is a design principle for separating a computer program into distinct sections, so that each section addresses a separate concern. A concern is a set of information that affects the code of a computer program. A concern can be as general as the details of the hardware the code is being optimized for, or as specific as the name of a class to instantiate. A program that embodies SoC well is called a modular[1] program. Modularity, and hence separation of concerns, is achieved by encapsulating information inside a section of code that has a well-defined interface. Encapsulation is a means of information hiding.[2] Layered designs in information systems are another embodiment of separation of concerns (e.g., presentation layer, business logic layer, data access layer, persistence layer).[3]
Separation of concerns results in higher degrees of freedom for some aspect of the program’s design, deployment, or usage. Common among these is higher degrees of freedom for simplification and maintenance of code. When concerns are well-separated, there are higher degrees of freedom for module reuse as well as independent development and upgrade. Hiding the implementation details of modules behind an interface enables improving or modifying a single concern’s section of code without having to know the details of other sections, and without having to make corresponding changes to those sections. Modules can also expose different versions of an interface, which increases the freedom to upgrade a complex system in piecemeal fashion without interim loss of functionality.
Separation of concerns is a form of abstraction. As with most abstractions, interfaces must be added and there is generally more net code to be executed. So despite the many benefits of well separated concerns, there is often an associated execution penalty.
Link:
https://en.wikipedia.org/wiki/Separation_of_concerns
Single responsibility principle
The single responsibility principle is a computer programming principle that states that every module, class, or function[1] should have responsibility over a single part of the functionality provided by the software, and that responsibility should be entirely encapsulated by the class, module or function. All its services should be narrowly aligned with that responsibility. Robert C. Martin expresses the principle as, “A class should have only one reason to change,”[1] although, because of confusion around the word “reason” he more recently stated “This principle is about people.(Actor)”
Link:
https://en.wikipedia.org/wiki/Single_responsibility_principle