Principles of Software Design – with hands-on exercises applying the SOLID principles – Developer guidelines



Principles of Software Design – with hands-on exercises applying the SOLID principles – Developer guidelines

0 0


tdds-slides


On Github brookingcharlie / tdds-slides

Test-Driven Development

and Principles of Software Design

The exercise

Shopping checkout

Take customer's shopping basket and generate an itemised receipt.

We'll build a simple console application using TDD.

Task 1: Simple app

Input

Pizza - Pepperoni,12.99
Pizza - Supreme,12.99
Garlic bread,8.50
Chianti,21.00

Ouput

*************** RECEIPT ****************

Pizza - Pepperoni                  12.99
Pizza - Supreme                    12.99
Garlic bread                        8.50
Chianti                            21.00
                                --------
Total for 4 items                  55.48
                                ========

Simple app test

@Test
public void testSingle() throws Exception {
    String input =
        "Pizza - Pepperoni,12.99";
    String expectedOutput =
        "*************** RECEIPT ****************\n" +
        "\n" +
        "Pizza - Pepperoni                  12.99\n" +
        "                                --------\n" +
        "Total for 1 items                  12.99\n" +
        "                                ========\n";
    try (
        StringReader reader = new StringReader(input);
        StringWriter writer = new StringWriter();
    ) {
        App.run(reader, writer);
        assertThat(writer.toString(), is(equalTo(expectedOutput)));
    }
}

Simple app code

public static void run(Reader reader, Writer writer) throws IOException {
    try (
        BufferedReader bufferedReader = new BufferedReader(reader);
        PrintWriter printWriter = new PrintWriter(writer);
    ) {
        String line = bufferedReader.readLine();
        String[] parts = line.split(",");
        String product = parts[0];
        BigDecimal price = new BigDecimal(parts[1]);

        printWriter.println("*************** RECEIPT ****************");
        printWriter.println();
        printWriter.println(String.format("%-32s%8.2f", product, price));
        printWriter.println(String.format("%-32s%8s", "", "--------"));
        printWriter.println(String.format("%-32s%8.2f", "Total for 1 items", price));
        printWriter.println(String.format("%-32s%8s", "", "========"));
    }
}

Single Responsibility Principle

A class should have one, and only one, reason to change.

Responsibilities of our App class?

Reads formatted input text Calculates total price (well, it will) Writes formatted output text

Task 2: App modules

Separate the App class's responsibilities:

Model basket concept as domain class Make basket responsible for calculating total Read/write text formats in separate class
public class Basket {
    public void addItem(Item item) {...}
    public Item getItem(int i) {...}
    public int getCount() {...}
    public BigDecimal getTotal() {...}
}

public static void run(Reader input, Writer output) throws IOException {
    Basket basket = new Basket();
    new BasketReader().read(basket, input);
    new BasketWriter().write(basket, output);
}

Basket test

Test adding/getting items, count, and total.

@Test
public void testSingleItem() {
    Basket basket = new Basket();
    basket.addItem(new Item("Pizza - Pepperoni", new BigDecimal("12.99")));
    assertThat(basket.getCount(), is(equalTo(1)));
    assertThat(basket.getItem(0).getProduct(),
        is(equalTo("Pizza - Pepperoni")));
    assertThat(basket.getItem(0).getPrice(),
        is(equalTo(new BigDecimal("12.99"))));
    assertThat(basket.getTotal(),
        is(equalTo(new BigDecimal("12.99"))));
}

Basket code

public class Basket {
    private final ArrayList<Item> items;

    public Basket() { this.items = new ArrayList<Item>(); }
    public void addItem(Item item) { items.add(item); }
    public Item getItem(int i) { return items.get(i); }
    public int getCount() { return items.size(); }

    public BigDecimal getTotal() {
        return items.stream()
            .map(i -> i.getPrice())
            .reduce((a, b) -> a.add(b))
            .orElse(new BigDecimal("0.00"));
    }
}

Basket reader test

Verify that reader adds items to the basket.

@Test
public void testSingleItem() throws IOException {
    Basket basket = mock(Basket.class);
    StringReader input = new StringReader("Pizza - Pepperoni,12.99\n");
    new BasketReader().read(basket, input);

    ArgumentCaptor<Item> item = ArgumentCaptor.forClass(Item.class);
    verify(basket, times(1)).addItem(item.capture());
    assertThat(item.getValue().getProduct(),
        is(equalTo("Pizza - Pepperoni")));
    assertThat(item.getValue().getPrice(),
        is(equalTo(new BigDecimal("12.99"))));
}

Basket reader code

