If you’ve never done Test Driven Development or aren’t even sure what this "crazy TDD stuff” is all about than this is the series for you. Over the next 30 days this series of posts take you from “I can spell TDD” to being able to consider yourself a “functional” TDD developer. Of course TDD is a very deep topic and truly mastering it will take quite a bit of time, but the rewards are well worth it. Along the way I’ll be showing you how tools like JustCode
can help you in your practice of TDD.
In the previous two posts we started our practice of TDD with a simple example of a requirement, a couple of test cases and tests to match. We implemented the code necessary to make those tests pass. Along the way we discussed some of the tools we’ll be using going forward, including NUnit and JustCode. In this post I’ll be discussing the SOLID principals and how they can help with your practice of TDD and help you write better software in general.
Previous Posts in this Series: Day Four – Making Your First Test Pass
Is Your Code SOLID?
The SOLID Principals of software development is the brain child or Robert “Uncle Bob” Martin. In the early 2000 he described five principals that software developers could use to guide them in creating software that was well designed, high quality and easier to maintain than what was being produced at the time. These five ideas are simple, promote good practices in software design and development, and go a long way to help us in our practice of TDD.
Single Responsibility Principle
The Single Responsibility Principal (SRP) is the idea that every method or class in your application should have exactly one reason to change. You can logically extend that to say that every class or method in your application should have exactly one (a single) job or responsibility. The short way of saying this is that each method and each class should be responsible for one, and only one task. For example let’s consider a class the functions as a shopping cart for an e-commerce website. As a virtual shopping cart it would be logical for the class to hold a collection of items that a user has added to the cart to purchase and perhaps a way to communicate with a service for the user to checkout. To extend this example let’s say that the site has a loyalty rewards program that rewards points to customers based on purchases. Any functionality to award, track or manage points would not be appropriate to add to the shopping cart. That functionality should reside in a separate service. The shopping cart should not be responsible for, or actually have any concept of the rewards program. The shopping cart has one job; store a list of items the user wants to purchase. Therefore the shopping cart only has one reason to change; the methodology of storing the list of items a customer has selected has changed. Changes to the rewards program should have no effect on the shopping cart, and thus not require that class to change if a change is made to the rewards program. By ensuring that we are writing methods and classes that have only one responsibility, we make these methods easier to test. Methods that try to do too much tend to require tests with more complex and elaborate “Arrange” sections that make the test longer and more difficult to understand and maintain. We can also extend the SRP to our tests and how we write them. Ideally each test we write should test exactly one thing. This will result in more tests, but those tests will be more granular. There are many benefits to this approach. To start with, the tests themselves will be simpler and easier to understand. Another benefit of this approach is that when a test fails you’ll have a pretty specific piece of information in terms of where to look for the issue. If a test does one thing and it fails you should only have one place to look. If a test tests two or more things you’ve increased the amount of time you have to spend diagnosing the problem. Conforming to SRP when writing your tests will keep your tests simple and help you know where to look when the test fails.
The Open/Close Principle
The Open/Close Principle (OCP) is very closely related to the previously discussed topics of Encapsulation and Inheritance. In fact it’s fair to say the OCP is concept that unifies these two tenants of OOP. OCP states that software, be it a method or a class, should be open for extension, but closed to modification. To get a better idea of what this means, I’m going tackle each one of those statements individually. When developer write software they are often reliant on libraries of classes from other developers, solutions or even third party providers. In order to keep the appeal of these components broad, their functionality is often designed to provide a generalized form of their purpose. In many cases we are able to use the components in these libraries “as-is” meaning that the generalized functionality provided is sufficient for the given need. On occasion however a more specialized version of these components may be required. At other times we simply need the base component to function differently than it normally does. According to OCP these components should be open for extension. There are a couple of ways to do this. The most obvious way is to create a new derived class that inherits from the base component and either overrides existing functionality or adds functionality that is needed. Another less obvious way is through the use another SOLID Principle called Dependency Inversion, which I will discuss in more detail below. These two tools enable me to extend or change the functionality of a class without actually changing its internals, hence it is “open for extension.” The second part of OCP states the components should be closed to modification. This ties in a bit with the Encapsulation rule, which states that the internals of a component should be treated as private. In this case the OCP is really saying that if you need to add additional functionality to a component, or change how current functionality works, you have a few options. But changing the internals of a component in a way that violates the public interface (and as such, the Encapsulation rule) is NOT one of those ways. Keeping base components closed ensures that others who are dependent on that component don’t suffer do to new and unexpected functionality. It also ensures that as updates to that component are made available you will be able to integrate them into your application. The role of OCP TDD will start to make more sense when we discuss Dependency Inversion later in this post and Dependency Injection in the next post. But in essence the OCP helps us with mocking (which will be discussed in more detail in a future post) by ensuring our classes are open to mocks via Dependency Inversion.
The Liskov Substitution Principle
The Liskov Substitution Principle (LSP) states that an object in your application should be able to be replaced with a type derived from it without breaking the application. For example, in our previous discussion of Polymorphism (OOP post), I discussed the concept of super classes and public API’s on classes. By way of a review, if a class inherits from a base class, then the base class is said to be the “super” class and the inherited class is a “derived” class. For example, Animal would be a super class of Dog, whereas Dog would be a derived class from Animal. According to LSP, if my application is expecting an object of type “Animal” I should be able to pass in any class derived from Animal (Dog, Cat, Fish, etc.) without there being any problem or issue. The application will treat it like a generic animal (it will only be able to call methods on the Animal public API) and doesn’t have to know or care specifically what type of object was actually passed in. Like OCP, the strength of LSP really comes into view when we start to talk about Dependency Inversion. LSP in conjunction OCP and with Dependency Inversion enabled the practice of mocking. In short, LSP helps to make our code testable by creating substitutes for the code’s dependencies which help with keeping our tests isolated.
The Interface Segregation Principle
The Interface Segregation Principle states that clients should not be forced to rely on interfaces they do not use. Another way to say this is that you should make fine grained interfaces that are specific to the functions and needs of the client. For example; you may have a service that approves all loan applications for a bank. But your clients may be subdivided between secured loans (mortgages, car loans, etc.) and unsecured loans (credit cards, line of credit). Moreover, the API to your service may have separate and distinct methods for each type of loan. According to ISP a large, bulky interface for this service that satisfies all clients is the wrong way to go; instead you should have several smaller and more finely grained interfaces that are targeted to specific business needs. The benefits of this principle happen to tie very closely with why it’s important to TDD. A large interface full of methods and properties a client rarely uses makes that interface complex and cumbersome. Dealing with a smaller and more targeted interface makes developing against that service much easier. And since a class can implement as many interfaces as you need it to, there’s really not any technological barrier to keeping these interfaces finely grained. In TDD, smaller interfaces are easier to mock, making our tests smaller, less complex and easier to understand.
The Dependency Inversion Principle
Coupling and binding in software is a fact of life. For all or our efforts to eliminate the need for binding, at the end of the day our classes have to bind to something to be usable. Recognizing that, we should endeavor to make this coupling as loose as possible. This is where the Dependency Inversion Principle (DIP) comes into play. The DIP is the idea that code should depend on abstractions; not concrete implementations. Furthermore, those abstractions should not depend on the details; the details should depend on the abstractions. This is a slightly complex way of expressing a very simple idea. For example, you have sell a Human Resources software package. The application can be used with a variety of different database services. The application has an employee management service that among other things updates employee information in the corporate database. The employee management service likely has a component that controls database access. You wouldn’t write the employee management service in a manner where it depended on a specific MS SQL Server component or an Oracle component. Instead you would write it to depend on a generic data services component, from which an MS SQL component and Oracle component can be derived. This way I can use either component with my installation of the software and so long as they both derive from the common data services component ancestor my software won’t know (or care) which specific component it’s using. In this case we’ve inverted the dependency; instead of the employee management service being depended on lower level and more detailed MS SQL Server or Oracle components it’s now dependent on an abstraction that the two database components are also dependent on. Please note; Dependency Inversion is not
the same thing as Dependency Injection. Dependency Injection is a methodology of achieving Dependency Inversion, but they are not the same thing. Dependency Injection will be covered in the next post. Dependency Injection is a crucial component of TDD and in the next post you’ll see how DIP and Dependency Injection enable us to use mocking to test our code in isolation.
If you weren’t familiar with the SOLID Principles before this post I hope it has piqued your interest in learning more about them. I’ve only scratched the surface here and there is far, far more to learn about each of the principles. For another take on the SOLID Principles you should check out this post
. If you already were familiar with SOLID I hope this review has given you a renewed perspective on the principles and how they affect our practice of TDD. In the next post we’ll dive back into some code by discussing Dependency Injection.
Continue the TDD journey: