NU London
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")
![]() |
|
Free online references (mostly GoF patterns reiterated):
Wikipedia has an extensive list and a dedicated article on most of design patterns (https://en.wikipedia.org/wiki/Software_design_pattern)
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.
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.
A tree of objects: a hierarchy of objects with distributed responsibilities
A directed acyclic graph (DAG) of objects: more generic case!
Imagine we need to draw the following picture (a UML use case diagram):
We need classes Actor
, UseCase
and Arrow
,
and have to make them to be the components of Diagram
![]() | ![]() |
Draw
method of a diagramclass Diagram implements Element {
void draw() {
for (Element e: elements) {
e.draw();
}
}
}
A generic "layered" architecture of a typical Java application
Avoid creating huge "superobjects"
Remember: Divide and conquer!
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").
![]() |
It is impossible to make a single instance of |
![]() |
|
![]() |
|
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
}
![]() |
|
![]() |
|
Do not make your classes responsible for creation of their dependencies. Let Factory Method do this job for them!
Define dependencies as constructor parameters, save them in class member variables.
This approach facilitates unit testing.
Dependency Injection (DI) frameworks, such as Spring, provide automatic factory methods.
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));
}
}
Collections.sort(people,
Comparator
.comparing(Person::getLastName)
.thenComparing(Person::getFirstName));
);
In object-oriented programming, an object not only encapsulates data: it can encapsulate behaviour as well!
You can pass a Strategy as a parameter to a method or a generic algorithm.
Lambdas and method references are syntactic constructions that make it easy to define strategies which consist of a single method only.
Command object incapsulates the fact of invocation of some methods with some parameters.
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.
TAKE… | …TRANSFORM INTO |
![]() |
|
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
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.
Data Structure | Visitor |
|
|
//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
![]() |
|
\[\huge
\frac {-b - \sqrt {b^2- 4ac}} {2 a}\] | In our "language":
|
Visualize | Calculate |
![]() | Let’s solve \(x^2 + 2024x + 2023 = 0\) (Evaluate the formula for \(a = 1, b = 2024, c = 2023\)). |
![]() | ![]() |
![]() | ![]() |
![]() | ![]() |
![]() | ![]() |
![]() | ![]() |
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);
}
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.