public void read(Basket basket, Reader input) throws IOException {
    try (
        BufferedReader bufferedReader = new BufferedReader(input);
    ) {
        bufferedReader.lines()
            .map(line -> line.split(","))
            .map(parts -> new Item(parts[0], new BigDecimal(parts[1])))
            .forEach(item -> basket.addItem(item));
    }
}

Basket writer test

Compare actual vs. expected output.

@Test
public void testSingleItem() throws IOException {
    Basket basket = new Basket();
    basket.addItem(new Item("Pizza - Pepperoni", new BigDecimal("12.99")));
    StringWriter output = new StringWriter();
    new BasketWriter().write(basket, output);

    String expectedOutput =
        "*************** RECEIPT ****************\n" +
        "\n" +
        "Pizza - Pepperoni                  12.99\n" +
        "                                --------\n" +
        "Total for 1 items                  12.99\n" +
        "                                ========\n";
    assertThat(output.toString(), is(equalTo(expectedOutput)));
}

Basket writer code

public void write(AccessibleBasket basket, Writer output) throws IOException {
  try (PrintWriter printer = new PrintWriter(output)) {
    printer.println("*************** RECEIPT ****************");
    printer.println();
    for (int i = 0; i < basket.getCount(); i++) {
      printer.println(String.format("%-32s%8.2f",
        basket.getItem(i).getProduct(), basket.getItem(i).getPrice()));
    }
    printer.println(String.format("%-32s%8s", "", "--------"));
    String total = String.format("Total for %d items", basket.getCount());
    printer.println(String.format("%-32s%8.2f", total, basket.getTotal()));
    printer.println(String.format("%-32s%8s", "", "========"));
  }
}

Interface Segregation Principle

Make fine grained interfaces that are client specific.

Consider the clients of our Basket class:

  • BasketReader only needs addItem
  • BasketWriter needs getters (item, count, total)
  • ...but it shouldn't be able to call addItem

Task 3: Segregate interfaces

public interface MutatableBasket {
    void addItem(Item item);
}
public interface AccessibleBasket {
    Item getItem(int i);
    int getCount();
    BigDecimal getTotal();
}
public class Basket implements MutatableBasket, AccessibleBasket {...}

public class BasketReader {
    public void read(MutatableBasket basket, Reader input) {...}
}
public class BasketWriter {
    public void write(AccessibleBasket basket, Writer output) {...}
}

Liskov Substitution Principle

Functions that reference base classes must be able to use their derived classes without knowing it.
abstract class Shape {
}
class Rectangle extends Shape {
  void drawRectangle() {...}
}
class Square extends Shape {
  void drawSquare() {...}
}
class App {
  void render(Shape s) {
    if (s instanceof Rectangle) {
      shape.drawRectangle();
    }
    // ...
  }
}
abstract class Shape {
  abstract void draw();
}
class Rectangle extends Shape {
  void draw() {...}
}
class Square extends Shape {
  void draw() {...}
}


class App {
  void render(Shape shape) {
    shape.draw();
  }
}

Task 4: Sale basket

Everything must go!
  • Give all customers 10% discount
  • Print discount message on receipt

A sale basket

public class SaleBasket extends Basket {
  @Override
  public BigDecimal getTotal() {
    return super.getTotal().multiply(new BigDecimal("0.9")).setScale(2);
  }
}

BasketWriter

     String totalDescription = String.format("Total for %d items", basket.getCount());
     printWriter.println(String.format("%-32s%8.2f", totalDescription, basket.getTotal()));
     printWriter.println(String.format("%-32s%8s", "", "====...."));
+    if (basket instanceof SaleBasket) {
+      printWriter.println("Includes discount of 10%");
+    }
   }
 }

Another sale basket

Basket vs SaleBasket

public String getMessage() {
    return null;
}
@Override
public String getMessage() {
    return "Includes discount of 10%";
}

BasketWriter.java

-if (basket instanceof SaleBasket) {
-  printWriter.println("Includes discount of 10%");
-}
+if (basket.getMessage() != null) {
+  printWriter.println(basket.getMessage());
+}

Open-Closed Principle

Software entities should be open for extension, but closed for modification.

Problem: cannot extend a class without modifying it.

  • changes to a class result in...
  • mass changes to dependent classes

Solution: design fixed classes with extension points.

  • extend class behaviour by...
  • adding code instead of modifying it

Task 5: Extensible basket

The basket to end all baskets
  • What else could we want in a basket?
  • Maybe the discount should be a line item.
  • What if future baskets need extra line items?

Our extensible basket

Basket vs SaleBasket

@Override
public List<Item> getExtraItems() {
  return Collections.emptyList();
}
@Override
public List<Item> getExtraItems() {
  BigDecimal discount = super.getTotal().multiply(new BigDecimal("-0.1")).setScale(2);
  return asList(new Item("Discount", discount));
}

