Chapter 6 - SW Design
Chapter 6 - SW Design
Software Engineering - I
An Introduction to Software Construction Techniques for Industrial
Strength Software
Introduction
It includes modeling of the data structures and entities, the physical and logical
partitioning of the system into components, and the interfaces between different
components of the system as well as interfaces to the outside world. Sometimes design of
algorithms is also included in this activity.
A complex system that works is invariably found to have evolved from a simple system
that worked. The structure of a system also plays a very important role. It is likely that
we understand only those systems that have hierarchical structure and where intra-
component linkages are generally stronger than inter component linkages. To manage the
complexity of the system we need to apply the principles of separation of concern,
modularity, and abstraction. This leads to designs that are easy to understand and hence
easy to maintain.
Separation of concern, modularity, and abstraction are different but related principles.
A complex system may be divided into smaller pieces of lesser complexity called
modules. This is the classic divide-and-conquer philosophy – if you cannot solve a
complex problem, try to break it into smaller problems that you can solve separately and
then integrate them together in a systematic fashion to solve the original problem. One
major advantage of modularity is that it allows the designer to apply the principle of
separation of concern on individual modules.
Software design is not a sequential process. Design of a software system evolves through
a number of iterations. The design process usually involves developing a number of
different models, looking at the system from different angles and describing the system at
various levels of abstraction. Like the various different models used during requirement
engineering domain models, these models complement each other. As stated earlier,
software design provides a road map for implementation by clearly describing how the
software system is to be realized.
Software design process revolves around decomposing the system into smaller and
simpler units and then systematically integrate these units to achieve the desired results.
Two fundamental strategies have been used to that end. These are functional or structured
design and object oriented design.
In the functional design, the structure of the system revolves around functions. The entire
system is abstracted as a function that provides the desired functionality (for example, the
main function of a C program). This main function is decomposed into smaller functions
and it delegates its responsibilities to these smaller functions and makes calls to these
functions to attain the desired goal. Each of these smaller functions is decomposed into
even smaller functions if needed. The process continues till the functions are defined at a
level of granularity where these functions can be implemented easily. In this design
approach, the system state, that is the data maintained by the system, is centralized and is
shared by these functions.
The object-oriented design takes a different approach. In this case the system is
decomposed into a set of objects that cooperate and coordinate with each other to
implement the desired functionality. In this case the system state is decentralized and
each object is held responsible for maintaining its own state. That is, the responsibility of
marinating the system state is distributed and this responsibility is delegated to individual
objects. The communication and coordination among objects is achieved through
message passing where one object requests the other object if it needs any services from
that object.
The object-oriented approach has gained popularity over the structured design approach
during the last decade or so because, in general, it yields a design that is more
maintainable than the design produced by the functional approach.
A software design can be looked at from different angles and different parameters can be
used to measure and analyze its quality. These parameters include efficiency,
compactness, reusability, and maintainability. A good design from one angle may not
seem to be suitable when looked from a different perspective. For example, a design that
yields efficient and compact code may not be very easy to maintain. In order to establish
whether a particular design is good or not, we therefore have to look at the project and
application requirements. For example, if we need to design an embedded system for the
control of a nuclear reactor or a cruise missile, we would probably require a system that
is very efficient and maintainability would be of secondary concern. On the other hand, in
the case of an ordinary business system, we would have a reversal in priorities.
Maintainable Design
Since, in general, maintenance contributes towards a major share of the overall software
cost, the objective of the design activity, in most cases, is to produce a system that is easy
to maintain. A maintainable design is the one in which cost of system change is minimal
and is flexible enough so that it can be easily adapted to modify exiting functionality and
add new functionality.
Strong cohesion implies that all parts of a component should have a close logical
relationship with each other. That means, in the case some kind of change is required in
the software, all the related pieces are found at one place. Hence, once again, the scope is
limited to that component itself.
A component should implement a single concept or a single logical entity. All the parts of
a component should be related to each other and should be necessary for implementing
that component. If a component includes parts that are not related to its functionality,
then the component is said to have low cohesion.
Coupling and cohesion are contrasting concepts but are indirectly related to each other.
Cohesion is an internal property of a module whereas coupling is its relationship with
other modules. Cohesion describes the intra-component linkages while couple shows the
A good example of a system with a very high cohesion and very less (almost nil)
coupling is the electric subsystem of a house that is made up of electrical appliances and
wires. Since each one of the appliances has a clearly definable function that is completely
encapsulated within the appliance. That means that an appliance does not depend upon
any other appliance for its function. Therefore, each appliance is a highly cohesive unit.
Since there are no linkages between different appliances, they are not coupled. Let us
now assume that we have added a new centralized control unit in the system to control
different appliances such as lights, air conditioning, and heating, according to certain
settings. Since this control unit is dependent upon the appliances, the overall system has
more coupling than the first one.
Modules with high cohesion and low coupling can be treated and analyzed as black
boxes. This approach therefore allows us to analyze these boxes independent of other
modules by applying the principle of separation of concern.
This diagram depicts two systems, one with high coupling and the other one with low
coupling. The lines depict linkages between different components. In the case of highly
coupled system, module boundaries are not well defined, as everything seems to be
connected with everything else. On the other hand, in the system with low coupling
modules can be identified easily. In this case intra component linkages are stronger while
inter component linkages are weak.
Example of Coupling
The modules that interact with each other through message passing have low coupling
while those who interact with each other through variables that maintain information
about the state have high coupling. The following diagram shows examples of two such
systems.
Module A
A's Data
Module C
A
High Coupling
C's
A's Data
Low Coupling
In order to understand this concept, let us consider the following example. In this
example, we have a class vector in which the data members have been put in the public
part.
class vector {
public:
float x;
float y;
vector (float x, float y);
float getX();
float getY();
float getMagnitude();
float getAngle();
};
Now let us assume that we want to write a function to calculate dot product of two
vectors. We write the following function.
Since the data members are public, one could be enticed to use these members directly
(presumably saving some function calls overhead) and rewrite the same function as
follows:
So far, there does not seem to be any issue. But the scenario changes as soon as there are
changes in the class implementation. Now let us assume that for some reason the class
designer changes the implementation and data structure and decides to stores the angle
and magnitude instead of the x and y components of the vector. The new class looks like
as follows:
class vector {
public:
float magnitude;
float angle;
vector (float x, float y);
vector (float magnitude, float angle);
float getX();
float getY();
float getMagnitude();
float getAngle();
};
Now we see the difference in the two implementations of the dot product function written
by the user of this class. In the first case, as the dot product function is dependent upon
the public interface of the vector class, there will be no change while in the second case
the function will have to be rewritten. This is because in the first case the system was
loosely coupled while in the second case there was more dependency on the internal
structure of the vector class and hence there was more coupling.
Example of Cohesion
As mentioned earlier, strong cohesion implies that all parts of a component should have a
close logical relationship with each other. That means, in case some kind of change is
required in the software, all the related pieces are found at one place.
A class will be cohesive if most of the methods defined in a class use most of the data
members most of the time. If we find different subsets of data within the same class being
manipulated by separate groups of functions then the class is not cohesive and should be
broken down as shown below.
Class X1
f1 f1
f2 f2
f3
f4
Class X2
f3
f4
class order {
public:
int getOrderID();
date getOrderDate();
float getTotalPrice();
int getCustometId();
string getCustomerName();
string getCustometAddress();
int getCustometPhone();
The Order class shown above represents an Order entity that contains the attributes and
behavior of a specific order. It is easy to see that this contains information about the order
as well as the customer which is a distinct entity. Hence it is not a cohesive class and
must be broken down into two separate classes as shown. In this case each on of these is
a more cohesive class.
class order {
public:
int getOrderID();
date getOrderDate();
float getTotalPrice();
int getCustometId();
class customer {
public:
int getCustometId();
string getCustomerName();
string getCustometAddress();
int getCustometPhone();
int getCustomerFax();
Engineers of all fields, including computer science, have been practicing abstraction for
mastering complexity. Consider the following example.
This function can be rewritten by abstracting out some of the logical steps into auxiliary
functions. The new code is as follows.
In this function we have abstracted out two logical steps performed in this functions.
These functions are finding the index of the minimum value in the given range in an array
and swapping the minimum value with the value at the ith index in the array. It is easy to
see that the resultant new function is easier to understand than the previous version of the
selection sort function. In the process, as a by-product, we have created two auxiliary
function mentioned above, which are general in nature and hence can be used elsewhere
as well. Principle of abstraction thus generates reusable self-contained components.
Let us now try to understand the difference between object-oriented and function oriented
(or action oriented) approach.
Functions
Data
In this diagram, the ovals depict the function while rectangles/squares depict data. Since a
function contains dynamic information while data contains only static information, if the
function and data are managed separately, the required data components can be found by
scanning a function but the functions that use a particular data cannot be found by just
looking at the data. That is, the function knows about the data it needs to use but the data
do not know about the functions using it. That means it is easy to make a change in a
function since we would know which data components would be affected by this change.
On the other hand, changing a data structure would be more difficult because it would not
be easy to find all the functions that are using this data and hence also need to be
modified.
Object oriented approach solves this problem by putting the relevant data and
functionality together at one place. Hence, in case of a change, the effected components
can be identified easily and the effect of change is localized. Therefore, maintenance
becomes relatively easy as compared to function-oriented approach. This is made
possible because the data is not shared in this case. Anyone needing any information
contained in there would request the encapsulating object by sending it a message
through the interface provided by the object. In this case we create highly cohesive
objects by keeping the related data and function at one place and spinning-off non-related
information into other classes. This can be elaborated with the help of the following
diagram.
1 2
Let us assume that the circles represent sets of functions and rectangles represent data
that these function use to carry out their operation. In the object-oriented design, the data
areas that are common among different sets of functions would be spun-off into their own
classes and the user function would use these data through their interfaces only. This is
shown in the following diagram.
6 2 10
1
4
7
8 9