Design and Test Driven Development

Design and Test Driven Development

TDD is not about testing; it’s more of a design technique. But how can writing tests before you write the code encourage a good design? Simply taking the time to stop and think before you write code can have a huge impact on the design of a system. This leads to an important question: what is a good design?

Well-designed software is the subject of many different books. However, we follow a core set of concepts focused on the SOLID principles outlined by Robert Martin (a.k.a. Uncle Bob). SOLID is a set of five principles based on the best practices for object-oriented development.

 

The SOLID design principles started as a reply to a 1995 newsgroup thread entitled “The Ten Commandments of OO Programming.” This reply was formed by Robert Martin (Uncle Bob), and has become a great set of standards that many developers follow.

The principles encompassed in SOLID have been around for years, but many developers tend to ignore these principles that have been proven to aid in creating great software.

S—The Single Responsibility Principle

“There should never be more than one reason for a class to change.”

Robert Martin (Uncle Bob) http://www.objectmentor.com/resources/articles/srp.pdf

The first principle defined in SOLID is that each class should have a single responsibility. A responsibility is defined here as being a reason to change. If a class has multiple responsibilities, then there are multiple reasons why it could change. If each class only has its own responsibilities, then they will be more focused, isolated, and easier to test as a result.

For example, consider the Customer class in Figure 2-2.

Figure 2-2: Customer class model with multiple responsibilities

This class has two different responsibilities which breaks this principle. One role for the Customer class in Figure 2-2 is storing the domain information about the customer, for example their name and address. The second role is the ability to persist this information. If you wanted to add more information about the customer, then that would cause it to change. If you were to change the way you persisted the customer, then that would also cause the same class to change.

This is not ideal. Ideally, there would be two separate classes with each focused on their own roles in the system, as shown in Figure 2-3.

Figure 2-3: Customer class and Repository class model each with separate responsibilities

Figure 2-3 splits the original design into two classes. The system is now more isolated and each class focuses on its own responsibility. As your system is now more decoupled, if you wanted to change how you persisted a Customer object you would either change the CustomerRepository object or create a separate class without having to modify the Customer class.

When you attempt to understand a class to modify it, you tend to attempt to understand all the roles a class performs as a whole, even if there are a lot of roles. It can be a daunting task to be faced with a new section of the system because there is so much to understand. If your classes were more isolated, with correct responsibilities, then your code would be easier to understand because you would be able to process the information in small chunks.

From the viewpoint of TDD and testability, the tests you write are now more focused. If you wanted to test either a Customer object or an object dependent on the Customer object, you wouldn’t need to be concerned with the persistence side. If you had to initiate a database connection every time you created a Customer object, then your tests would become more heavyweight and take much longer to execute. As with the code, the tests that are created for these objects are now also easier to understand due to the more focused isolation.

However, one of the difficult facts about this principle is identifying the actual requirements. As mentioned, the Single Responsibility Principle defines a responsibility as a “reason for change.” If you look at a class and can identify more than one reason why it might need to change, then it has more than one responsibility.

O—The Open Closed Principle

“Software entities (classes, modules, functions, etc) should be open for extension but closed for modification.”

Robert Martin (Uncle Bob) http://www.objectmentor.com/resources/articles/ocp.pdf

This principle is focused on creating decoupled, extendable systems so that when changes are required, they can be achieved in an effective fashion with very little impact and overall cost.

The term open refers to the fact that an item should be extendable and abstract. This means that if you need to change some of the behavior of the class, you can achieve it without having to change the code. An example of this is to replace the dependencies of different parts of the system by providing a different implementation or take advantage of inheritance and override selected behavior. To do this, you need to think about extensibility and how your application is coupled together.

The term closed relates to the first principle of the Single Responsibility Principle (SRP): a class should be closed from requiring modifications to be made when adding new or changing functionality. When reviewing your code, if you find that a section of code needs to be changed every time you implement new functionality, then it isn’t closed.

In real-world terms, a class can never be closed from modifications; however, you can make huge gains by thinking about how you structure your code so that it is as closed as it possibly can be. Remember that SOLID is a set of principles, not hard and fast rules.

