There are many ways we can introduce new types of objects in a test-driven approach to design. Commonly, developers reference classes that don’t yet exist in their test, and then declare them so they can continue writing the test.
But if you favour back-loading the bulk of your design decisions until after you’ve passed the test, there’s a simple pattern I often use to help me discover objects through refactoring. The advantage of this see-the-code-then-extract-the-objects approach is that we’re more likely to end up with good abstractions, since the design of our objects is based purely on what’s needed to pass the test.
Let’s take a shopping basket example. If I write a test for adding an item to a shopping basket that references no implementation, with all the code contained inside the test:
import static org.junit.Assert.*; | |
import java.util.ArrayList; | |
import java.util.List; | |
import org.junit.Test; | |
public class ShoppingCartTest { | |
private static final int PRICE = 2; | |
private static final int QUANTITY = 3; | |
@Test | |
public void addingItemChangesBasketTotal() { | |
List<Object[]> basket = new ArrayList<>(); | |
String productCode = "P111"; | |
String productDescription = "Widget (Large)"; | |
double price = 10.0; | |
int quantity = 10; | |
Object[] item = new Object[] { | |
productCode , | |
productDescription , | |
price , | |
quantity | |
}; | |
basket.add(item); | |
double total = basket.stream() | |
.mapToDouble(i –> (double)i[PRICE] * (int)i[QUANTITY]) | |
.sum(); | |
assertEquals(100.0, total , 0.0); | |
} | |
} |
This design currently suffers from a code smell called “Primitive Obsession”. There’s just raw, exposed data. And we’d expect this if we hadn’t encapsulated any of the logic inside classes (or closures, if you’re that way inclined.)
The test passes, though. So, as icky as our design might be, it works.
Time to start refactoring. First, let’s isolate the action we’re testing into its own method.
import static org.junit.Assert.*; | |
import java.util.ArrayList; | |
import java.util.List; | |
import org.junit.Test; | |
public class ShoppingCartTest { | |
private static final int PRICE = 2; | |
private static final int QUANTITY = 3; | |
@Test | |
public void addingItemChangesBasketTotal() { | |
List<Object[]> basket = new ArrayList<>(); | |
String productCode = "P111"; | |
String productDescription = "Widget (Large)"; | |
double price = 10.0; | |
int quantity = 10; | |
addItem(basket, productCode, productDescription, price, quantity); | |
double total = basket.stream() | |
.mapToDouble(i –> (double)i[PRICE] * (int)i[QUANTITY]) | |
.sum(); | |
assertEquals(100.0, total , 0.0); | |
} | |
private void addItem(List<Object[]> basket, | |
String productCode, | |
String productDescription, | |
double price, | |
int quantity) { | |
Object[] item = new Object[] { | |
productCode , | |
productDescription , | |
price , | |
quantity | |
}; | |
basket.add(item); | |
} | |
} |
What I’m interested in when I see a stateless method like addItem() is whether there’s an object identity lurking among its parameters – a potential this pointer, if you like. Right now, this method does action->object. To make it OO, we want to flip that around to object.action. I reckon the object in this case – the thing to which an item is being added – is the basket collection. Let’s introduce a parameter object for it.
private void addItem(ShoppingBasket shoppingBasket, | |
String productCode, | |
String productDescription, | |
double price, | |
int quantity) { | |
Object[] item = new Object[] { | |
productCode , | |
productDescription , | |
price , | |
quantity | |
}; | |
shoppingBasket.getBasket().add(item); | |
} |
This method is about adding an item to the shopping basket – as evidenced by the line:
shoppingBasket.getBasket().add(item);
We can eliminate this Feature Envy by moving addItem() to shoppingBasket, making it the target of the invocation.
@Test | |
public void addingItemChangesBasketTotal() { | |
List<Object[]> basket = new ArrayList<>(); | |
String productCode = "P111"; | |
String productDescription = "Widget (Large)"; | |
double price = 10.0; | |
int quantity = 10; | |
ShoppingBasket shoppingBasket = new ShoppingBasket(basket); | |
shoppingBasket.addItem(productCode, productDescription, price, quantity); | |
double total = shoppingBasket.getBasket().stream() | |
.mapToDouble(i –> (double)i[PRICE] * (int)i[QUANTITY]) | |
.sum(); | |
assertEquals(100.0, total , 0.0); | |
} |
Now we can see that the code for calculating the total really belongs in the new ShoppingBasket class, putting that work where the data is. First we extract a total() method.
ShoppingBasket shoppingBasket = new ShoppingBasket(basket); | |
shoppingBasket.addItem(productCode, productDescription, price, quantity); | |
assertEquals(100.0, total(shoppingBasket) , 0.0); |
Then we can move total() to the object of its Feature Envy.
ShoppingBasket shoppingBasket = new ShoppingBasket(basket); | |
shoppingBasket.addItem(productCode, productDescription, price, quantity); | |
assertEquals(100.0, shoppingBasket.total() , 0.0); |
Looking inside the ShoppingBasket class, we see we have a bit of cleaning up to do.
public class ShoppingBasket { | |
private final List<Object[]> basket; | |
public ShoppingBasket(List<Object[]> basket) { | |
this.basket = basket; | |
} | |
public List<Object[]> getBasket() { | |
return basket; | |
} | |
void addItem(String productCode, String productDescription, double price, int quantity) { | |
Object[] item = new Object[] { | |
productCode , | |
productDescription , | |
price , | |
quantity | |
}; | |
getBasket().add(item); | |
} | |
double total() { | |
double total = getBasket().stream() | |
.mapToDouble(i –> (double)i[ShoppingCartTest.PRICE] * (int)i[ShoppingCartTest.QUANTITY]) | |
.sum(); | |
return total; | |
} | |
} |
Let’s properly encapsulate the list of items.
public class ShoppingBasket { | |
private final List<Object[]> basket = new ArrayList<>(); | |
void addItem(String productCode, String productDescription, double price, int quantity) { | |
Object[] item = new Object[] { | |
productCode , | |
productDescription , | |
price , | |
quantity | |
}; | |
basket.add(item); | |
} | |
double total() { | |
double total = basket.stream() | |
.mapToDouble(i –> (double)i[ShoppingCartTest.PRICE] * (int)i[ShoppingCartTest.QUANTITY]) | |
.sum(); | |
return total; | |
} | |
} |
Our test code is much, much simpler now (a sign of improved encapsulation).
@Test | |
public void addingItemChangesBasketTotal() { | |
String productCode = "P111"; | |
String productDescription = "Widget (Large)"; | |
double price = 10.0; | |
int quantity = 10; | |
ShoppingBasket shoppingBasket = new ShoppingBasket(); | |
shoppingBasket.addItem(productCode, productDescription, price, quantity); | |
assertEquals(100.0, shoppingBasket.total() , 0.0); | |
} |
But we’re not done yet. Next, let’s turn our attention to the Long Parameter List code smell in addItem().
void addItem(String productCode, String productDescription, double price, int quantity) { | |
Object[] item = new Object[] { | |
productCode , | |
productDescription , | |
price , | |
quantity | |
}; | |
basket.add(item); | |
} |
We can introduce a parameter object that holds all of the item data.
void addItem(BasketItem basketItem) { | |
Object[] item = new Object[] { | |
basketItem.getProductCode() , | |
basketItem.getProductDescription() , | |
basketItem.getPrice() , | |
basketItem.getQuantity() | |
}; | |
basket.add(item); | |
} |
And now that we have a BasketItem class that holds the item data, we can add that to the list instead of the ugly and not-very-type-safe object array.
public class ShoppingBasket { | |
private final List<BasketItem> basket = new ArrayList<>(); | |
void addItem(BasketItem basketItem) { | |
basket.add(basketItem); | |
} | |
double total() { | |
double total = basket.stream() | |
.mapToDouble(i –> i.getPrice() * i.getQuantity()) | |
.sum(); | |
return total; | |
} | |
} |
Then we clean up the test code a little more, with various inlinings.
@Test | |
public void addingItemChangesBasketTotal() { | |
ShoppingBasket shoppingBasket = new ShoppingBasket(); | |
BasketItem basketItem = new BasketItem("P111", "Widget (Large)", 10.0, 10); | |
shoppingBasket.addItem(basketItem); | |
assertEquals(100.0, shoppingBasket.total() , 0.0); | |
} |
Almost there.
The total() method has Feature Envy for data of BasketItem.
double total() { | |
double total = basket.stream() | |
.mapToDouble(i –> i.getPrice() * i.getQuantity()) | |
.sum(); | |
return total; | |
} |
Let’s fix that.
double total() { | |
double total = basket.stream() | |
.mapToDouble(i –> i.subtotal()) | |
.sum(); | |
return total; | |
} |
This helps us to improve encapsulation in BasketItem.
public class BasketItem { | |
private final String productCode; | |
private final String productDescription; | |
private double price; | |
private int quantity; | |
public BasketItem(String productCode, String productDescription, double price, int quantity) { | |
this.productCode = productCode; | |
this.productDescription = productDescription; | |
this.price = price; | |
this.quantity = quantity; | |
} | |
double subtotal() { | |
return price * quantity; | |
} | |
} |
Two last little notes: firstly, it turns out that for this particular action, product code and product description aren’t needed. I see developers do this often – modeling data based on their understanding of the domain rather than thinking specifically “what data is needed here?” This can lead to redundancy and unnecessary complexity. Until we have a feature that requires it, leave unused data out of the design. Always be led by function, not by data. We might get a requirement later to, say, report how many units of P111 we sold each day. We’d add the product code then.
Let’s fix that.
public class BasketItem { | |
private double price; | |
private int quantity; | |
public BasketItem(double price, int quantity) { | |
this.price = price; | |
this.quantity = quantity; | |
} | |
double subtotal() { | |
return price * quantity; | |
} | |
} |
And finally, I started out with a test fixture for a “Shopping Cart”, but as the design emerged, that concept changed. It’s important to keep your tests – as living documentation for your code – in step with the emerging design, or things will get confusing.
Let’s fix that.
public class ShoppingBasketTest { | |
@Test | |
public void addingItemChangesBasketTotal() { | |
ShoppingBasket shoppingBasket = new ShoppingBasket(); | |
BasketItem basketItem = new BasketItem(10.0, 10); | |
shoppingBasket.addItem(basketItem); | |
assertEquals(100.0, shoppingBasket.total() , 0.0); | |
} | |
} |
Now, you might have come up with this design up front. But then again, you might not. The benefit of this approach to discovering the design is that we start only with what we need, and end with an equivalent version with the code smells removed and nothing we don’t need.
The price is that you spend more time refactoring. But as the model emerges, we tend to find that this overheard decreases, and the emphasis shifts to reusing the design instead of discovering it every time. In other words, it gets easier as we go.