In our 2004 OOPSLA paper, Mock Roles not Objects, Steve, Joe, Tim and I described how we used Mock Objects and TDD to guide the design of object-oriented software. Briefly, we described the process as:
Unfortunately we neglected to describe a vital part of the process: refactoring. As the system grows we look out for interfaces that define similar ways in which objects collaborate — common patterns of communication between objects. We then collapse different interfaces that are incompatible but semantically equivalent into the same type.
We took for granted that when programmers refactor a system they apply as much refactoring effort to the interfaces between the objects as they do to the classes of the objects themselves. However, I have found that this is not the case. For example, Martin Fowler's canonical book of Refactoring patterns does not contain any patterns about refactoring interfaces or the communication protocols between objects.
If you follow the interface discovery process without refactoring the interfaces you discover, you end up with a system containing lots of interfaces, many of which represent very similar concepts in incompatible ways. Objects that should be plug-compatible cannot communicate without lots of awkward little adapter classes. As a result, I have found that teams develop a negative reaction to interfaces or even object-oriented design and end up with a design that is difficult to change because its classes are statically coupled.
When you refactor interfaces into a set of common communication patterns, objects in the system become much more "pluggable". You can then change the behaviour of the system by changing the composition of its objects—adding and removing instances, and plugging different combinations together—rather than writing procedural code. The code that composes objects acts a declarative definition of the how entire system will behave. You therefore end up working at a much greater level of abstraction and can focus on what you want the system to do, and not how that is implemented.
Obviously you need to strike a balance. If you end up with interfaces like the following, you've gone too far!
public interface Thing {
Object doSomething(Object arg) throws Exception;
}
Two refactoring steps I often apply are:
Steve and I have been working on a book for the last few months entitled Growing Object-Oriented Software, Guided by Tests. We're now at the stage where we can put content online to garner feedback. We'll post a chapter every week or so. Eventually, the book will appear as a Rough Cut on Safari.
If you'd like to read and comment, please join the Yahoo group we've set up for discussion about the content. We'd love to hear what you think.
]]>![]() |
XP Day is looking for experience reports, tutorials and structured workshops to run in a track alongside the more freeform lightning talks and open space that will make up the rest of the conference. This year the focus of the conference will advancing the state of the art, rather than introducing Agile or Scrum or XP or TDD. |
![]() |
SPA 2009 is looking for session proposals which are interactive and leading edge, possibly even experimental. They can be about technology or teams, practice or process - in fact anything to do with improving software development. |
I think both conferences are excellent places for first-time presenters to run a session. New presenters will get a lot of help from experienced presenters as they go through the shepherding process. And this year, SPA is offering a free place to the best proposal from a new presenter.
]]>
Here's yet more blather about exception handling. The last one, I promise! (For now...)
My last post describes how I like to coordinate exception handling inside the components of a distributed system. A component handles the failure of other remote services that it uses — database servers, for example — by sending back appropriate error responses or rolling back distributed transactions. That's all well and good when a component runs within an application server, because the server handles all the messy details of fail-over and reconnection for you. But what about "main apps": plain old Java programs that run from a main method?
It can take a lot of code to correctly handle connection failure and reconnection, exponentially back-off connection attempts, clean up long-lived objects that hold onto connections, and hide all the messy technical details away from the business logic behind domain-term interfaces. It's also hard to get all the corner cases right.
It's much easier to just not bother with reconnection at all.
When a main app catches an EnvironmentException it should roll back transactions, send back response codes, or whatever it needs to do and then, instead of trying to reconnect, just die. Launch the app from a supervisor process that restarts it whenever it dies. The Java Service Wrapper does the job very nicely.
Now you don't need to write any reconnection logic at all. The application's start-up code is enough.
This greatly simplifies writing distributed Java apps. And a simple system is more reliable, easier to secure and easier to change.
]]>
I'm on a bit of a roll when it comes to exception handling tips, so here's another technique that's worked well in the last few systems I've had a hand in, this time for coordinating exception handling within a component of a distributed system.
A component in a distributed system receives requests from remote components and reacts by making requests of its own to remote components in its environment that it depends on, before sending back a response. However, because its clients and dependencies are out of its control, it cannot guarantee that the requests it receives are correct or that the services it depends on are available when it must service a request. By "request" I mean either a client-server style request/response interaction or an asynchronous event received from a message broker.
In Java terms, because both bad requests and failed dependencies are out of the component's control, they should be reported by throwing checked exceptions. However, the way they should be handled is significantly different. A bad request should never be retried, but could be logged for manual repair and replay if that makes sense for the system. The failure of a dependency is (hopefully) temporary, and so the request can be retried later.
Apart from RMI, Java frameworks for writing distributed systems don't make this distinction in the exceptions they throw or handle. Therefore, when building distributed systems, among the first things I write are two base exception classes: BadRequestException and EnvironmentException. Depending on the communication protocol, the application will handle these in different ways:
| Bad Request Exception | Environment Exception | |
|---|---|---|
| HTTP (e.g. Servlets) | Return a 4xx response code | Return a 5xx response code |
| JMS (e.g. Message Driven Beans) | Move message to a hospital queue and commit the transaction. | Roll back the transaction, leaving the message on the input queue for later redelivery |
Because frameworks don't make the distinction between Bad Requests and Environment Exceptions, I keep the framework code — servlet or MDB class, for example — as thin as possible, doing little more than delegating the request to an interface that throws BadRequestException or EnvironmentException and handling each kind of error as appropriate.
The client-side code of an synchronous remote call needs to translate the status it receives appropriately. If the returned status indicates that the client made a bad request (e.g. an HTTP 4xx code), it should throw a RuntimeException to indicate that a programming error has been detected. If the status indicates an environment exception (e.g. an HTTP 5xx code), it should throw a checked exception so the compiler ensures that the exception is handled. I usually wrap that logic up in a convenient proxy object.
Photo by Ian Hughes used under the Creative Commons Attribution license.
]]>
While I'm on the subject of exceptions, let me share another exception idiom I've used a lot on recent projects... the Defect exception.
The Java compiler is very strict. It will complain about missing code paths that you, the programmer, know should never be followed at runtime. This is especially common when using APIs that throw checked exceptions to process data that will always be correct unless the programmer has screwed up..
For example, loading and parsing template from a resource that is compiled into the application should never fail, but throws a checked IOException.
Template template;
try {
template = new Template(getClass().getResource("data-that-is-compiled-into-the-app.xml"));
}
catch (IOException e) {
// should never happen
}
...If an IOException is caught, something is seriously wrong with the application itself, because of something the programmer has done. The application has been built incorrectly, perhaps, or the template is syntactically incorrect. If the application continues running it will fail with confusing errors later on. The error will be difficult to diagnose.
So, the catch block must throw another exception. It can't throw a checked exception. Because it has caught a programming error, it should throw some kind of RuntimeException. RuntimeException itself is a bit vague, and there isn't a subclass of RuntimeException that really fits the bill for this situation. Therefore, I like to define a new exception type to report programmer errors. I first called it StupidProgrammerException, but now, as suggested by Romilly, I call it by the less confrontational name of Defect:
public class Defect extends RuntimeException {
public Defect(String message) {
super(message);
}
public Defect(String message, Throwable cause) {
super(message, cause);
}
}When the compiler asks me to write code paths that should never happen, I throw a Defect. For example:
Template template;
try {
template = new Template(getClass().getResource("data-that-is-compiled-into-the-app.xml"));
}
catch (IOException e) {
throw new Defect("could not load template", e);
}
...
Image by Jonas B, distributed under the Creative Commons Attribution license.]]>
A seemingly little known fact about Java generics is that you can write
generic throws declarations by declaring a type parameter that
extends Exception. For example, the following interface defines a
generic finder that looks up a value of type T, returns null if
it is not found,or may report that the lookup failed completely by
throwing an exception of type X:
public interface Finder<T, X extends Exception> {
@Nullable
T find(String criteria) throws X;
}
Different implementations can fail in different ways. A finder that performed an HTTP query can fail with an IOException. A finder that queries a database can fail with a SQLException. And so on.
But what about queries that cannot fail? You might want to implement
the query in-memory, with a HashMap or something. You don't want to
declare that find throws a checked exception. Therefore bind X
to RuntimeException. The compiler will ignore the throws
clause and you don't even need to include it in implementing classes:
public class InMemoryFinder<T> extend Finder<T, RuntimeException> {
private final Map<String,T> entries;
public InMemoryFinder(Map<String,T> entries) {
this.entries = entries;
}
@Nullable
T find(String criteria) { // No need for a throws clause
return entries.get(criteria);
}
}
Generic throws can remove a lot of boilerplate code to pass checked exceptions through interfaces by wrapping them in more abstract checked exceptions. Hopefully one day Java will provide anchored exceptions so we can avoid all this generics jiggery-pokery.
Wrapped exceptions are still necessary when an interface must be used
polymorphically. A type that declares generic throws is generic but
not polymorphic: an interface T<IOException>
cannot be used wherever a T<SQLException> is
acceptable.
If necessary, an interface with generic throws can be converted by an Adaptor into a version that throws wrapped exceptions and can be used polymorphically.
public class PolymorphicFinder<T,X> extend Finder<T, FinderException> {
private final Finder<T,X> implementation;
public PolymorphicFinder(Finder<T,X> implementation) {
this.implementation = implementation;
}
@Nullable
T find(String criteria) throws FinderException {
try {
return implementation.find(criteria);
}
catch (X x) {
throw new FinderException("query failed", x);
}
}
}
Imagine if the Iterable and Iterator
interfaces were parameterised by exception type:
public interface Iterator<T, X extends Exception> {
boolean hasNext() throws X;
T next() throws X;
}
public interface Iterable<T, X extends Exception> {
Iterator<T,X> iterator();
}
Collections could implement
Iterable<T,RuntimeException> and appear no different
from the way they are now. However, I/O streams, SQL result sets and other
streams of data read from the program's environment could be represented
as Iterable objects and the for-each loop could be used to iterate over
their contents.
For example, BufferedReader could implement
Iterable<String,IOException>, which would let you
write:
try {
BufferedReader reader = new BufferedReader(...);
for (String line : reader) {
... do something with the line
}
}
catch (IOException e) {
... iteration failed
}
Unfortunately, it's probably too late to make this change because it would break backward compatibility.
Photo by Wildcat Dunny, distributed under a Creative Commons license.
]]>An API is a user interface for programmers. That means you have to develop it like a user-interface: iteratively, adapting to how the users actually use and misuse the interface in real life. In an open-source project we can't afford to run usability experiments, hiding behind one-way mirrors observing how users work with our software. Instead we have to react to explicit user feedback, bug reports and feature requests on our project's mailing lists, forums and issue trackers. Because programmers are passive aggressive and love to moan about things, they'll post negative feedback on their blogs instead of discussing it on the mailing lists. Google Blog Search is an excellent tool for finding out what programmers really think about your API.
But... programmers focus on solutions, not goals, so their feature requests will be worded in terms of how they think they would change the API to support whatever it is they want to do. You have to engage in a lot of arduous back-and-forth to find out what they are trying to achieve, rather than what they think they would change in your code. Often you (and they) discover that their goal can be achieved with the current API but they haven't noticed because of their solution myopia. If it turns out your API does not do what they want, you will be better placed to work out the correct way of extending the API or push back on the request because it would violate the API's conceptual integrity.
An API encapsulates a conceptual model for representing and solving problems in the API's application domain. It guides programmers towards what the API designer considers to be the "right" way of doing things and away from the "wrong" way.
But... although programmers know there are no silver bullets, they love their golden hammers! They've learned your API and now they want to apply it to problems that are unrelated or only vaguely related. They'll submit bug reports, feature requests and patches aplenty as they encounter difficulties. To steal a colleague's joke: "if all you have is a hammer, everything looks like a thumb". You have to be willing to push back on requests that would violate the conceptual integrity of your API, otherwise it turns into an inconsistent mess that cannot communicate any useful understanding about the problem domain. It seems a strange idea to some programmers, but just because they use your API doesn't mean that they cannot not use another API at the same time.
A clear and consistent system of names help people understand the conceptual model. For an API, the names should be chosen to make the calling code as readable as possible, even if it makes the implementation of the API less readable. It's really useful to discuss naming ideas on the mailing lists before adding new features to find out if what you think is easy to understand actually confuses people.
But... people often don't take notice of names or understand their meaning, especially when they speak a different language from the API implementors. I wrote an example class in Java that was called UnsafeHackConcreteClassImposteriser. I'd have thought that the phrase "unsafe hack" in the name was enough to warn programmers away from using it as anything more than an example, but apparently not. Someone still used it in their project and complained that it didn't work exactly as they wanted.
Users will always want to adapt your (object-oriented) API to their needs. If you don't define clear plug-in points they will adapt your API by inheriting your classes and overriding internal methods. That is brittle: it will increase your support overhead or reduce your ability to change the code. Define plug-in interfaces to be focused on one thing, so that they remain stable. For example, the Hamcrest Matcher interface has only two methods and has not significantly changed since it was first defined over 8 years ago. To ensure that code that uses the API is as readable as possible, plug-in points should be seamless: code that plugs user defined objects into the API should not look different than code that uses the built-in objects.
But... if you provide extension points, programmers will want you to maintain their extensions for them. Your users will contribute loads obscure extensions that make sense in their projects but don't have a wide applicability. If you adopt every contributed extension you will end up with an enormous maintenance overhead. Therefore, find out which are the most popular plug-in points and keenest users. Spin off libraries of extensions as external projects and get your keen users to maintain them. E.g. the Hamcrest matchers used to be part of the jMock project but now they're maintained by Neil and Joe. Thanks chaps!
Error messages are part of the API. Take notice of the errors that confuse users the most (using the mailing lists, issue tracker and blog searches) and improve error messages to reduce confusion. This will reduce the support load in the long term.
But... there is a tension between error diagnostics and generalisation. The more generic and pattern-tastic is your implementation, the harder it is to determine what the user was trying to do that caused an error, and so the harder it is to generate error messages that make sense to the user. Sometimes you have to sacrifice generality for better error messages.
To save time, make documentation extensible so that you can grow it piecemeal without it obviously being under construction. It's easiest to generate reference documentation with Javadoc.
But... programmers don't always want reference documentation. Javadocs are not useful for people trying to learn an API. Programmers want canned solutions and concrete examples that they can copy into their project and adapt to their needs. The best form of documentation for both you the implementor and your users is HOWTO documents, cookbook recipes and FAQs. The more flexible and extensible the API, the more useful recipes and HOWTOs are because applying the API may require some lateral thinking to understand how the features of the API are used to solve problems.
]]>
I have noticed teams who are new to TDD reach for ever more sophisticated tools to help them test their code rather than refactor their code to make it more testable. By doing so, they lose one of the major benefits of TDD: the feedback it gives on internal design quality.
Code is difficult to test when classes are tightly coupled, are entangled by hidden dependencies like static methods or singletons, or don't clearly distinguish between internal implementation details and peer objects (dependencies, policies or notifications). To unit-test classes like this, it can be tempting to use tools that use clever class loader tricks and bytecode manipulation [1, 2] to break the tight dependencies. But by using tools like this you lose one of the major benefits of unit-testing in the TDD process: rapid feedback on the internal quality of the software.
TDD is not merely about testing — that is, about verifying the external quality of the software: whether it is reliable enough, accurate enough, fast enough, etc. It also gives you feedback about its internal quality: how well designed the code is, such as how loosely coupled and cohesive the classes are, whether dependencies are explicit or hidden, whether information hiding is being used appropriately. And that translates into feedback about how maintainable the code is: how easy it is to extend, change or correct its behaviour over its lifetime.
If you use unit-testing tools that let you side-step poor dependency management in the design, you lose this valuable source of feedback and, when you find that you do need to address these design issues because you have to modify the production code, it will be much harder to do so. The poor structure will have influenced the design of other parts of the system that rely upon it. The programmers responsible for the change will not understand the code as well as those who wrote it (even if they are the same people). It's far easier to nip these design issues in the bud as you discover them than let them remain to affect the design of the rest of the code.
I therefore use a simple rule of thumb when choosing technologies to help me write unit tests:
Break dependencies in unit tests only with techniques that would be acceptable in the production code.
Would I use classloader magic and bytecode manipulation to set dependencies in production code? No. Would I use reflection to modify private fields in production code? No. Would I refactor to introduce an interface between objects? Yes. Would I refactor an object to get a dependency through its constructor instead of from a global variable? Yes.
Following this rule of thumb ensures that code is always easy to change. There are no nasty surprises late in the project when new functionality requires massive changes to the design and developers are pressured to hack the change in because the estimated costs of doing it the right way are too high.
]]>
Test Data Builders remove a lot of duplication from test code, but there can often still be duplicated logic at the point at which the built objects are used. Many different tests will have very similar code that creates an object using a builder and then passes it to the code under test. We can address this duplication by factoring out test scaffolding that works with builders, not system objects. Doing so produces a higher level testing API that more clearly communicates the intent of the test and hides away unimportant details of how the system is being tested.
For example, consider a system to process orders. Orders are sent into our system and processed asynchronously. To perform an end-to-end system test, the test must must create an order, send the order to our system and track the processing of the order by waiting for correlated events to appear on the system's monitoring topic and driving the client through its user interface. That would look something like the following (where the requestSender and progressMonitor do lots of behind the scenes magic with JMS connections, sessions, message producers and consumers, message properties and correlation IDs).
@Test public void reportsTotalSalesOfOrderedProducts() {
Order order1 = anOrder()
.withLine("Deerstalker Hat", 1)
.withLine("Tweed Cape", 1)
.withCustomersReference(1234)
.build();
requestSender.send(order1);
progressMonitor.waitForConfirmation(order1);
progressMonitor.waitForCompletion(order1);
Order order2 = anOrder()
.withLine("Deerstalker Hat", 1)
.withCustomersReference(5678)
.build();
requestSender.send(order2);
progressMonitor.waitForConfirmation(order2);
progressMonitor.waitForCompletion(order2);
TotalSalesReport report = gui.openSalesReport();
report.displaysTotalSalesFor("Deerstalker Hat", equalTo(2));
report.displaysTotalSalesFor("Tweed Cape", equalTo(1));
}
It is tempting pull this duplication into a "helper" method that builds and uses an object. For example:
@Test public void reportsTotalSalesOfOrderedProducts() {
submitOrderFor("Deerstalker Hat", "Tweed Cape");
submitOrderFor("Deerstalker Hat");
TotalSalesReport report = gui.openSalesReport();
report.displaysTotalSalesFor("Deerstalker Hat", equalTo(2));
report.displaysTotalSalesFor("Tweed Cape", equalTo(1));
}
void submitOrderFor(String ... products) {
OrderBuilder orderBuilder = anOrder()
.withCustomersReference(customersReference++);
for (String product : products) {
orderBuilder = orderBuilder.withLine(product, 1);
}
Order order = orderBuilder.build();
requestSender.send(order);
progressMonitor.waitForConfirmation(order);
progressMonitor.waitForCompletion(order);
}
private int customersReference = 1;
However, this refactoring leaves us with the same difficulties that we encountered with the Object Mother when we have to vary data in different tests. We will need to submit orders with different properties and submit different kinds of events — orders, order amendments, order cancellations, etc. The helper method has the very same problems we found with the Object Mother, and that we avoided by using builders to create our test data.
void submitOrderFor(String ... products) { ... }
void submitOrderFor(String product, int count) { ... }
void submitOrderFor(String product, int count, String otherProduct, int otherCount) { ... }
void submitOrderFor(String product, double discount) { ... }
void submitOrderFor(String product, String giftVoucherCode) { ... }
... etc ...
Instead, we can pass an order builder to the method that sends an order into the system, just as we do when combining builders. That method can add properties through the builder before building the order sending it into the system.
@Test public void reportsTotalSalesOfOrderedProducts() {
sendAndProcess(anOrder()
.withLine("Deerstalker Hat", 1)
.withLine("Tweed Cape", 1));
sendAndProcess(anOrder()
.withLine("Deerstalker Hat", 1));
TotalSalesReport report = gui.openSalesReport();
report.displaysTotalSalesFor("Deerstalker Hat", equalTo(2));
report.displaysTotalSalesFor("Tweed Cape", equalTo(1));
}
void sendAndProcess(OrderBuilder orderDetails) {
Order order = orderDetails
.withDefaultCustomersReference(customersReference++)
.build();
requestSender.send(order);
progressMonitor.waitForConfirmation(order);
progressMonitor.waitForCompletion(order);
}
private int customersReference = 1;
Finally, a bit of judicious renaming can change the language of the test so that it communicates more about what behaviour is being tested than how the system implements that behaviour.
@Test public void reportsTotalSalesOfOrderedProducts() {
havingReceived(anOrder()
.withLine("Deerstalker Hat", 1)
.withLine("Tweed Cape", 1));
havingReceived(anOrder()
.withLine("Deerstalker Hat", 1));
TotalSalesReport report = gui.openSalesReport();
report.displaysTotalSalesFor("Deerstalker Hat", equalTo(2));
report.displaysTotalSalesFor("Tweed Cape", equalTo(1));
}
@Test public void takesAmendmentsIntoAccountWhenCalculatingTotalSales() {
Customer theCustomer = aCustomer().build();
havingReceived(anOrder().from(theCustomer)
.withCustomerReference(10)
.withLine("Deerstalker Hat", 1)
.withLine("Tweed Cape", 1));
havingReceived(anOrderAmendment().from(theCustomer)
.withCustomerReference(10)
.withLine("Deerstalker Hat", 2));
TotalSalesReport report = gui.openSalesReport();
report.displaysTotalSalesFor("Deerstalker Hat", equalTo(2));
report.displaysTotalSalesFor("Tweed Cape", equalTo(1));
}
Test Data Builders are a foundation upon which we can define higher-level testing APIs that better communicates the intent of our tests in a language that is closer to that used by non-technical project stakeholders and so greatly help communication within the project.
Update: Thanks to David Peterson and Michael Hunger for helpful feedback. I've fixed typos in the test code and improved the test names. Hopefully the code is easier to follow now.
]]>
Tests that use Test Data Builders can be made less noisy by combining builders. This still leaves some noise in the test: the test code overly emphasises how the tests are building objects at the expense of what they are building. A future reader of the test will be far more interested in what objects are being used than in the way that those objects are constructed.
We can de-emphasise the builders further by instantiating them in clearly named factory methods:
Order order =
anOrder().fromCustomer(
aCustomer().withAddress(
anAddress().withNoPostcode())).build();
When we do this, the naming convention we've used for builder methods up to now gets in the way instead of making things clearer. The builder code looks better if we rename the methods to reflect the relationship between objects only, and not include the type of object at the far end of the relationship:
Order order =
anOrder().from(aCustomer().with(anAddress().withNoPostcode())).build();
This relies on Java's method overloading and so only works for properties that have unique, user-defined types. Longer method names are necessary for primitive types, or if the built object has different relationships with the same type of object. For example, most of the fields of an Address are Strings, and so the builder methods must be explicitly named after the field. However, the post code is strongly typed and so can be passed to an overloaded method:
Address aLongerAddress = anAddress()
.withStreet("222b Baker Street")
.withCity("London")
.with(postCode("NW1", "3RX"))
.build();
]]>
If an object built with a Test Data Builder contains other objects built with other Test Data Builders, you can pass one builder to another to save keystrokes and reduce noise, making tests easier to read. For example, instead of:
Invoice invoice = new InvoiceBuilder()
.withRecipient(new RecipientBuilder()
.withAddress(new AddressBuilder()
.withNoPostcode()
.build())
.build())
.build();
You can write:
Invoice invoice = new InvoiceBuilder()
.withRecipient(new RecipientBuilder()
.withAddress(new AddressBuilder()
.withNoPostcode())))
.build();
The result is significantly easier to read.
]]>
Using separate Test Data Builders to construct objects with common state leads to duplication and can make the test code harder to read and maintain. For example:
Invoice invoiceWith10PercentDiscount = new InvoiceBuilder()
.withLine("Deerstalker Hat", new PoundsShillingsPence(0, 3, 10))
.withLine("Tweed Cape", new PoundsShillingsPence(0, 4, 12))
.withDiscount(0.10)
.build();
Invoice invoiceWith25PercentDiscount = new InvoiceBuilder()
.withLine("Deerstalker Hat", new PoundsShillingsPence(0, 3, 10))
.withLine("Tweed Cape", new PoundsShillingsPence(0, 4, 12))
.withDiscount(0.25)
.build();
Instead, you can initialise a single builder with the common state and then repeatedly call its build method after defining values that apply only to the built objects:
InvoiceBuilder products = new InvoiceBuilder()
.withLine("Deerstalker Hat", new PoundsShillingsPence(0, 3, 10))
.withLine("Tweed Cape", new PoundsShillingsPence(0, 4, 12));
Invoice invoiceWith10PercentDiscount = products
.withDiscount(0.10)
.build();
Invoice invoiceWith25PercentDiscount = products
.withDiscount(0.25)
.build();
This can make tests much easier to read because there is less code and you can give the builder a descriptive name.
However, you have to be careful if the built objects need different fields to be initialised. Because the withXXX methods change the state of the shared builder, objects built later will be created with the same state as those created earlier unless it is explicitly overridden. For example, in the following code, the second invoice has both a discount and a gift voucher, which is not what the code appears to communicate at first glance.
InvoiceBuilder products = new InvoiceBuilder()
.withLine("Deerstalker Hat", new PoundsShillingsPence(0, 3, 10))
.withLine("Tweed Cape", new PoundsShillingsPence(0, 4, 12));
Invoice invoiceWithDiscount = products
.withDiscount(0.10)
.build();
Invoice invoiceWithGiftVoucher = products
.withGiftVoucher("12345")
.build();
A solution is to add a method or copy constructor to the builder that copies state from another builder:
InvoiceBuilder products = new InvoiceBuilder()
.withLine("Deerstalker Hat", new PoundsShillingsPence(0, 3, 10))
.withLine("Tweed Cape", new PoundsShillingsPence(0, 4, 12));
Invoice invoiceWithDiscount = new InvoiceBuilder(products)
.withDiscount(0.10)
.build();
Invoice invoiceWithGiftVoucher = new InvoiceBuilder(products)
.withGiftVoucher("12345")
.build();
Alternatively, you could add a factory method to the builder that returns a new builder with a copy of the builder's state:
InvoiceBuilder products = new InvoiceBuilder()
.withLine("Deerstalker Hat", new PoundsShillingsPence(0, 3, 10))
.withLine("Tweed Cape", new PoundsShillingsPence(0, 4, 12));
Invoice invoiceWithDiscount = products.but().withDiscount(0.10)
.build();
Invoice invoiceWithGiftVoucher = products.but().withGiftVoucher("12345")
.build();
The safest option is to make every with method create an entirely new copy of the builder instead of returning this.
For the last eight years or so, London's famous Extreme Tuesday Club (XTC) has been meeting every Tueday to share ideas about XP, "agile" software development, test-driven development and otherwise waffle philosophise about software development over fine ale and microwaved, puff-pastry pies (being in the UK, the beer is much better than the food).
The last few years, we have met in the Old Bank of England pub on the Strand. From now on we'll be in the Counting House pub on Cornhill, near Bank tube station.
Lots of great ideas and collaborations have spun out of the XTC, among them the XP Day family of grass-roots conferences, the mock objects technique and the jMock library for test-driven development of object-oriented code, the First International Conference on Postmodern Programming, the Jester mutation testing tool, the Extreme Lego workshop and other training games, and lots more.
An interesting aspect of XTC is that it is completely anarchic. There is no central organising committee. There is no formal membership. What happens is entirely up to whoever turns up and gets involved. This year we will be spending some of the income from XP Day on regularly hosting more formal presentations and rerunning the most popular sessions from XP Day to make them available to a wider audience. But for the rest of the time, the informal get-together style will prevail.
So, if you're interested in the future direction of software development techniques and practices, do please come along the Counting House and join in. The beer is the same but the pies are better, I've been told.
]]>