Skip to main content

Agile design: SOLID

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. 
  • FragilityFragility 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.


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:



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. 

Example


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();
}


However, two responsibilities are being shown here. The first responsibility is connection management. The second is data communication. The dial and hangup functions manage the connection of the modem; the send and recv functions communicate data.







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.
  1. 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.
  2. 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


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


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.


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)

  1. High-level modules should not depend on low-level modules. Both should depend on abstractions. 
  2. 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

Popular posts from this blog

Mock pattern

using mock pattern TestPayroll public void testPayroll() {    MockEmployeeDatabase db = new MockEmployeeDatabase();    MockCheckWriter w = new MockCheckWriter();    Payroll p = new Payroll(db, w);    p.payEmployees();    assert(w.checksWereWrittenCorrectly());    assert(db.paymentsWerePostedCorrectly()); }