Our Thinking

Refactoring Code to meet SOLID Design Principles - Part 2

Posted by Dawson Kroeker on Mar 17, 2014 2:40:45 PM

This is the final installment of a two-part series on what you can do to ensure your project's code base is following the SOLID design principles as closely as possible. In Part 1, we looked at the Single Responsibility Principle and the Open-Closed Principle. Here in Part 2, we will look at the final three principles: the Liskov Substitution Principle, the Interface Segregation Principle, and the Dependency Inversion Principle.

1. The Liskov Substitution Principle (LSP)

Liskov Substitution Principle

What's the Problem?

The Liskov Substitution Principle (LSP) explains that you should be able to substitute a subclass for a parent class without the client code caring at all.

Code that violates LSP is susceptible to very subtle bugs that can creep in when a subclass is used in place of a parent class and the calling code isn't updated to call it correctly.

1

Where do I Find it?

In an LSP violation, the calling code must know what concrete type the abstract class is in order to call it, leading to code like this:

2

Type checks are the first place to look for LSP violations, however, active LSP bugs do not yet have these type checks in place because the bug has not yet been fixed! To find these, look for subclasses that implement a method with nothing inside, or only an exception.

3

What can I do About it?

In an LSP violation the subclass violates the invariant or contract of the parent class. In other words, the parent abstraction doesn't accurately describe the subclass. This requires a change in hierarchy. Either the contract for the parent must change so that the subclass can fulfill it, or the subclass stops implementing the parent class. Sometimes shared code can be split out from the parent class into a new base class that both classes inherit from.

2. The Interface Segregation Principle (ISP)

What's the Problem?

The Interface Segregation Principle insists that clients should not be forced to depend on interfaces they do not use. Suppose we found a new breed of ducks that dance, and we wanted to pass it to the Theatre client that accepts Dancer objects.

4

We might modify the Duck interface to inherit from the Dancer interface so we can pass it along to the Theatre object. Unfortunately, with this solution, we have just forced every Duck to implement the Dance() method, even though many of them would never dream of dancing!

5

This also forces every client of Duck to know about the Dance() method as well. This solution pollutes the Duck interface with the Dancer interface and should be avoided.

Where do I Find it?

ISP violations can often be found hiding in tall class hierarchies, particularly in classes or interfaces with many methods. If you find classes with empty methods that exist solely to satisfy interfaces, you have found a likely candidate for an ISP violation.

What can I do About it?

Look for interfaces that have methods that do not apply to all its implementers and split the uncommon methods into another interface. Then only the classes that need this functionality need to implement it. In our Duck example, the DancingDuck can inherit from both the Duck and Duck interfaces.

6

3. The Dependency Inversion Principle

What's the Problem?

The Dependency Inversion Principle (DIP) is the last of the SOLID coding principles and makes two demands:

  1. High-level modules should not depend on low-level modules. Both should depend on abstractions.
  2. Abstractions should not depend on details. Details should depend on abstractions.

A quick way to see if a class is violating this principle is to look at its dependencies. If any of them are not interfaces or abstract classes, the class is likely violating DIP.

This ToyDuck implementation works fine while it uses Alkaline batteries, but what happens if we want to start using Nickel-metal hydride batteries? The ToyDuck class should not be depending on the details of its power source.

7

Where do I Find it?

One of the most obvious ways to find DIP violations is to write full coverage unit tests for your classes. As soon as a class has a dependency on a concrete object, you will start having trouble writing unit tests for that class! This is why using TDD can be such an effective way to enforce writing good code. The act of writing unit tests for a class means that the class under test has at least two contexts in which it runs: in production code and in a unit test run, helping the class rely on abstractions rather than concrete dependencies.

What can I do About it?

After you have encountered a dependency problem while writing unit tests, fix the problem by changing the class under test to depend on an interface instead of a concrete class. Now your unit test can pass a stub or a mock of that interface instead of the real object.

Instead of relying on Alkaline batteries directly, our ToyDuck class could accept a Batteries interface. But this poses a problem when we go to change the batteries. What type do we new up inside the ChangeBatteries() method? There are at least two options: 1) change the interface for ChangeBatteries() to accept new batteries instead of newing it up itself, or 2) accept a battery factory that takes care of creating the batteries for us. In this simple example, the first make sense; but in a more realistic situation, the factory can often be a helpful solution.

8

Conclusion

Hopefully this brief overview of the SOLID design principles will give you some idea of how you could go about refactoring your code base to meet these guidelines. As long as a code base is changing, it will require refactoring. Although refactoring takes effort, the SOLID principles are helpful guides to prevent your code from becoming a fragile mess that nobody wants to touch!

 

Topics: Design, Design Pattern

Our Thinking - The Online Blog is a source for insights, resources, best practices, and other useful content from our multi-disciplinary team of Onliners.

Subscribe to Blog Updates