This chapter discusses the relationship between ATDD and Design Patterns. Essentially, ATDD provides us with quality acceptance criteria in the form of test specifications. We can use these to design our code from a behavior point of view instead of from an implementation point of view.
Doing this makes for more testable code, a quality highly correlated with other code qualities we want – strong cohesion, loose coupling and no redundancy. Design patters provide us with a method for combining these high quality objects together through the Gang of Four’s mantra’s of designing to behavior, using variation and encapsulating variation – in this case variation of type.
In the Agile world where requirements evolve, ATDD and Design Patterns Thinking work together to enable emerging designs from emerging requirements.
ATDD is the practice of having the customer, developer and tester (the last two possibly being the same person) create acceptance criteria in the form of tests together. Doing so has several advantages. First of all, it combines three different mentalities of testing:
- The customer – just figures it’s going to work
- The developer – it’s working if a case he’s trying to get to work works
- The tester – it’s not working until s/he can’t get it to fail
These different perspectives are somewhat natural given each of their roles. We don’t need to change their thought process here. When these roles work together a nice collective behavior is automatically created.
In addition to this, of course, they have different perspectives on the requirements. Customers tend to think from the perspective of how they’d like to behave. Developers think from the way the system is implemented and testers can lie anywhere in between. Having conversations among all three skills results in greater clarity of the true requirements and how you can validate that they are being met.
But the creation of the specification of how to validate the implementation of the requirement is only the beginning of the value of ATDD. Using these specifications can provide a different method of doing design – one that is highly correlated with design patterns. Before exploring that connection let’s look at a common way of designing software and then see how ATDD suggests a shift that matches the way patterns suggest.
Contrasting Normal Design with ATDD Design
A Common Design Approach
A common design approach is to take our requirements and consider an implementation that will meet them. We sketch out the functions needed to manifest the requirement. We then start implementing them. If we’re in an object-oriented language we might define our classes based on the nouns in our internal conversation. We put methods on our classes based on the actions these nouns take (that is, the verbs in our mental dialog). We try to follow good object-oriented practices to make our code be loosely coupled and strongly cohesive. We’ll try to avoid redundancy by putting anything needed more than once into a function on its own. Stop for a second and see if this is how you do it.
Designing from Acceptance Tests
Now, if we’ve created acceptance tests using ATDD, we might do something different. ATDD would suggest that we design the system based on the acceptance criteria. Consider what the test specifications we got from ATDD provide us. To be complete test criteria would need to specify:
- The inputs to the system
- The outputs we expect from the system
- Any behavior and state changes the system does (e.g., turning something on, storing something in a database)
We can start designing not from an implementation, or algorithmic perspective but from a behavior perspective. In other words, we start with the systems’ behavior and then, we decompose our system into sub-parts, each based on the behavior needed. We’re essentially decomposing our system into sub-behaviors and considering the test specifications as we do so.
Connecting ATDD Design With Design Patterns
Let’s look at patterns before connecting them with ATDD. Christopher Alexander, in his iconic Timeless Way of Building (which inspired the software patterns community) called patterns “solutions to recurring problems in a context.” But, towards the end of the book he also said “At this final stage, the patterns are no longer important: the patterns have taught you to be receptive to what is real.” In other words, patterns are to design the way recipes are to great cooking.
This new way of design was concisely (and, unfortunately, somewhat obscurely) espoused in their iconic Design Patterns: Elements of Reusable Object-Oriented Software.
The Gang of Four presents three pillars of design.
- Design to the behavior of an object
- Favor delegation over inheritance. An extension of this is to prefer delegation of non-cohesive behavior as well.
- Find what varies and encapsulate it. This can be in a subclass (such as the Template Pattern does) or through delegation (as the Strategy pattern does).
When I was studying patterns I noticed that following this advice created designs where the objects had, what I call high ‘testability’. Testability is the ease with which objects can be tested (not the amount of test coverage achieved). Testability is strongly correlated with other code qualities. It is difficult to test objects when one has poorly designed classes (weak cohesion, tight coupling, redundancy). Conversely, objects which have high testability have strong cohesion, loose coupling and little if any redundancy. As a result of this, if one designs in a way to increase the testability of one’s objects, good design will be achieved. This is one of the reasons test-driven development is so effective. By creating our unit tests before writing our code the design of our code is improved.
So now we have it. ATDD has us become clear about the behavior of our objects. These behaviors are defined in terms of test specifications. This enables us to define our objects from the perspective of these behaviors. Using the Gang of Four’s mantras, we can decompose our system into testable objects that are connected via delegation. Dependencies are lowered by encapsulating different implementations.
Emerging requirements can be accommodated by having an emerging architecture. This requires the use of emergent design. See the “Technical Agility” on Part IV: Additional Resources on the Net Objectives Portal.
Why This Connection Is Important
Most developers spend more time trying to understand how a system works, looking for bugs and a combination of both. When designed from an implementation it is all too easy to get very specific about which object is using which object. This results in highly coupled, redundant logic. When we design from behavior and connect our objects in the manner suggested by the Gang of Four, our code is both more understandable and more loosely coupled. This enables changes more quickly and robustly. ATDD provides us the behavior from which to design in the form of acceptance criteria.