SOLID Principle
SOLID Principle
(SOLID) Principles
For this reason, it is necessary for an agile team to maintain the code structure as
flexible as possible, so that the new requirements of the clients have the smallest
impact possible on the existing architecture. However, this doesn"t mean that the
team will make an extra effort to take into consideration the future requirements
and necessities of the clients, nor that it will spend more time to implement an
infrastructure which might support possible requirements necessary in the future.
Instead, this means that they will focus on developing the current product as well
as possible.
With this purpose in view, we shall investigate some of the software design
principles necessary to be applied by an agile programmer from one iteration to
another, in order to maintain the project"s code and design as clean and flexible as
possible.
In the SRP context, responsibility can be defined as "a reason to change". When
the requirements of the project modify, the modifications will be visible through
the alteration of the responsibilities of the classes. If a class has several
responsibilities, then, it will have more reasons to change. Having more coupled
responsibilities, the modifications on a responsibility will imply modifications on
the other responsibilities of the class. This correlation leads to a fragile design.
Suppose we have a class which encapsulates the concept of phone and the
associated functionalities.
class Phone
{
public void Dial(const string&
phoneNumber);
In the case where the signature of the methods responsible for performing the
connection would be subjected to changes, this design would be rigid, since all the
classes which call the Dial and Hangup methods would have to be recompiled. In
order to avoid this situation, a re-design is necessary, to divide the two
responsibilities.
Figure 1
In this example, the two responsibilities are separated, so that the class that uses
them - Phone, does not have to couple the two of them. The changes of the
connection will not affect the methods responsible with data transmission. On the
other hand, in the case where the two responsibilities do not show reasons for
modification in time, their separation is not necessary either. In other words, the
responsibilities of a class should be separated only if there are real chances that the
responsibilities would produce modifications, mutually influencing each other.
Conclusion
The Single-Responsibility Principle is one of the simplest of the principles but one
of the most difficult to get right. Finding and separating those responsibilities is
much of what software design is really about. In the rest of the principles of agile
software design we will analyse further on, we will come back to this issue in one
way or another.
This means that the behaviour of the code can be extended. When the requirements
of the project are modified, the code can be extended by implementing the new
requirements, meaning that one can modify the behaviour of the already existing
module.
The implementation of the new requirements does not need modifications on the
already existing code.
Fig. 2 presents a block of classes that do not conform to the open-closed principle.
Both the Client class and the Server class are concrete. The Client class uses the
Server class. If we want for a Client object to use a different server object, the
Client class must be changed to name the new server class.
Figure 2. Example which does not comply with the OCP 1 principle
In Fig.3, the same design as the one in Fig.2 is presented, but this time the open-
closed principle is observed. In this case, the abstract class AbstractServer was
introduced, and the Client class uses this abstraction. However, the Client class
will actually use the Server class which implements the ClientInterface class. If, in
the future, one wishes to use another type of server, all that needs to be done is to
implement a new class derived from the ClientInterface class, but this time the
client doesn"t need to be modified.
A particular aspect in this example is the way we named the abstract class
ClientInterface and not ServerInterface, for example. The reason for this choice is
the fact that abstract classes are more closely associated to their clients than to the
classes that implement them.
The Open-Closed principle is also used in the Strategy and Plugin design patterns
(3). For instance, Fig.4 presents the corresponding design, which observes the
open-closed principle.
Figure 4
The Sort_Object class performs a function of sorting objects, function which can
be described in the abstract interface Sort_Object_Interface. The classes derived
from the abstract class Sort_Object_Interface are forced to implement the
methodSort_Function(), but, at the same time, they have the freedom to offer any
implementation for this interface. Thus, the behaviour specified in the interface of
the method void Sort_Function(), can be extended and modified by creating new
subtypes of the abstract class Sort_Object_Interface.
In the definition of the class Sort_Object we will have the following methods:
void Sort_Object::Sort_Function()
{
m_sort_algorithm->sortFunction();
}
void Sort_Object::Set_Sort_Algorithm(const Sort_Object_Interface*
sort_algorithm)
{
std::cout << "Setting a new sorting algorithm..." << std::endl;
m_sort_algorithm = sort_algorithm;
}
Conclusions
The main mechanisms behind this principle are abstraction and polymorphism.
Whenever the code has to be modified in order to implement some new
functionality, one must also take into consideration the creation of an abstraction
which can provide an interface for the desired behaviour and offer at the same time
the possibility to add new behaviours for the same interface in the future. Of
course, the creation of an abstraction is not always necessary. This method is
generally useful where there are frequent changes to be made.
In exchange, the Open/Closed Principle is, in many ways, at the heart of object-
oriented programming. Conformance to this principle is what yields the greatest
benefits claimed for object-oriented technology: code flexibility, reusability and
maintainability.
C. Liskov Substitution Principle (LSP)
Subtypes must be substitutable for their base types.
In languages such as C++ or Java, the main mechanism through which abstraction
and polymorphism is done is inheritance. In order to create a correct inheritance
hierarchy we must make sure that the derived classes extend, without replacing, the
functionality of the base classes. In other words, the functions using pointers or
references to the base classes should be able to use instances of the derived classes
without being aware of this. Contrary, the new classes may produce undesired
outcomes when they are used in the entities of the already existing program. The
importance of the LSP principle becomes obvious the moment it is violated.
Example:
Suppose we have a Shape class, whose objects are already used somewhere in the
application and which has a SetSize method, containing the mSize property which
can be used as a side or diameter, depending on the represented figure.
class Shape
{
public:
void SetSize(double size);
void GetSize(double& size);
private:
double mSize;
};
Figure 5
Later on, we will extend the application by adding the Square and Circle classes.
Taking into consideration the fact that the inheritance models an IS_A relationship,
the new Square and Circle classes can be derived from the Shape class.
Let"s suppose hereafter that the Shape objects are returned by a factory method,
based on some conditions established at run time, so that we do not know exactly
the type of the returned object. But we do know it is Shape. We get the Shape
object, we set its size property to 10 units and we compute its surface. For a Square
object, the area will be 100.
In this example, when the f function gets r as a parameter, an instance of the Circle
class will have a wrong behaviour. Since, in function f, the Square type objects
cannot substitute the Rectangle type objects, the LSP principle is violated. The f
function is fragile in relation to the Square/Circle hierarchy.
Conclusions
The LSP principle is a mere extension of the Open-Closed principle and it means
that, when we add a new class derived in an inheritance hierarchy, we must make
sure that the newly added class extends the behaviour of the base class, without
modifying it.
This principle stresses the fact that when an interface is being defined, one must be
careful to put only those methods which are specific to the client in the interface. If
in an interface one adds methods which do not belong there, then the classes
implementing the interface will have to implement those methods, too. For
instance, if we consider the interface Employee, which has the method Eat, then all
the classes implementing this interface will also have to implement the Eatmethod.
However, what happens if the Employee is a robot? Interfaces containing
unspecific methods are called "polluted" or "fat" interfaces.
class Timer
{
public:
void Register(int timeout, TimerClient* client);
};
class TimerClient
{
public:
virtual void TimeOut();
};
class Door
{
public:
virtual void Lock() = 0;
virtual void Unlock() = 0;
virtual bool IsDoorOpen() = 0;
};
The separation of interfaces can be done through the mechanism of multiple
inheritance. In Fig. 9, we can see how multiple inheritance can be used to comply
with the Interface Segregation principle in design. In this model, the TimeDoor
interface inherits from both Door and TimerClient interfaces.
Figure 8
Figure 9
Conclusions
Polluted or fat classes cause harmful couplings between their clients. When one
client forces a change on the fat class, all the other clients of the polluted class are
affected. Thus, clients should have to depend only on methods that they actually
call. This can be achieved by breaking the interface of the fat class into many
client-specific interfaces. Each client-specific interface declares only those
functions that its particular client or client group invoke. The fat class can then
inherit all the client-specific interfaces and implement them. This breaks the
dependence of the clients on methods that they don"t invoke and allows the clients
to be independent of one another.
B. Abstractions should not depend upon details. Details should depend upon
abstractions.
This principle enunciates the fact that the high-level modules must be independent
of those on lower levels. This decoupling is done by introducing an abstraction
level between the classes forming a high hierarchy level and those forming lower
hierarchy levels. In addition, the principle states that the abstraction should not
depend upon details, but the details should depend upon the abstraction. This
principle is very important for the reusing of software components. Moreover, the
correct implementation of this principle makes it much easier to maintain the code.
Figure 6
Figure 7 presents the same class diagram as in Fig.6, but this time the dependency
inversion principle is observed. Thus, to each level accessing the functionality of a
lower level, we added an interface which will be implemented by the lower level.
This way, the interface through which the two levels communicate is defined in the
higher hierarchical level, so that the dependency was reversed, namely the low
level depends on the high level. Modifications carried out on the low levels no
longer affect the high levels, but it happens backwards. In conclusion, the class
diagram in Fig. 7 complies with the dependency inversion principle.
Figure 7
Conclusions
Procedural traditional programming creates dependency policies where high level
modules depend on the details of low level modules. This programming method is
inefficient, since modifications of the details lead to modifications in the high level
modules also. Object oriented programming reverses this dependency mechanism,
so that both the details and the high levels depend upon abstractions, and the
services often belong to the clients.
No matter the programming language used, if the dependencies are inverted, then
the code design is object oriented. If the dependencies are not inversed, then the
design is procedural. The dependency inversion principle represents the
fundamental low level mechanism at the origin of many benefits offered by the
object oriented programming. Complying with this principle is fundamental for the
creation of reusable modules. It is also essential for writing code that can stand
modifications. As long as the abstractions and the details are mutually isolated, the
code is much easier to maintain.
Agile design represents a process based on the continuous application of the agile
principles and the design patterns, so that the design of the application constantly
remains simple, clean and as expressive as possible.