The document discusses principles and best practices for object-oriented design and test-driven development. It recommends loose coupling and high cohesion between classes, defining clear roles and responsibilities for objects, and passing dependencies into constructors rather than setting them later. It also provides tips for writing cleaner tests through use of builders to generate test data, value types for literals, and factory methods to simplify object creation. Tests should assert one behavior each and use meaningful messages on failures.
4. Coupling and Cohesion:
Loose coupling easier maintenance
Cohesion: unit of responsibility
Roles, Responsibilities, Collaborators:
Role: set of related responsibilities
Collaborators: roles you interact with
Internals vs. Peers
5. No And’s, Or’s, or But’s
Every object should have a single, clearly defined
responsibility
Three types of Peers:
Dependencies
▪ The object can’t live without them
Notifications
▪ Need to be kept up to date, but we “don’t care” if they
listen
Adjustments
6. New or new not. There is no try. (Yoda)
Dependencies have to be passed into the
constructor.
Notifications and Adjustments can be
initialised to safe defaults.
7. Identify Relationships with Interfaces
Keep interfaces narrow
Interfaces are pulled into existence by tests
No I<class> (ICustomer Customer)
No InterfaceImpl (Customer CustomerImpl)
Name each implementation!
If you don’t find a good name for the
implementation, maybe it’s not a good
interface…
9. Try to understand this (train wreck):
((EditSaveCustomizer) master
.getModelisable()
.getDockablePanel()
.getCustomizer())
.getSaveItem()
.setEnabled(Boolean.FALSE.booleanValue());
Isn’t this simpler?
master.allowSavingOfCustomisations();
10. Or, now really with a train:
public class Train {
private final List<Carriage> carriages […]
private int percentReservedBarrier = 70;
public void reserveSeats(ReservationRequest request) {
for (Carriage carriage : carriages) {
if (carriage.getSeats().getPercentReserved() <
percentReservedBarrier) {
request.reserveSeatsIn(carriage);
return;
}
}
request.cannotFindSeats();
}
}
Why isn’t that good design?
11. Isn’t this simpler?
public void reserveSeats(ReservationRequest request) {
for (Carriage carriage : carriages) {
if (carriage.hasSeatsAvailableWithin
(percentReservedBarrier)) {
request.reserveSeatsIn(carriage);
return;
}
}
request.cannotFindSeats() ;
}
13. Phrase Messages with Meaning
What went wrong here?
Found <null> expected <not null>
Found <0> expected <17>
Use constants
Use special types
Found <Harry> expected <Customer.NotFound>
13
15. Beware “Long” Strings!
Test format and contents independently
“13/09/10 – Order 8715 signed by Manfred Mayer”
Define a Value Type for longer Messages
“<decision.date> ‐ Order
<decision.no> signed by <decision.owner>”
Public class Decision{
Public Date date;
Public String no;
Public String owner;
}
15
16. Use Assertions sensibly
Assert one expected behaviour in exactly one
test
Otherwise you have to regularly update multiple
tests!
Otherwise it’s much less clear what the test does
Rather have one more test than an unclear
one
The “Single Responsibility Principle” applies
here, too!
16
18. Many attempts to communicate are nullified
by saying too much.— Robert Greenleaf
19. First Try
@Test public void chargesCustomerForTotalCostOfAllOrderedItems() {
Order order = new Order(
new Customer("Sherlock Holmes",
new Address("221b Baker Street",
"London",
new PostCode("NW1", "3RX"))));
order.addLine(new OrderLine("Deerstalker Hat", 1));
order.addLine(new OrderLine("Tweed Cape", 1));
[…]
}
21. Centralise test data creation…
Order order1 =
ExampleOrders.newDeerstalkerAndCapeAndSwordstickOrder();
Order order2 =
ExampleOrders.newDeerstalkerAndBootsOrder();
[…]
24. public class OrderBuilder {
private Customer customer = new CustomerBuilder().build();
private List<OrderLine> lines = new ArrayList<OrderLine>();
private BigDecimal discountRate = BigDecimal.ZERO;
public static OrderBuilder anOrder() {
return new OrderBuilder();
}
public OrderBuilder withCustomer(Customer customer) {
this.customer = customer;
return this;
}
public OrderBuilder withOrderLines(List<OrderLine> lines) {
this.lines = lines;
return this;
}
public OrderBuilder withDiscount(BigDecimal discountRate) {
this.discountRate = discountRate;
return this;
}
public Order build() {
Order order = new Order(customer);
for (OrderLine line : lines) order.addLine(line);
order.setDiscountRate(discountRate);
}
return order;
}
}
25. public class OrderBuilder {
private Customer customer = new CustomerBuilder().build(); Default values
private List<OrderLine> lines = new ArrayList<OrderLine>();
private BigDecimal discountRate = BigDecimal.ZERO;
public static OrderBuilder anOrder() {
return new OrderBuilder();
}
public OrderBuilder withCustomer(Customer customer) {
this.customer = customer;
return this;
}
public OrderBuilder withOrderLines(List<OrderLine> lines) {
this.lines = lines;
return this;
}
public OrderBuilder withDiscount(BigDecimal discountRate) {
this.discountRate = discountRate;
return this;
}
public Order build() {
Order order = new Order(customer);
for (OrderLine line : lines) order.addLine(line);
order.setDiscountRate(discountRate);
}
return order;
}
}
26. public class OrderBuilder {
private Customer customer = new CustomerBuilder().build(); Default values
private List<OrderLine> lines = new ArrayList<OrderLine>();
private BigDecimal discountRate = BigDecimal.ZERO;
public static OrderBuilder anOrder() {
return new OrderBuilder();
}
public OrderBuilder withCustomer(Customer customer) {
this.customer = customer;
return this; Return: Builder
}
public OrderBuilder withOrderLines(List<OrderLine> lines) {
this.lines = lines;
return this;
}
public OrderBuilder withDiscount(BigDecimal discountRate) {
this.discountRate = discountRate;
return this;
}
public Order build() {
Order order = new Order(customer);
for (OrderLine line : lines) order.addLine(line);
order.setDiscountRate(discountRate);
}
return order;
}
}
27. public class OrderBuilder {
private Customer customer = new CustomerBuilder().build(); Default values
private List<OrderLine> lines = new ArrayList<OrderLine>();
private BigDecimal discountRate = BigDecimal.ZERO;
public static OrderBuilder anOrder() {
return new OrderBuilder();
}
public OrderBuilder withCustomer(Customer customer) {
this.customer = customer;
return this; Return: Builder
}
public OrderBuilder withOrderLines(List<OrderLine> lines) {
this.lines = lines;
return this;
}
public OrderBuilder withDiscount(BigDecimal discountRate) {
this.discountRate = discountRate;
return this;
}
public Order build() {
Order order = new Order(customer);
for (OrderLine line : lines) order.addLine(line);
order.setDiscountRate(discountRate);
}
return order; build() always returns a new
}
} order!
28. Default fits into one row:
Order order = new OrderBuilder().build();
Any deviation is obvious:
new OrderBuilder()
.fromCustomer(
new CustomerBuilder()
.withAddress(new AddressBuilder().withNoPostcode()
.build())
.build())
.build();
29. Creation using Object Mother hides the error:
TestAddresses.newAddress("221b Baker Street", "London", "NW1 6XE");
With the Data Builder the error is explicit:
new AddressBuilder()
.withStreet("221b Baker Street")
.withStreet2("London")
.withPostCode("NW1 6XE")
.build();
30. Multiple objects lead to repetition (again...):
Order orderWithSmallDiscount = new OrderBuilder()
.withLine("Deerstalker Hat", 1)
.withLine("Tweed Cape", 1)
.withDiscount(0.10)
.build();
Order orderWithLargeDiscount = new OrderBuilder()
.withLine("Deerstalker Hat", 1)
.withLine("Tweed Cape", 1)
.withDiscount(0.25)
.build();
31. This is better if objects differ in only one field:
OrderBuilder hatAndCape = new OrderBuilder()
.withLine("Deerstalker Hat", 1)
.withLine("Tweed Cape", 1);
Order orderWithSmallDiscount =
hatAndCape.withDiscount(0.10).build();
Order orderWithLargeDiscount =
hatAndCape.withDiscount(0.25).build();
32. Attention, possible error:
Order orderWithDiscount = hatAndCape.withDiscount(0.10) .build();
Order orderWithGiftVoucher =
hatAndCape.withGiftVoucher("abc").build();
The second order has a discount as well!
33. Better:
We add a CopyConstructor to the Builder:
Order orderWithDiscount = new OrderBuilder(hatAndCape)
.withDiscount(0.10)
.build();
Order orderWithGiftVoucher = new OrderBuilder(hatAndCape)
.withGiftVoucher("abc")
.build();
34. More elegant:
Factory‐Method, naming what it’s used for:
Order orderWithDiscount = hatAndCape.
but().withDiscount( 0.10).build();
Order orderWithGiftVoucher =
hatAndCape.but().withGiftVoucher("abc").build();
35. instead of:
Order orderWithNoPostcode = new OrderBuilder()
.fromCustomer(new CustomerBuilder()
.withAddress(new AddressBuilder()
.withNoPostcode()
.build()).build()).build();
it’s more elegant, to pass the Builder:
Order order = new OrderBuilder()
.fromCustomer( new CustomerBuilder()
.withAddress(new AddressBuilder().withNoPostcode() ))).build();
36. Even more elegant: Factory methods
Order order =
anOrder().fromCustomer(
aCustomer().withAddress(anAddress().withNoPostcode() ) ) .build();
Overloading makes it shorter:
Order order =
anOrder() .from(aCustomer().with(anAddress().withNoPostcode()))
.build();
This is how readable a test can (and should)
be.