First, you’ll look at an example which violates the openclosed principle, as shown in Figure 2-4. Imagine you have a system that needs to decide which company should be used for shipping a particular order. This could be based on a number of different criteria; however, in this example it is simply based on the postcode.

Figure 2.4: Order and OrderShipper class model

In this first example, you have an OrderShipping. The special casing of the terms already clues the reader in to the fact that they are methods and objects with a method called ShipOrder. Based on the Order object, this method decides which company is the most appropriate and starts processing the shipment:

        public void ShipOrder(Order o)

        {

            int i;

            if(o.PostCode.StartsWith("A"))

            {

                //Do processing for CompanyA

            }

            else if (o.PostCode.StartsWith("B") || int.TryParse(o.PostCode, out i))

            {

                //Do processing for CompanyB

            }

            else if (o.PostCode.StartsWith("C"))

            {

                //Do processing for CompanyC

            }

        }

This method has a number of problems. First, it’s breaking the first principle of SRP, but more importantly it is an expensive design to work with when you want to make changes. If you wanted to add a new shipper to the list, you would have to add another additional if statement to the list. If one of your existing shippers changed the rules, then you would need to go back and modify this single method. Imagine if the system grew from three shippers to a thousand shippers. This single method will soon become uncontrollable, not maintainable, and as a result a lot more likely to contain bugs.

Here’s the same scenario, but following the openclose principle instead.

In Figure 2-5 you can see that the shipping process has been decoupled. Every company now has its own object within the system, associated together by a common interface. If a new shipper needs to be added, you can simply create a new object and inherit from this interface.

Figure 2-5: Decoupled class model

Each of the objects implements two methods: one is based on whether they can ship the order, and the other is based on processing the actual order. The CanHandle method takes the logic from the previous example and has extracted it into its own class and method:

public bool CanHandle(Order o)

{

   if (o.PostCode.StartsWith("A"))

     return true;

 

     return false;

}

Also, Figure 2-5 introduces a ShipmentController. This simply asks all the shippers if they can ship the order; if they can, the controller then calls Process and stops:

public void ShipOrder(Order o)

{

   foreach (var company in shipmentCompanies)

   {

      if (company.CanHandle(o))

      {

         company.Process(o);

         break;

      }

   }

}

The resulting code is not only more readable and maintainable—it’s also more testable. The logic for each company can now be tested in isolation to ensure it meets the correct requirements, and you can test that the ShipmentController is correctly calling the items by using a stub IShipper object. Stubs is a concept that we’ll cover later in the Stubs and Mocks section. For now, simply by following this principle, you have made your tests easier to write and maintain and have improved the quality of the code.

Although you have improved the code, there are still problems. If you add a new shipper, you’ll need to inform the ShipmentController about the new shipper. This is far from being a disaster, because you could implement support for automatically identifying all the shippers within the system. But you need to weigh this option against the cost of implementation, testing, and maintenance to decide if this would be worth the effort, compared to manually editing the constructor of ShipmentController.

L—The Liskov Substitution Principle

“Classes must be able to use objects of derived classes without knowing it.”

Robert Martin (Uncle Bob) http://www.objectmentor.com/resources/articles/lsp.pdf

The Liskov Substitution Principle is based on the concept that all implementations should program against the abstract class and should be able to accept any subclass without side effects. Classes should never be aware of any subclass or concrete implementation, instead it should only be concerned with the interface. The aim here is that you can replace the functionality with a different implementation without modifying the code base.

The example in Figure 2-6 violates the principle. You have a simple string formatter and all the formatters inherit from IFormatter. As such, you should be able to use any formatter in exactly the same fashion.

Figure 2-6: Class Diagram of Principle

However, the Format method for each class handles empty strings in a slightly different way. For example, the uppercase formatter is shown next:

class UpperCaseFormatter: IFormatter

{

   public string Format(string s)

   {

      if(String.IsNullOrEmpty(s))

         throw new ArgumentNullException("s");

 

      return s.ToUpper();

   }

}

Although many might consider throwing an exception to be bad practice, in the right scenario this could be acceptable. However, for the lowercase formatter in the same scenario it would return null:

