SE 350 - Object-Oriented Software Development

Object-Oriented Programming Principles: SOLID

Instructor: Stefan Mitsch

Learning Objectives

  • Understand some common design flaws
  • Understand design principles
  • Be able to discuss benefits and drawbacks of design principles

Design Principles

S

Single Responsibility Principle

O

Open-Closed Principle

L

Liskov Substitution Principle

I

Interface Segregation Principle

D

Dependency Inversion Principle

Design Problems

  • Rigidity: implementing a change is difficult because it translates into a cascade of changes
  • Fragility: any change tends to break code in many, even conceptually unrelated, places
  • Immobility: unable to reuse modules across projects because of too many dependencies
  • Viscosity: developers will prefer easy changes even if they break design

Design Problems

Improper design leads to

S
Singleton

T
Tight Coupling

U
Untestabality

P
Premature Optimization

I
Indescriptive Names

D
Duplication

Principles to Avoid Design Problems

S

Single Responsibility Principle

O

Open-Closed Principle

L

Liskov Substitution Principle

I

Interface Segregation Principle

D

Dependency Inversion Principle

Single Responsibility Principle

A class should do one thing and have only a single reason to change

  • Gather together things that change for the same reason
  • Benefits
    • Easier testing and maintenance
    • Less internal complexity
  • How to follow
    • Write small classes with specific names and purposes
  • Tradeoffs
    • Cohesion vs. coupling

Single Responsibility Principle: Car

diagram

What is bad about this design?

Solution Washing, changing tires, and checking oil are not core responsibilities of a car. Driving could be (self-driving car).

Single Responsibility Principle: Car

diagram

  • A Driver drives the car, not the car itself
  • A CarWash can handle washing a car
  • A Mechanic can change tires and check oil

Single Responsibility Principle: Employee

diagram

What is bad about this design and how to fix it?

Solution

diagram

Single Responsibility Principle: Bank Customer

public class BankCustomer {
  private long personId;
  private String firstName;
  private String lastName;
  private List<Long> accountIds;
  private List<String> accountNumbers;
}
  • What is bad about this design and how to fix it?
Solution
public class BankCustomer {
  private long id;
  private String firstName;
  private String lastName;  
  private List<Account> accounts;
}

public class Account {
  private long id;
  private String number;
}
  • What can still be improved about this design?

Single Responsibility Principle: Bank Customer

public class Person {
  private long id;
  private String firstName;
  private String lastName;  
}

public class Account {
  private long id;
  private String number;
}

public class BankCustomer {
  private Person p;
  private List<Account> accounts;
}
  • Now Person is a class that is reusable by itself in other non-bank applications
  • This design can be criticized for premature flexibility!

Single Responsibility Principle: Book Invoice

public class Book {
  private String title;
  private List<Person> authors;  
}
public class Invoice {
  private Book book;
  private int quantity;
  private double itemPrice;

  public double totalPrice() {
    return itemPrice*quantity;
  }

  public void print() {
    System.out.println("Invoice " + book.title);
    System.out.println("Total price = $" + totalPrice + " (" + quantity + " * $" + itemPrice);
  }

  public void save() {
    // persist to a file
  }
}
  • What is bad about this design?

Single Responsibility Principle: Book Invoice

diagram

What is the benefit?

Solution

diagram

S

Single Responsibility Principle

O

Open-Closed Principle

L

Liskov Substitution Principle

I

Interface Segregation Principle

D

Dependency Inversion Principle

Open-Closed Principle

Software components should be open for extension, but closed for modification

  • Modification: change existing code
  • Extension: add new functionality
  • Open for extension: inheritance; add new functionality with new (sub)classes
  • What if there is a bug in existing code, can we not fix it?
  • Benefits:
    • Existing code does not need to change to add new functionality
  • How to follow
    • Provide interfaces and abstract classes, program to an interface
  • Tradeoffs
    • Needed vs. premature flexibility

Open-Closed Principle: Bookstore

public class InvoiceStore {
  private Invoice invoice;
  public InvoiceStore(Invoice invoice) {
    this.invoice = invoice;
  }

