Object-Oriented Design Patterns

NU London

Why do we need design patterns?

  • We’re facing the same problems over and over again while designing the software

  • There are "standard" good solutions used in numerous projects

  • These solutions have specific names which makes communication easier
    (e.g. "I used Delegate pattern in order to get the value for this property")

The Classical Book

gof
  • Erich Gamma et al., Design Patterns: Elements of Reusable Object-Oriented Software

  • Published in 1994

  • A classical book on software design (although partially outdated)

  • Patterns from this book are referred to as GoF patterns ("Gang of Four" is 4 authors of this book).

Some other sources

Free online references (mostly GoF patterns reiterated):

Types of design patterns

  • Creational patterns  — various object creation mechanisms, which increase flexibility and reuse of existing code

  • Structural patterns — focus on how to assemble objects and classes into larger structures, while keeping these structures flexible and efficient.

  • Behavioural patterns — concerned with algorithms and the assignment of responsibilities between objects.

What we will cover

  • In total, there are about 30 design patterns, but not all of them are equally frequently used.

  • Singleton GoF design pattern is now considered to be an antipattern because of its difficulty to test. Currently, superceded by Dependency Injection frameworks (e.g. Spring in Java).

  • We will cover only some basic patterns and those that will be useful for your homework assignment.

Superobject antipattern

diag aa912d7680456dbc0aa4f298af480458

Composite: Divide and conquer!

A tree of objects: a hierarchy of objects with distributed responsibilities

diag 7b431cca8aa9faef123b6231f3936a8e

Composite: Divide and conquer!

A directed acyclic graph (DAG) of objects: more generic case!

diag f87276e0417d72bdac3c78610a9fc0db

A classic example

Imagine we need to draw the following picture (a UML use case diagram):

diag 14675fbc670e028f614e527c00c74415

We need classes Actor, UseCase and Arrow,
and have to make them to be the components of Diagram

A classic example

diag 14675fbc670e028f614e527c00c74415
diag 682407ba0a4003f62cdc6a555d5743f2

Draw method of a diagram

diag 2b68a3948f2943f0abfabb972743e0cb
class Diagram implements Element {
    void draw() {
        for (Element e: elements) {
            e.draw();
        }
    }
}

A generic decomposition of a typical application

A generic "layered" architecture of a typical Java application

layers

Your own application from Project 1 & 2

diag e6f236616672e21b8b0dab21ae21fd76

Composite takeaways

  1. Avoid creating huge "superobjects"

  2. Remember: Divide and conquer!

  3. When your components implement the same interface, you can command them to do perform the same action at once (e. g. "draw me a diagram").

Now let’s consider the following example

png

A naïve approach: "Let the objects create what they need themselves"

png
public class A {
  private final B b = new B();
  private final C c = new C();
}

public class B {
  private final D d = new D();
}

public class C {
  private final D d = ???; //OOPS!
}

It is impossible to make a single instance of D
shared between B and C!

How do we test?

png
  • If A creates B and C, how can we test A separately?

  • How do we substitute B and C with mock implementations?

  • This is impossible if we do it like this:

public class A {
  private final B b = new B(...);
  private final C c = new C(...);
}

Let’s make external dependencies "pluggable"

png
class A {
  private final B b;
  private final C C;
  public A(B b, C c) {
      this.b = b;
      this.c = c; }}

class B {
    private final D d;
    public B(D d){
        this.d = d; }}

class C {
    private final D d;
    public C(D d){
        this.d = d; }}

Now testing is possible!

class ATest {
    //Use mocks instead of "real" dependencies
    A objectUnderTest = new A(
            Mockito.mock(B.class),
            Mockito.mock(C.class)
    );


    @Test
    // setup mocks and test as needed
}

Factory Method

png
/*Move all the "wiring" from classes
to the factory method*/
public class ApplicationFactory {

  public static A createA() {
     D d = new D();
     B b = new B(d);
     C c = new C(d);
     A a = new A(b,c);
     return a;
  }
}

How it’s done in DI ("dependency injection") frameworks

png
@Component
class A {
  private final B b;
  private final C C;
  public A(B b, C c) { this.b = b; this.c = c; }}

@Component
class B {
  private final D d;
  public B(D d) { this.d = d; }}

@Component
class C {
  private final D d;
  public C(D d) { this.d = d; }}

A a = context.getBean(A.class);

Factory method takeaways

  1. Do not make your classes responsible for creation of their dependencies. Let Factory Method do this job for them!

  2. Define dependencies as constructor parameters, save them in class member variables.

  3. This approach facilitates unit testing.

  4. Dependency Injection (DI) frameworks, such as Spring, provide automatic factory methods.

Strategy: incapsulating a behaviour into an object

strategy

In Java, Strategy is often a lambda or a method reference