class LowerCaseFormatter: IFormatter

{

   public string Format(string s)

   {

      if (String.IsNullOrEmpty(s))

         return null;

 

      return s.ToLower();

   }

}

This breaks the principle. The expectation of how you expect the implementations to work is different. Imagine if you had a large number of different implementations, each behaving slightly differently: your implemented code would be chaos. Instead, you want a consistent view of how the code should work with each implementation being constant. For example, it should return String.Empty:

if (String.IsNullOrEmpty(s))

   return String.Empty;

Another common cause for breaking this principle is when implementers demand more information than stated by the method signature or interface. In the following class, the correct interface has been implemented, but a new public property has also been added for total width:

    class PaddingStringFormatter: IFormatter

    {

        public int TotalWidth { get; set; }

        public string Format(string s)

        {

            return s.PadLeft(TotalWidth, ‘_’);

        }

    }

When a developer comes along who wants to use the previous class, they need to cast the formatter argument down to the concrete instance. I’m sure you’ve encountered code similar to this before:

     class Implementor

    {

        public void OutputHelloWorld(IFormatter formatter)

        {

            string s = "Hello World";

 

            PaddingStringFormatter padder = formatter as PaddingStringFormatter;

            if(padder != null)

            {

                padder.TotalWidth = 20;

                s = padder.Format(s);

            }

 

            Console.WriteLine(s);

        }

    }

Ideally, you want to move away from code such as this, because it causes a lot more coupling to occur. When it comes to testing, it is also much more difficult, because you need to be aware of the internals of a method and how this affects execution.

The Liskov substitution principle was introduced by Barbara Liskov during a 1987 keynote titled “Data Abstraction and Hierarchy.” In 1968, Liskov was the first woman in the United States to be awarded a Ph.D. from a computer science department (Stanford University). More recently, Liskov was awarded with the 2008 Turning Award for her contributions to practical and theoretical foundations of programming languages and system design.

I—Interface Segregation Principle

“Clients should not be focused to depend upon interfaces that they do not use.”

Robert Martin (Uncle Bob) http://www.objectmentor.com/resources/articles/isp.pdf

This principle states that interfaces should have high cohesion to allow clients to keep their implementation as minimal as possible. Simply put, clients should not be forced to depend upon interfaces that they do not use.

This principle is often broken without people realizing about the possible implications. For example, it’s often broken in parts of the .NET framework, particularly ASP.NET. The main cause of this principle being broken is when a particular interface is too large with too many unrelated methods. As a result of implementing a simple interface, the concrete implementation must implement nonrequired methods to get the desired effect.

The result of heavy interfaces is a large amount of unwanted boiler plate code, making your objects move heavyweight and memory intensive. The interfaces also tend to be more confusing making it more difficult to know which methods you use.

In an ideal scenario, your interfaces should be lightweight and simple to implement allowing for an accurate description of what the class actually does. As with before, let’s start with a simple example that breaks this principle, as shown in Figure 2-7.

Imagine you have extended your shipment options.

Figure 2-7: Two implementations of the IShipper interface

Next you can see the ability to add some personalization to an item:

interface IShipper

{

      bool CanHandle(Order o);

      void Process(Order o);

      void AddGiftWrapping(Order o);

      void HandDeliver(Order o);

      void AddSpecialMessage(Order o, string message);

}

The previous listing looks similar to a sensible interface. It defines what a shipper might have to do. However, if you look at the implementation you will notice the problem. Although your PersonalShipper object is fine, because the object requires all the methods to fulfill all the different options, your GenericShipper object doesn’t offer any customization, but is still required to implement those additional methods. The result is that the code is littered with the following methods, which throw a NotSupportedException exception:

public void HandDeliver(Order o)

{

   throw new NotSupportedException("We do not support this option");

}

As the principle states, your GenericShipper object is dependent upon methods that it does not require. Ideally, you want to separate out the interface into two objects: one for the generic shipper and one for a shipper that supports personalization (as shown in Figure 2-8).

Figure 2-8: Shipper implementations with focused interfaces 

