How do we know how whether the design of a software system is good?
Symptoms of poor design.
- Rigidity. Rigidity is the tendency for software to be difficult to change, even in simple ways.
- Fragility. Fragility is the tendency of a program to break in many places when a single change is made.
- Immobility. A design is immobile when it contains parts that could be useful in other systems, but the effort and risk involved with separating those parts from the original system are too great.
- Viscosity. Viscosity comes in two forms: viscosity of the software and viscosity of the environment. When faced with a change, developers usually find more than one way to make that change. Some of the ways preserve the design; others do not (i.e., they are hacks). When the design-preserving methods are more difficult to use than the hacks, the viscosity of the design is high.
- Needless complexity. Overdesign.
- Needless repetition. Cut and paste may be useful text-editing operations, but they can be disastrous code-editing operations.
- Opacity. Opacity is the tendency of a module to be difficult to understand.
Principles (SOLID)
- (S)RP - Single-Responsibility Principle
- (O)CP - Open/Closed Principle
- (L)SP - Liskov Substitution Principle
- (I)SP - Interface Segregation Principle
- (D)IP - Dependency-Inversion Principle
Agile teams apply principles only to solve smells; they don't apply principles when there are no
smells. It would be a mistake to unconditionally conform to a principle just because it is a
principle. The principles are there to help us eliminate bad smells. They are not a perfume to be
liberally scattered all over the system. Over-conformance to the principles leads to the design
smell of needless complexity.
Example
public void Hangup(); public void Send(char c);
public char Recv();
}
There is a corollary here. An axis of change is an axis of change only if the changes occur. It is not wise to apply SRP or any other principle, for that matter, if there is no symptom.
An agile developer does not apply those principles and patterns to a big,
up-front design. Rather, they are applied from iteration to iteration in an attempt to keep the code,
and the design it embodies, clean.
Single-Responsibility Principle (SRP)
A class should have only one reason to change.
Example: In the diagram below you can see that Rectangle has two responsiblities: to draw itself, and to calculate the area:
Example: In the diagram below you can see that Rectangle has two responsiblities: to draw itself, and to calculate the area:
The former design breaks the SRP principle. We should separate the responsibilities into different classes:
What is responsibility?
In the context of the SRP, we define a responsibility to be a reason for change. If you can think of more than one motive for changing a class, that class has more than one responsibility.
Most of us will agree that this interface looks perfectly
reasonable. The four functions it declares are certainly functions belonging to a modem.
public interface Modem
{
public void Dial(string pno); public void Hangup(); public void Send(char c);
public char Recv();
}
Note that the design kept both responsibilities coupled in the ModemImplementation class. This is
not desirable, but it may be necessary. There are often reasons, having to do with the details of the
hardware or operating system, that force us to couple things that we'd rather not couple. However,
by separating their interfaces, we have decoupled the concepts as far as the rest of the application is
concerned.
There is a corollary here. An axis of change is an axis of change only if the changes occur. It is not wise to apply SRP or any other principle, for that matter, if there is no symptom.
Fortunately, the practice of test-driven development will usually force these
two responsibilities to be separated long before the design begins to smell. However, if the tests did
not force the separation, and if the smells of rigidity and fragility become strong, the design should
be refactored, using the Facade, DAO (Data Access Object), or Proxy patterns to separate the two
responsibilities.
The Open/Closed Principle (OCP)
Software entities (classes, modules, functions, etc.) should be open for extension but
closed for modification.
Modules that conform to OCP have two primary attributes.
-
They are open for extension. This means that the behavior of the module can be extended. As
the requirements of the application change, we can extend the module with new behaviors that
satisfy those changes. In other words, we are able to change what the module does.
-
They are closed for modification. Extending the behavior of a module does not result in changes
to the source, or binary, code of the module. The binary executable version of the
module whether in a linkable library, a DLL, or a .EXE file remains untouched.
How is it possible that the behaviors of a module can be modified without changing its source code?
Without changing the module, how can we change what a module does?
The answer is abstraction.
The answer is abstraction.
It is possible for a module to manipulate an abstraction. Such a module can be closed for
modification, since it depends on an abstraction that is fixed. Yet the behavior of that module can be
extended by creating new derivatives of the abstraction.
Example:
You may wonder why I named ClientInterface the way I did. Why didn't I call it AbstractServer instead? The reason, as we will see later, is that abstract classes are more closely associated to their clients than to the classes that implement them.
Example:
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.
Using Strategy pattern
In this case, the ClientInterface class is abstract with abstract member functions.You may wonder why I named ClientInterface the way I did. Why didn't I call it AbstractServer instead? The reason, as we will see later, is that abstract classes are more closely associated to their clients than to the classes that implement them.
Using Template method
The Policy class has a set of concrete public functions that implement a policy, similar to the functions of
the Client. As before, those policy functions describe some work that needs to be done
in terms of some abstract interfaces. However, in this case, the abstract interfaces are part of the Policy class itself. In C#, they would be abstract methods. Those functions are implemented in the
subtypes of Policy. Thus, the behaviors specified within Policy can be extended or modified by
creating new derivatives of the Policy class.
These two patterns are the most common ways of satisfying OCP. They represent a clear separation
of generic functionality from the detailed implementation of that functionality.
Since closure cannot be complete, it must be strategic. That is, the designer must choose the kinds of
changes against which to close the design, must guess at the kinds of changes that are most likely,
and then construct abstractions to protect against those changes.
Liskov Substitution Principle (LSP)
Subtypes must be substitutable for their base types.
Now the rule for preconditions and
postconditions of derivatives, as stated by Meyer, is: "A routine redeclaration [in a derivative] may
only replace the original precondition by one equal or weaker, and the original post-condition by one
equal or stronger."
We can view the postcondition of the Rectangle.Width setter as follows:
assert((width == w) && (height == old.height));
Clearly, the postcondition of the Square.Width setter is weaker than the postcondition of the
Rectangle.Width setter, since it does not enforce the constraint (height == old.height). Thus, the
Width property of Square violates the contract of the base class.
The term IS-A is too broad to act as a definition of a subtype. The true definition of a subtype is
substitutable, where substitutability is defined by either an explicit or implicit contract.
Interface Segregation Principle (ISP)
Clients should not be forced to depend on methods they do not use.
This principle deals with the disadvantages of "fat" interfaces. Classes whose interfaces are not
cohesive have "fat" interfaces. In other words, the interfaces of the class can be broken up into
groups of methods. Each group serves a different set of clients. Thus, some clients use one group of
methods, and other clients use the other groups.
ISP acknowledges that there are objects that require noncohesive interfaces; however, it suggests that clients should not know about them as a single class. Instead, clients should know about abstract base classes that have cohesive interfaces.
Example:
Consider the following example, Door implements Timer Client, even when not all doors are timed. This is an example of a fat interface.
This solution is my normal preference. The only time I would choose the previous solution is if the translation performed by the DoorTimerAdapter object were necessary or if different translations were needed at different times (i.e.: The interface in Timed Door had to be different from Timer Client).
ISP acknowledges that there are objects that require noncohesive interfaces; however, it suggests that clients should not know about them as a single class. Instead, clients should know about abstract base classes that have cohesive interfaces.
Example:
Consider the following example, Door implements Timer Client, even when not all doors are timed. This is an example of a fat interface.
Separation through delegation
Separation through multiple inheritance
This solution is my normal preference. The only time I would choose the previous solution is if the translation performed by the DoorTimerAdapter object were necessary or if different translations were needed at different times (i.e.: The interface in Timed Door had to be different from Timer Client).
Dependency-Inversion Principle (DIP)
- High-level modules should not depend on low-level modules. Both should depend on abstractions.
- Abstractions should not depend upon details. Details should depend upon abstractions.
The reason the word inversion is used is that more traditional software development methods, such as structured analysis and design, tend to create software structures in which high-level modules depend on low-level modules and in which policy depends on detail.
When DIP is applied, we find that the clients tend to own the abstract interfaces and that their servers derive from them.
Dependence on Abstractions
- No variable should hold a reference to a concrete class.
- No class should derive from a concrete class.
- No method should override an implemented method of any of its base classes.
Certainly, this heuristic is usually violated at least once in every program. Somebody has to create the instances of the concrete classes, and whatever module does that will depend on them.
Comments
Post a Comment