class Calculator {
    private final Function<Integer, Integer> taxStrategy;
    Calculator(Function<Integer, Integer> taxCalculationStrategy) {
        this.taxStrategy = taxCalculationStrategy;
    }
    Integer calculate(Integer amount) {
        Integer surcharged = amount + 15;
        return surcharged + taxStrategy.apply(surcharged);
    }
    public static void main(String[] args) {
        System.out.println(new Calculator(
                //Flat 20% strategy
                amount -> amount / 5
        ).calculate(100));
    }
}

Comparator: another example of a strategy

Collections.sort(people,
  Comparator
    .comparing(Person::getLastName)
    .thenComparing(Person::getFirstName));
);

Strategy takeaways

  1. In object-oriented programming, an object not only encapsulates data: it can encapsulate behaviour as well!

  2. You can pass a Strategy as a parameter to a method or a generic algorithm.

  3. Lambdas and method references are syntactic constructions that make it easy to define strategies which consist of a single method only.

Command: Undo and Redo

undoredo
Undo and Redo stacks of commands

Command interface

diag 5369fb4c7ee2c23bc6e9704ea99b00ec

Command takeaways

  1. Command object incapsulates the fact of invocation of some methods with some parameters.

  2. Using Command patterns, we can log user actions (e. g. for audit purposes or for replication of user actions across a distributed system), and implement Undo/Redo functionality.

Visitor Pattern: A Practical Problem

TAKE…​

…​TRANSFORM INTO

something.png
  • Code

  • Text

  • Visual representation

  • Result of calculation (number, vector, matrix…​)

A very important specific case

  • Take AST (abstract synthax tree) of parsed code and

    • verify the correctness of code (e. g. that all the variables are defined and all the types are compatible)

    • compile it into machine code

    • execute (interpret) the code without compilation

Naive approach: add methods to the class?

Program program = Program.parse(....);
//These are methods of class Program
program.verify();
program.compile();
program.execute();

Drawback: too many responsibilities for Program class. Impossible to maintain and extend.

Separation of responisibilities: Visitor pattern

Data Structure

Visitor

  • Keeps the data.

  • "Knows" how to traverse the data.

  • "Navigates" the visitor.

  • "Knows" how to transform the data.

  • Each visitor is responsible for one kind of transformations only, but we can define as many visitors as needed.

Classes and their methods

diag 2416d56a2110d2112def00671c08122b

Order of calling

diag d4646fa58a49ec0a7c96f483ce998c9a

What is the simplest visitor pattern implementation?

//This is the data structure
Collection<T> collection = ...;

//Consumer<? super T> parameter is the visitor
collection.forEach (element -> {
  //do anything with each element
})
  • accept(Visitor) is forEach(Consumer)

  • lambda (as an object) is the Visitor

  • lambda’s method is the Visitor’s visit

Example: Let’s implement a simple language

calculator
PrimaryTerm
  • variable

  • numeric literal

UnaryOperation
  • unary -

  • square root (sqrt)

BinaryOperation
  • +, - (lower precedence)

  • *, / (higher precedence)

Parentheses
  • (, )

Quadratic equation root formula

\[\huge \frac {-b - \sqrt {b^2- 4ac}} {2 a}\]

In our "language":

(-b - sqrt(b * b - 4 * a * c)) / (2 * a)

Let’s do two things

Visualize

Calculate

diag dd20dc8e802a2b8af8e9e56d36d47665

Let’s solve

\(x^2 + 2024x + 2023 = 0\)

(Evaluate the formula for \(a = 1, b = 2024, c = 2023\)).

Evaluating the formula

\[\huge \sqrt{9} + 1\]
diag 43eb977dcf172e7b895cc66c3c3eb4e0
diag 7bb2deb27c7b1b644d0d401c5aa5fe6d

Evaluating the formula

\[\huge \sqrt{9} + 1\]
diag 1a9b78f8b9b1afdf0f55a048b4065e9e
diag cbcdf00ad3e0290c93292dc57cc13154

Evaluating the formula

\[\huge \sqrt{9} + 1\]
diag 1a9b78f8b9b1afdf0f55a048b4065e9e
diag cbcdf00ad3e0290c93292dc57cc13154

Evaluating the formula

\[\huge \sqrt{9} + 1\]
diag 7dfa41b66a87e899941f372ac5f54204
diag 4b65b63a15cc727fd4f92737026a8c0b

Evaluating the formula

\[\huge \sqrt{9} + 1\]
diag 0446379254b687867a56e19e36ceeedd
diag 80f9fe376309583d4fd9fa4368527410

Warning

  • Do not use pattern matching outside of visitor pattern (as a substitute of polymorphism)

/* It is better to have an abstract draw() method on Element class */
switch (element) {
    case Actor actor -> drawActor(actor);
    case Arrow arrow -> drawArrow(arrow);
    case UseCase useCase -> drawUseCase(useCase);
}

Takeaways (for Visitor)

  • Visitor pattern separates responsibilities between data structure and processor (visitor).

  • Data structure implements accept(Visitor), visitor implements visit(Element).

  • Depth-first traversal utilizing recursive calls on each of the elements of data structure is a common way to implement the pattern on the data structure side.

  • There can be multiple visitors implemented for different tasks.