With the two separate interfaces, it becomes easier to see what each shipper actually provides in terms of functionality. You have seen the distinction between PersonalShipper and GenericShipper very clearly from looking at the Classes interface. Although the implementation of PersonalShipper is identical, apart from implementing the new IPersonalisedShipper, your GenericShipper code is much shorter and simpler to maintain.

Although this principle is great from the code point of view, how does it affect testing and testability? As your interfaces are more focused, your client classes are also more focused, and as a result you do not have as much code to test. Before, you would have had to test the nonrequired methods and hence waste time or leave them untested, which affects your code coverage.

D—The Dependency Inversion Principle

“A. High-level modules should not depend upon low- level modules. Both should depend upon abstractions.

B. Abstractions should not depend upon details. Details should depend upon abstractions.”

Robert Martin (Uncle Bob) http://www.objectmentor.com/resources/articles/dip.pdf

When developing your system, if you have considered the other four principles you would have a decoupled and isolated system. However, at some point, all your isolated components will need to talk to each other. In the situation where you have dependencies within your system, you should then use abstractions as a way to manage them. By depending upon the abstractions and not the concrete class, you can interchange implementations when required.

The first term that needs to be defined in more detail is dependency. What is a dependency? In the context of this book, dependency is a low-level class that a higher-level class must use to fulfill its requirements. A simple example is that a text formatter shouldn’t be concerned with how to actually obtain the text; if the formatter could only process text from a file called MyImportantDocument.docx, it would be extremely limiting. The formatter should be flexible enough to cope with many different ways of obtaining the text. By removing this lower-level dependency, your higher-level formatter can be more effectively reused.

The inversion part simply says that the dependency should come from outside and be placed into the class to use. Generally dependencies are created inside other classes and as such are closed. Inversion simply states that the dependency should be created outside the class and injected in.

A common example of this is the business logic and data access problem. You have two separate, isolated parts of the application that need to talk to each other. A common pattern is that the business logic would have created an instance of the DataAccess class and callmethods on it as represented in Figure 2-9.

Figure 2-9: Business Logic with an implicit link to DataAccess

The constructor of the BusinessLogic class would reassemble the following listing:

private DataAccess access;

public BusinessLogic()

{

   access = new DataAccess();

}

A developer using the BusinessLogic class would have no idea how, or even if, the underlying system was accessing data until something went wrong. This common pattern is far from ideal. You want the dependency to be very clear, but also in control of the object which requires the dependency.

An improved example would look similar to Figure 2-10.

Figure 2-10: Dependency has been split and now explicit

Next, a new interface for defining a DataAccess object is included, as this allows you to have different implementations of the data access. In this case, you simply have the Database class. Your BusinessLogic class has a constructor that takes in an IDataAccess object as shown next:

public IDataAccess Access { get; set; }

public BusinessLogic(IDataAccess access)

{

   Access = access;

}

Now, the higher-level business logic does not depend on concrete implementation of the lower-level Data Access class.

From the point of view of an implementer of the BusinessLogic class, the dependency is quite apparent. There is a clear boundary between the two different parts of the system. Sometimes dependencies can hide themselves in unusual ways until they start to cause major problems. This is one example of where TDD can identify your dependencies much earlier in the process and change the way you develop software. As you want your unit tests to be nicely isolated and focused, you will find that if you are greeted with a dependency on a more complex or external part of the system then your tests will become much more difficult to write. When you see this issue in your tests, then you will know that you need to do something about it.

In this scenario, if you abstract and decouple the dependencies the DataAccess layer will no longer cause you problems when testing the BusinessLayer, because your tests will be in control of this external dependency.

However, if you have isolated all of your dependencies, and the user of the object is responsible for constructing the dependencies, then your code is quickly going to be difficult to manage.

Imagine the following scenario—your business logic code has dependencies on your Data Access layer, a user management system, and your shipment system. The Data Access layer also depends on a configuration object. The class diagram would look similar to Figure 2-11.

Figure 2-11: Decoupled system design with multiple dependencies

This is a well-designed, decoupled system. In theory, it should be good. However, if you need to construct a BusinessLogic class, then the code would look like this: 