BasketWriter.java

 for (int i = 0; i < basket.getCount(); i++) {
   printWriter.println(String.format("%-32s%8.2f", basket.getItem(i).getProduct(), basket.getItem(i).getPrice()));
 }
+for (Item item : basket.getExtraItems()) {
+  printWriter.println(String.format("%-32s%8.2f", item.getProduct(), item.getPrice()));
+}

Our extensible basket

Basket vs SaleBasket

public class Basket implements MutatableBasket, AccessibleBasket {
  public final List<Item> getItems() {
    List<Item> result = new ArrayList<Item>(items);
    result.addAll(getExtraItems());
    return result;
  }
  protected List<Item> getExtraItems() {
    return Collections.emptyList();
  }
}
public class SaleBasket extends Basket {
  @Override
  protected List<Item> getExtraItems() {
    BigDecimal discount = super.getTotal().multiply(new BigDecimal("-0.1")).setScale(2);
    return asList(new Item("Discount", discount));
  }
}

Our extensible basket

Basket

public interface Basket implements MutatableBasket, AccessibleBasket {
  public void addItem(Item item) {...}
  public List<Item> getItems() {...}
  public BigDecimal getTotal() {...}
  public String getMessage() {...}
}

BasketWriter.java

for (Item item : basket.getItems()) {
  printWriter.println(String.format("%-32s%8.2f", item.getProduct(), item.getPrice()));
}

Dependency Inversion Principle

High-level modules should not depend upon low-level modules.

Both should depend upon abstractions!

  • Code naturally depends on its dependencies, right?
  • But is this wrong? Does our design become brittle?
  • Rigorously apply Open-Closed and Liskov.

Task 6: App dependencies

Our app depends on several concrete classes.

  • Creates BasketReader/Writer and Basket
  • Goal: depend on reader/writer abstractions
  • Goal: use a SaleBasket on weekends
public class App {
  public void run(Reader input, Writer output) throws IOException {
    BasketReader inputReader = new BasketReader();
    BasketWriter outputWriter = new BasketWriter();
    Basket basket = new Basket();
    inputReader.read(basket, input);
    outputWriter.write(basket, output);
  }
}

Injecting dependencies

public class App {
  private final IBasketFactory basketFactory;
  private final IBasketReader inputReader;
  private final IBasketWriter outputWriter;

  public App(IBasketFactory basketFactory, IBasketReader inputReader,
             IBasketWriter outputWriter) {
    this.basketFactory = basketFactory;
    this.inputReader = inputReader;
    this.outputWriter = outputWriter;
  }

  public void run(Reader input, Writer output) throws IOException {
    Basket basket = basketFactory.createBasket();
    inputReader.read(basket, input);
    outputWriter.write(basket, output);
  }
}

Basket factory

public interface BasketFactory {
  Basket createBasket();
}
public class WeekendSaleBasketFactory implements BasketFactory {
  @Override
  public Basket createBasket() {
    LocalDate date = LocalDate.now();
    if (date.getDayOfWeek() == SATURDAY || date.getDayOfWeek() == SUNDAY) {
      return new SaleBasket();
    }
    else {
      return new Basket();
    }
  }
}

Basket factory 2

public class WeekendSaleBasketFactory implements BasketFactory {
  private LocalDate date;

  public WeekendSaleBasketFactory(LocalDate date) {
    this.date = date;
  }

  @Override
  public Basket createBasket() {
    if (date.getDayOfWeek() == SATURDAY || date.getDayOfWeek() == SUNDAY) {
      return new SaleBasket();
    }
    else {
      return new Basket();
    }
  }
}

The "SOLID" principles

Single Responsibility A class should have one, and only one, reason to change. Open-Closed You should be able to extend a class's behavior without modifying it. Liskov Substitution Derived classes must be substitutable for their base classes. Interface Segregation Make fine grained interfaces that are client specific. Dependency Inversion Depend on abstractions, not on concretions.

General design principles

  • Do only what you need to do (YAGNI)
  • Aim for high cohesion and low coupling
  • Low cohesion means module does too much, hard to maintain
  • High coupling means high rigidity and fragility

Why BigDecimal?

public class DecimalTest {
  @Test
  public void testIncorrectDoubles() {
    double result = 0.1 + 0.2;
    assertThat(result, is(equalTo(0.30000000000000004)));
  }

  @Test
  public void testCorrectBigDecimal() {
    BigDecimal result = new BigDecimal("0.1").add(new BigDecimal("0.2"));
    assertThat(result, is(equalTo(new BigDecimal("0.3"))));
  }
}
Test-Driven Development and Principles of Software Design