Relevant Reading Material
Separation of Concerns
The best way to understand and implement a system is by breaking it into smaller pieces.
-
Each piece has a reason for its existence.
-
Each piece has a clear definition of how it can be used.
-
Each pieces does one job and it does it well.
-
Single responsibility principle - A class should have one and one job.
-
Open Close principle - open to extension closed for modification.
-
Liskov substitution principle - subtypes honor the properties set by their supertypes.
-
Interface segregation principle - clients should never be forced to implement/depend on interfaces/methods they do not use.
-
Dependency Inversion principle - depend on abstractions not concrete implementations when you can.
-
Design Patterns
A collection of well thought out and well documented solutions to commonly found problems.
-
Creational Patterns - deal with constructing new instances
-
Structural Patterns - deal with relationships between objects
-
Behavioural Patterns - deal with communication patterns between objects
Singelton
Recall our list implementation with Cons
and Empty
classes. The Empty
class had some peculiar properties
-
We could create multiple instance of
Empty
but they all had to be considered equal. Our implementation then-
equals
was unconventional, even the IDE refused to auto-create anequals
method since there was no state insideEmpty
objects -
hashCode
was also unconventional
-
Can we do better? Can we design a solution that
-
There is one and only one instance of
Empty
that is used through out of our program.-
Equality then can be
==
since we can rely on the fact that there is always 1 and only 1 object
-
Singleton UML class diagram
Static Factory Method
We have been using this pattern, this refers to the static method in our code, typically found in an interface, e.g., createEmpty
inside List
, that allows us to create the appropriate object.
Factory Pattern
A Factory pattern allows us to hide the creational process of a set of instances for our clients.
Factory UML Class diagram
A non-standard implementation
class ShapeName {
private static ShapeName self = new ShapeName();
private static ShapeName circleInstance;
private static ShapeName squareInstance;
private static ShapeName rectangleInstance;
static ShapeName circleName(){
if (ShapeName.circleInstance == null) {
ShapeName.circleInstance = self.new CircleName();
}
return ShapeName.circleInstance;
}
static ShapeName squareName(){
if (ShapeName.squareInstance== null) {
ShapeName.squareInstance= self.new SquareName();
}
return ShapeName.squareInstance;
}
static ShapeName rectangleName(){
if (ShapeName.rectangleInstance == null) {
ShapeName.rectangleInstance = self.new RectangleName();
}
return ShapeName.rectangleName();
}
private ShapeName() {}
private class CircleName extends ShapeName {
private CircleName(){}
}
private class SquareName extends ShapeName {
private SquareName(){}
}
private class RectangleName extends ShapeName {
private RectangleName(){}
}
}
private static ShapeFactory instance;
private final Posn posn;
private final Integer radius;
private final Integer side;
private final Integer height;
private final Integer width;
public static ShapeFactory getInstance() {
if (ShapeFactory.instance == null) {
ShapeFactory.instance = new ShapeFactory();
}
return ShapeFactory.instance;
}
private ShapeFactory() {
this.posn = new Posn(0, 0);
this.radius = new Integer(10);
this.side = new Integer(10);
this.width = new Integer(20);
this.height = new Integer(10);
}
public Shape getCircle(ShapeName name) {
if (ShapeName.circleName().equals(name)) {
return new Circle(posn, radius);
} else if (ShapeName.squareName().equals(name)) {
return new Rectangle(posn, side, side);
} else if (ShapeName.rectangleName().equals(name)) {
return new Rectangle(posn, width, height);
} else {
throw new IllegalArgumentException("Unknown product : " + name);
}
}
}
I’ll let you read up on the Abstract Factory Pattern
Template Method
We use the template method pattern when we have a predefined sequence of steps that we need to perform, but, the individual steps themselves can vary depending on the use cases that we need to address.
Template Method UML Class Diagram
Consider a video game that contains different players holding different roles
-
builder, builders are set to a construction site and then the work on the construction site until it is done.
-
warrior, warriors are in the barracks practising until they are set to join a battle, then then fight in the battle until the battle is won, then they return to their barracks or the battle is lost at which point the warrior is dead.
-
miner, miners are set to a mine and work on retrieving raw materials from the mine. e.g, gold, coal etc.
The sequence of tasks for each role shares some commonalities while at the same time each role needs to perform a specialized (different) task.
Iterator Pattern
You have already seen this pattern. In fact Java has it implemented for most of its collection classes and allows you to implement it for your own.
Composite Design Pattern
We have been using something similar (if not the same) for our immutable lists implementation
Compare to our immutable List
design
Adapter Pattern
We use the adapter pattern to allow an existing interface to be used as another interface. Typically used when there is an existing class/interface that cannot be altered/extended and we need a new class/interface that does not exactly "fit" the existing interface. We create an new class/interface to "adapt" the existing interface and confront to the new requirements.
For a visual, similar problem in real life, think of a electric plug adaptor that we use to connect a device that we have that is designed for a different kind of plug.
Adapter UML Class Diagram
Let’s use our adapter pattern to add drawing our shapes with colour
public abstract class Adapter {
protected Shape original;
public Adapter(Shape original) {
this.original = original;
}
public abstract void draw(Colour colour);
}
public class CircleAdapter extends Adapter {
public CircleAdapter(Shape original) {
super(original);
}
@Override
public void draw(Colour colour) {
System.out.println(original.area() + " in colour " + colour.toString());
}
}
public class RectangleAdapter extends Adapter {
public RectangleAdapter(Shape original) {
super(original);
}
@Override
public void draw(Colour colour) {
System.out.println(original.circumference() + " in colour " + colour);
}
}
public class Main {
public static void main(String[] args) {
Adapter circle = new CircleAdapter(new Circle(new Posn(1,1), new Integer(10)));
Adapter rect = new RectangleAdapter(new Rectangle(new Posn(1,1), new Integer(10), new
Integer(20)));
circle.draw(new Red());
circle.draw(new Green());
rect.draw(new Blue());
}
}
-
Can we hide the concrete adapter types from our client? How?
-
Can we find a better way to create
Colour
s that better captures their role as a single entity in our program? -
Can you think of a way to "mimic" inheritance without using
extends
but using Adaptors instead?
The Interpreter Pattern
Used to capture the structure of sentences in a language and evaluate them.
Interpreter Pattern UML Class Diagram
-
Looks very close to composite pattern :)
Let’s build a small arithmetic calculator
What do we need to do in order to extend our calculator and allow for global variable, e.g.,
x = 3 + 4 y = x - 2 y
Should return 5
.
-
Why is
Define
not anOp
?-
What happens when we make it an
Op
? -
Can you think of a programming language where defining a variable is consider as an expression?
-
-
Why is
Var
a value?-
What if we make
Var
a non-value expression?
-
Pretty Print sentences of our language
Visitor Pattern
Double Dispatch
Recall from our assignment the Rock Paper Scissors game? Can we solve that problem without using
-
casting
-
instanceof
-
getClass()
-
equals()
method that we implemented which in turn uses some of the preceding language features?