internal class SimpleUI

{

   public SimpleUI()

   {

      IConfiguration config = new Configuration();

      IDataAccess access = new ComplexDataAccess(config);

      IUserManagement management = new UserManagement();

      IShippingRules shippingRules = new ShippingRules();

 

      MoreComplexBusinessLogic logic = new MoreComplexBusinessLogic(access, management, shippingRules);

   }

}

You can imagine that this would soon become extremely difficult to manage. If you introduced a new dependency, you would have to change a lot of the code. You could introduce a helper method, or you could have a parameter default constructor, which automatically defines the default objects to use:

internal class MoreComplexBusinessLogic

{

   public IDataAccess Access { get; set; }

   public IUserManagement User { get; set; }

   public IShippingRules ShippingRules { get; set; }

 

   public MoreComplexBusinessLogic(IDataAccess access, IUserManagement user, IShippingRules shippingRules)

   {

      Access = access ?? new ComplexDataAccess(new Configuration());

      User = user ?? new UserManagement();

      ShippingRules = shippingRules ?? new ShippingRules();

   }

 

   public MoreComplexBusinessLogic(): this(null, null, null)

   {}

}

Managing dependencies via constructor overloading is bad! This is because you still have all the problems you had before as the concrete dependencies are still being hidden. If you removed or changed the ShippingRules object, then this constructor would break—even if you never used the overloaded version. It also causes confusion as you will never be 100 percent sure of which default implementation is being used and if that is even the correct set of objects to use. The result could be that the system only fails at runtime. The only advantage is that you can construct a new BusinessLogic with a single line of code, and this is still not a good enough reason.

Yet, you do want to be able to easily add new dependencies into a system without having to go back and update all the construction set-up code that you want to be a single line. The answer to this problem is to use an Inversion of Control, IoC, framework to support this scenario.

Inversion of Control is another one of those principles that has been around for years. A Google search will turn up a bunch of IoC frameworks for the .NET platform, but you don’t have to download a large framework that someone else developed to take advantage of IoC. There are many situations where developers cannot use an open source framework. Because IoC is a pattern, don’t be afraid to create your own framework.

As with unit testing frameworks, there are a number of different IoC frameworks, each targeting and solving a particular problem in a particular way, yet the core concept is the same. In the next few examples we will be using a very simplistic 33 line IoC framework created by Ken Egozi.

The first major concept with an IoC has to do with the ways you set up your configuration and the mapping between the interface and concrete implementation. You should simply register all the interfaces using generics. The Register method will keep the mappings in memory, so this method only needs to be called the first time your application is initialized:

    static class IoCRegister

    {

        public static void Configure()

        {

            IoC.Register<IConfiguration, Configuration>();

            IoC.Register<IDataAccess, ComplexDataAccess>();

            IoC.Register<IUserManagement, UserManagement>();

            IoC.Register<IShippingRules, ShippingRules>();

        }

    }

In the UI for this example, the Configure method is called to set up the mappings. You will need to ask the IoC framework to resolve and initialize an instance of the MoreComplexBusinessLogic class. Under the covers, it will resolve all the dependencies for the constructor of each object based on our configuration: 

    internal class SimpleUI

    {

        public SimpleUI()

        {

            IoCRegister.Configure();

 

            MoreComplexBusinessLogic logic = IoC.Resolve<MoreComplexBusinessLogic>();

        }

    }

The result is that the dependency for the ComplexDataAccess object is automatically resolved. If you want to add a new dependency to the constructor, you would simply need to register it in one location for the rest of the system to pick up and use the new object. This makes managing your dependencies very effective and maintainable. 

This article is excerpted from chapter 2 "Design and Testability" of the book Testing ASP.NET Web Applications by by Jeff McWherter and Ben Hall (ISBN: 978-0-470-49664-0, Wrox, 2009, Copyright Wiley Publishing Inc.)

Tags:

Comments

Leave a Reply

What is 12 + 6 ?
Please leave these two fields as-is:
IMPORTANT! To be able to proceed, you need to solve the following simple math (so we know that you are a human) :-)