Principles in software engineering are not usually rules of the form
Thou shalt … or Thou shalt not ….
Software design decisions are rarely justified by saying "Its a matter
of principle".
A principle's most important role is determining what you learn from
experience — how you understand cause and effect relationships
that deal with values.
So, software engineering principles focus your attention on
crucial issues:
-
Separation of Concerns
-
Anticipation of Change
-
Generality
-
Incremental Development
-
Consistency
Separation of concerns is a recognition of the need for human beings to
work within a limited context.
As described by G. A. Miller
[Miller56],
the human mind is limited to dealing with approximately seven units of
data at a time.
A unit is something that a person has learned to deal with as a whole
— a single abstraction or concept.
By separating software abstractions, the components that implement
them are easier to use and maintain.
Separation of concerns is closely related to the concepts of
cohesion and coupling.
Taking a broad view,
-
Software components can be regarded as cohesive when their
responsibilities abstract simple concerns.
-
Coupling can be regarded as a result of failure to separate
concerns.
Separation of concerns is a broad principle with many sub-principles
depending on the nature of the separation.
-
Modularity: separation based on functionality and
responsibility.
-
Abstraction: separating what a component does from how it
does it.
-
What/When Separation: Treating what is done and
when it gets done as separate concerns
-
Value Separation: Treating different values as
separate concerns.
-
Skill Separation: Treating developmental activities that
require different developer skills as separate concerns.
The principle of modularity is a specialization of the principle
of separation of concerns.
Modularity implies separating software into
components according to:
-
Functionality: What the software is supposed to do
-
Responsibility: Which components accomplish it
Modularity is related to the software developer's value
of cohesion, or making sure that a component's responsibility
centers around a single unifying abstraction.
Parnas
[Parnas72]
wrote one of the earliest papers discussing the considerations involved
in modularization.
A more recent work,
[WWW90],
describes a responsibility-driven methodology for modularization in an
object-oriented context.
The principle of abstraction is another specialization of the principle
of separation of concerns.
Abstraction requires:
-
Separating the behavior of software components from their
implementation
-
Looking at software and software components from
two points of view: what it does, and how it does it.
Failure to separate behavior from implementation is a common cause of
unnecessary coupling.
For example, it is common in recursive algorithms to introduce extra
parameters to make the recursion work. Here is a Scheme procedure
that can compute factorial:
(define (factorial-product a b) ; compute a × b!
(if (= b 0)
a
(factorial-product (* a b) (- b 1))))
To compute the factorial of n, one must call
(factorial-product 1 n)
which exposes details of how the computation is done and couples the
algorithm's use to its implementation.
To fix this, we define:
(define (factorial n)
(factorial-product 1 n))
Now to compute the factorial of
n, one calls
(factorial n)
and the implementation is free to change without clients knowing about it.
Design by contract is an important methodology for dealing with
abstraction.
According to this methodology, software design is guided by the
following specifications:
-
Preconditions: conditions for using a component
(responsibilities of the client/user)
-
Postconditions: guarantees concerning the behavior of
a component (responsibilities of the implementor)
-
Invariants: conditions guaranteed to be true while the
component runs (responsibilities of the implementor)
The basic ideas of design by contract are sketched by Fowler and Scott
[FS97].
The most complete treatment of the methodology is given by Meyer
[Meyer92a].
Software design often involves two concerns that are often unconsciously
tied together: what gets done and when it gets done.
There are numerous design problems where these two concerns need to be
separated.
Failure to separate these concerns results in what can be called
temporal coupling.
Many of the program design methodologies and techniques in use today
— data structures and design patterns for frameworks, toolkits,
and asynchronous programs — are aimed at addressing these
difficulties.
Software engineers must deal with complex value relationships in
attempting to optimize the quality of a product.
For example, timely delivery (a purchaser value) may conflict
with response time (a user value).
Thus it makes sense to separate handling of different
values by:
-
Dealing with different values at different
times in the software development process
-
Structuring the design so that responsibility for achieving
different values is assigned to different components.
Efficiency is often dealt with as a separate concern in
an
optimization step:
-
After the software is designed to meet other criteria, it's runtime
can be checked and analyzed to see where the time is being spent.
-
If necessary, the portions of code that are using the greatest part of
the runtime can be modified to improve it.
Complex applications often have several different types of components.
For example, a complex web application may involve
-
Java beans for managing user data
-
Web pages for presenting data to the user and allowing the user
to navigate to other web pages
-
Style sheets for layout and styling of data
-
A database for long-term store of data
A well-designed application framework allows developers with knowledge
of one type of component to work on that type of component with minimal
need for skills involved in other types of components:
-
Developers with database skills should be able to write
SQL commands without having to embed the commands
inside Java code.
-
Graphic designers should be able to write HTML code
without embedding Java code in web pages
-
Developers concerned with business logic should be able to
write Java code without having to embed HTML code
in source files
Focusing on
anticipation of change is important for two reasons:
-
The world in which software is situated changes
constantly, and
-
Change requires complex learning processes in both software
developers and their clients
The first reason is fairly obvious. The second requires some
explanation.
Computer software automates a solution to a problem.
The problem arises in some context, or domain that is familiar
to the users of the software.
Users of software within the domain see domain data
differently than software developers.
-
Users see data as the sources of information within their
domain
-
Software developers are familiar with a technology
that deals with data in an abstract way
-
They deal with structures and algorithms without regard for the
meaning or importance of the data that is involved.
-
Example: A software developer thinks in terms of graphs and graph
algorithms without attaching concrete meaning to vertices and
edges.
Working out an automated solution to a problem is thus a learning
experience for both software developers and their clients.
For developers:
-
They must learn the domain the clients work in
-
They must learn the values of the client:
-
What form of data presentation is most useful to the
client
-
What kinds of data are
crucial and require special protective measures
For clients:
-
They must learn to see the range of possible solutions that
software technology can provide
-
They must learn to evaluate the possible solutions with regard to
their effectiveness in meeting their needs
The principle of acticipation of change recognizes the complexity of the
learning process for both software developers and their clients.
If the problem to be solved is complex then it is not reasonable to
assume that the best solution will be worked out in a short period of
time, but some developer values can help:
-
Coupling is a major obstacle to change.
-
If two components are strongly coupled then it is likely that
changing one will not work without changing the other.
-
Cohesiveness has a positive effect on ease of
change.
-
Cohesive components are easier to reuse when requirements
change.
-
If a component has several tasks rolled up into one package, it is
likely that it will need to be split up when changes are made.
The principle of generality is important in designing software
that is free from unnatural restrictions and limitations, and that
survives beyond its expected lifetime.
Examples that embrace generality:
-
The use of more than two digits to represent year numbers,
which avoided the Y2K (year 2000) problem
-
Creating general frameworks that can be applied to many
specific problems
-
Coding to interfaces rather than class types
The principle of incremental development is embraced by
the spiral model of software process, and later agile
approaches.
In this process, you build the software in small increments; for
example, adding one use case at a time.
Advantages:
-
Simplifies verification
-
If you develop software by adding small increments of
functionality then for verification you only need to deal with the
added portion.
-
If there are any errors detected then they are already partly
isolated so they are much easier to correct.
-
Eases requirements changes if use cases likely to change are
put towards the end of the development process
The principle of consistency is a recognition of the fact that it
is easier to do things in a familiar context.
Such contexts can be as diverse as coding style and idioms, graphical
user interface design, and object-oriented library APIs.
Laying out code text in a consistent manner serves two purposes.
-
Makes reading the code easier
-
Allows programmers to automate parts of code entry, freeing the
programmer's mind to deal with more important issues
At a higher level, consistency involves the development of idioms and
patterns for dealing with common programming problems.
-
For example, Coplien
[Coplien92]
gives an excellent presentation of the use of idioms for coding in C++
Consistency serves two purposes in designing graphical user
interfaces.
-
A consistent look and feel makes it easier for users to learn to
use software
-
Once the basic elements of dealing with an interface are learned,
they do not have to be relearned for a different software
application
-
A consistent user interface promotes reuse of the interface
components
-
Graphical user interface toolkits have a collection of frames,
panes, and other view components that support a common look
-
They also have a collection of components for responding
to user input (e.g. menus and buttons),
supporting a common feel
As the object-oriented class libraries grow more and more complex it is
essential that they be designed to present a consistent interface to the
client:
-
Consistent naming
-
For example, use "remove" consistently rather than sometimes
"remove" and sometimes "delete"
-
Consistent ordering of parameters