  public void saveToFile(String filename) {}
  public void saveToDatabase() {}
}

What is bad about this design?

Solution

Need to modify InvoiceStore every time we want to add a new stores; violates both Single-Responsibility Principle and Open-Closed Principle

Open-Closed Principle: Bookstore

diagram

Open-Closed Principle: Calculator

public abstract class BinCalcOp {
  public double l;
  public double r;  
}
public class Plus implements CalcOp {}
public class Minus implements CalcOp {}
public class Calculator {
  public double calculate(BinCalcOp op) {
    if (op == null) {
      throw new IllegalArgumentException("Unable to perform op");
    } else if (op instanceof Plus) {
      return op.l + op.r;
    } else if (op instanceof Minus) {
      return op.l - op.r;
    }
  }
}

What is bad about this design?

Solution

Calculator has to change every time we add a new operation.

Open-Closed Principle: Calculator

public abstract class BinCalcOp {
  private double l;
  private double r;
  // constructors, getters and setters
  public abstract double calc();
}
public class Plus implements CalcOp {
  @Override public double calc() { return getL() + getR(); }
}
public class Minus implements CalcOp {
  @Override public double calc() { return getL() - getR(); }
}
public class Calculator {
  public double calculate(BinCalcOp op) {
    if (op == null) throw new IllegalArgumentException("Unable to perform op");
    return op.calc();
  }
}

Open-Closed Principle: Banking System

diagram

What is bad about this design?

Solution

WithdrawalService will have to change every time we add a new account type

Open-Closed Principle: Banking System

diagram

Now WithdrawalService can withdraw and deposit to any account; doesn't need to change for new account types

Open-Closed Principle: Store

diagram

if (shipping == "ground") {
  if (getTotal() > 100) return 0;
  else return 10;
} else if (shipping == "air") return 50;

What is bad about this implementation?

Solution

diagram

S

Single Responsibility Principle

O

Open-Closed Principle

L

Liskov Substitution Principle

I

Interface Segregation Principle

D

Dependency Inversion Principle

Liskov Substitution Principle

Derived types must be completely substitutable for their base types

  • Strong behavioral subtyping
  • Let be a property provable about objects of type . Then should be true for objects of type where is a subtype of .
  • Symbolically
  • Desirable properties are e.g. correctness, termination
  • Benefits
    • Helps conform to "is-a" relationship
    • Extension does not break correctness
  • How to follow
    • Make class and method contracts explicit, and satisfy them on extension
    • Avoid extension with unsupported methods

Liskov Substitution Principle: Square

A square is a special rectangle?

public class Rectangle {
  protected double length;
  protected double width;
  public Rectangle(double length, double width) { /* ... */ }
  public void getLength() { return length; }
  public void setLength(double l) { length = l; }
  public void getWidth() { return width; }
  public void setWidth(double w) { width = w; }
}

public class Square extends Rectangle {
  public Square(double side) { super(side, side); }
  public void setSide(double s) { length = s; width = s; }
  @Override
  public void setLength(double l) { setSide(l); }
  @Override
  public void setWidth(double w) { setSide(w); }
}

What is bad about this design?

Solution

Clients of Rectangle do not expect the width to change when changing the length and vice versa.

Rectangle r = new Rectangle(5, 10);
r.setLength(7);
// client code breaks for squares
assert r.getWidth() == 10; 

Liskov Substitution Principle: Banking System

public class FixedTermAccount extends Account {
  @Override
  public void deposit(BigDecimal amount) { }
  public void withdraw(BigDecimal amount) {
    throw new UnsupportedOperationException("Cannot withdraw from fixed-term account");
  }
}

Why does this code violate the Liskov Substitution Principle?

Solution

Clients of Account expect to be able to deposit and withdraw!

Liskov Substitution Principle: Banking System

diagram

Liskov Substitution Principle: Documents

diagram

  • Why is this design violating the Liskov Substitution Principle?
Solution

Class Project expects all Documents to be saveable, but ReadOnlyDocument throws an exception.

  • foreach (doc in documents) if (!doc instanceof ReadOnlyDocument) doc.save()
  • What is bad about the fix above?

Liskov Substitution Principle: Documents

diagram

S

Single Responsibility Principle

O

Open-Closed Principle

L

Liskov Substitution Principle

I

Interface Segregation Principle

D

Dependency Inversion Principle

Interface Segregation Principle

Clients should not be forced to implement unnecessary methods which they will not use

  • Lots of client-specific interfaces are better than 1 general-purpose interface
  • Related to Single Responsibility Principle
  • Violating it often violates also Liskov Substitution Principle
  • Benefits
    • Avoids empty/unsupported implementations
    • Allows objects to take on many different roles
  • How to follow
    • Avoid general-purpose interfaces, provide specific interfaces
  • Tradeoffs
    • Cohesion vs. coupling

Interface Segregation Principle: Parking Lot

public interface ParkingLot {
  int park(Car c);
  Car unpark(int ticket);
  void unpark(Car c);
  int getCapacity();
  double getFee(int ticket);
  void pay(int ticket);
}

public class FreeParking implements ParkingLot {
  // can park and unpark, but fee and pay do not apply
}
public class ValetParking implements ParkingLot {
  // cannot self-park
}
public class StreetParking implements ParkingLot {
  // can only self-park
  // has unknown capacity
}

Interface Segregation Principle: Parking Lot

diagram

Interface Segregation Principle: Cloud Provider

diagram

Solution

diagram

Interface Segregation Principle: Java AWT

  • Lots of listener interfaces, e.g.
    • FocusListener
    • KeyListener
    • MouseMotionListener
    • MouseWheelListener
    • TextListener
    • WindowFocusListener
  • Clients implement only what they need

S

Single Responsibility Principle

O

Open-Closed Principle

L

Liskov Substitution Principle

I

Interface Segregation Principle

D

Dependency Inversion Principle

Dependency Inversion Principle

Modules should depend on interfaces/abstract classes, but not on concrete classes

  • Depend on abstractions (not specializations)
    • High-level modules should not import anything from low-level modules; both should depend on abstractions
    • Abstractions should not depend on concrete implementations; implementations should depend on abstractions
  • Benefits
    • Decouples client from implementation
    • Sets up design for Open-Closed Principle
  • How to follow
    • Provide interfaces for any anticipated extension point
  • Tradeoffs
    • Needed vs. premature flexibility

Dependency Inversion Principle

diagram

diagram

Dependency Inversion Principle: Payment

public class SqlProductRepo {
  public Product getProduct(String id) {}
}
public class PaymentProcessor {
  public void pay(String productId) {
    SqlProductRepo r = new SqlProductRepo();
    Product product = r.getProduct(String id);
    processPayment(product);
  }
  private void processPayment(Product product) {}
}
public interface ProductRepo {
  Product getProduct(String id);
}
public class SqlProductRepo implements ProductRepo {}
public class PaymentProcessor {
  private ProductRepo r;
  public PaymentProcessor(ProductRepo r) {}
  public void pay(String productId) {
    Product product = r.getProduct(String id);
    processPayment(product);
  }
  private void processPayment(Product product) {}
}

Dependency Inversion Principle: Budget Report

High-level references low-level

diagram

Abstraction breaks direct dependency

diagram

Dependency Inversion Principle: Calculator

diagram

diagram

Criticisms of SOLID

  • Somewhat vague principles
  • Focuses too much on dependencies
  • Long inheritance and delegation chains
  • Too much separation and abstraction can make code unreadable

Tony Marston, 2011

Marco Cecconi, 2014

Summary

S

Single Responsibility Principle


A class should have a single responsibility and a single reason to change

O

Open-Closed Principle


Open for extension but closed for modification

L

Liskov Substitution Principle


Objects should be replacable with instances of their subtypes without affecting correctness

I

Interface Segregation Principle


Clients should not be forced to depend on unused interfaces

D

Dependency Inversion Principle


Program to an interface, not to an implementation