What can Pacman teach us?
A super simplified version of Pacman
- Takes place in a grid
- Game is turn-based: Pacman moves
one square, then Ghosts moves one
square and so on. - Pacman and Ghosts move to an orthogal cell
- There are 4 ghosts:
Inky, Pinky, Blinky, Clyde - Inky moves directly to
where Pacman is - Pinky moves to a cell 3 spaces
ahead of where Pacman is - Blinky moves to strategic
places on the board - Clyde behaves like Blinky if he
is far from Pacman and like
Inky if he is close
- Player wins if Pacman eats all pills
- If Pacman eats a Super Pill, then all
Ghosts change their behavior and
try to move away from it - If a Ghost moves to the same cell Pacman
is, Pacman loses a life - If Pacman loses all his lives he loses
Game Board
The Game has a Board
public class Game {
private char[][] board;
}
Is that the best we can do?
What about?
public class Book {
private String authorName;
private String publicationDate;
private String ISBN;
}
What's a String?
- A sequence of Unicode characters
- It doesn't have a limit on its size
What about
Book's attributes?
- What's a name? It can contain any character? Are there reasonable expectations about its length?
- What's a publication date? It can contain any characters? Numbers are separated with: '/', '-', ' ', etc.? Are there reasonable expectations about its length?
- What's an ISBN? It can contain any characters? Are there reasonable expectations about its length?
Beware this syndrome
public class Book {
private Author author;
private YearMonth publication; // Java 8
private ISBN isbn;
}
A better design!
When you create a Type
- You can define invariants
- You don't need to validate it
when you use it as a parameter - Your model is more expressive
What about... a triangle
- A triangle has 3 angles: alpha, beta, gamma
- and 3 sides: A, B, C
- How you would define a class Triangle?
public class Triangle {
private Side[] sides = new Side[3];
private Angle[] angles = new Angle[3];
}
Is this what you were thinking?
The mark of a great
OOP developers is
that it loves Types!
Ok we need a Board
class, now what?
- A board has rows and columns, in each square there is a tile: an empty tile, a wall, a pill, a super pill
- So there are 4 kinds of tiles, but they are tiles anyway
public class Board {
private Tile[][] tile;
}
Pretty simple, right?
public interface Tile {
boolean isSolid();
}
But there is something to take into account
- Tiles are stateless
- But some of them
are repeated a lot in a board! - It would be dreamy if there
was a way to avoid a waste
of memory!
Singleton
- The most misused Design Pattern
- Don't trust people that the first Design
Pattern they can think of is Singleton :)
What is really a Singleton?
- It's a class that should only have one instance
It's there something in the Java language that is related to this idea of one-instance-only?
Enums!
What's an enum?
- An enum type is a special data type that enables for a variable to be a set of predefined constants.[1]
public enum Day {
MONDAY, TUESDAY, WEDNESDAY, THURSDAY,
FRIDAY, SATURDAY, SUNDAY;
}
If each day is a constant,
it will be a big waste to have more
than one instance of each!
How Enums are implemented in Java?
- Java creates an abstract class, e.g. Day
- Then, it creates a constant for each value, which is a concrete class that extends the abstract class!
public enum Cell {
DEAD {
@Override
public Cell nextGeneration(int aliveNeighbors) {
if (aliveNeighbors == 3) {
return ALIVE;
}
return this;
}
},
ALIVE {
@Override
public Cell nextGeneration(int aliveNeighbors) {
if (aliveNeighbors == 2 || aliveNeighbors == 3) {
return this;
}
return DEAD;
}
};
public abstract Cell nextGeneration(int aliveNeighbors);
}
The biggest problem implementing Singleton is truly ensuring that only one instance is created
If the JVM can do it for you, why you
would try to do it yourself!
But the biggest problem is understanding
when to use a Singleton
Each Tile is a
Singleton because...
- There should only be one instance of each type,
since each type is stateless
What other classes
should be Singletons?
- Services which are usually stateless
(but they have collaborators) - The default behaviour of Dependency Injection
FW's is that they always return you the same
instance of the bean you are asking for.
The key to use Singletons well
is: stateless
(or immutability)
public enum Tile {
EMPTY {
public String toString() {
return " ";
}
},
WALL {
public boolean isSolid() {
return true;
}
public String toString() {
return "#";
}
},
PILL {
public String toString() {
return ".";
}
},
SUPER_PILL {
public String toString() {
return "o";
}
};
public boolean isSolid() {
return false;
}
}
Now I got some implementations of a
given interface, how
do I instantiated them?
The obvious approach
Ship ship = new BattleShip();
Is there a
problem with new?
- new creates coupling
- classes from your model
tend to be instantiated all
over your code
But constructors are the only way to create objects, right?
Factory Method
- If you are going to know just one
Design Pattern, let it be this one! - A factory method, usually,
returns a concrete instance of
an interface but the caller doesn't
know (or care) which. - You can use parameters to
determine different implementations!
public class Ships {
public static Ship build(String type) {
switch(type) {
case "BattleShip":
return new BattleShip();
case "AirCraft":
return new Aircraft();
case "Submarine":
return new Submarine();
case "Destroyer":
return new Destroyer();
case "Patrol":
return new PatrolBoat();
default:
return null;
}
}
}
public class Ships {
public static Ship build(String type) {
switch(type) {
case "BattleShip":
return new BattleShip();
case "AirCraft":
return new Aircraft();
case "Submarine":
return new Submarine();
case "Destroyer":
return new Destroyer();
case "Patrol":
return new PatrolBoat();
default:
return null;
}
}
}
There are 2 major problems with this code!
The problem with Strings
- Is it "Battleship"?
- Is it "BATTLESHIP"?
- Is it "BattleShip"?
- Is it "battleship"?
- ...
What's better?
Enums!
Enums
- Remember, they are: A type that
represents a finite set of constants - Compiler will check the spelling
- Design is more expressive!
public enum Type {
BATTLESHIP, AIRCRAFT, SUBMARINE, DESTROYER, PATROL;
}
public class Ships {
public static Ship build(Type type) {
switch(type) {
case BATLLESHIP:
return new BattleShip();
case AIRCRAFT:
return new Aircraft();
case SUBMARINE:
return new Submarine();
case DESTROYER:
return new Destroyer();
case PATROL:
return new PatrolBoat();
default:
return null;
}
}
}
The other problem?
Returning null
- There is nothing that stands out
in the signature of a method when
you may return null - If caller doesn't do a null-check,
eventually you will get a NPE! - NPE is the billion-dollar mistake!
Why we were
returning null in
the first place?
public class Ships {
public static Ship build(Type type) {
switch(type) {
case BATLLESHIP:
return new BattleShip();
case AIRCRAFT:
return new Aircraft();
case SUBMARINE:
return new Submarine();
case DESTROYER:
return new Destroyer();
case PATROL:
return new PatrolBoat();
}
}
}
Doesn't compile! Why?
There are possible paths
of execution that
don't return a value!
You may think that because it is an enum (a finite set of constant values), there are no more possible paths!
- That's true... for now!
- If a programmer adds a new kind of ship, e.g. TransformerShip, then that code would be broke
- Remember that change is the only constant of
software development!
Let's avoid returning null!
public class Ships {
public static Ship build(Type type) {
switch(type) {
case BATLLESHIP:
return new BattleShip();
case AIRCRAFT:
return new Aircraft();
case SUBMARINE:
return new Submarine();
case DESTROYER:
return new Destroyer();
default:
return new PatrolBoat();
}
}
}
What's wrong now?
- If we add a TransformerShip and forget to update our Factory Method, we will return a PatrolBoat and that is definitely not what we want!
- We are silencing an error, we may never notice until our software goes to production!
A better approach
public class Ships {
public static Ship build(Type type) {
switch(type) {
case BATLLESHIP:
return new BattleShip();
case AIRCRAFT:
return new Aircraft();
case SUBMARINE:
return new Submarine();
case DESTROYER:
return new Destroyer();
case PATROL:
return new PatrolBoat();
default:
throw new AssertionError(String.format("Unrecognised type %s", type));
}
}
}
Use AssertionError
- To signal code that should have not been reached!
- If someone adds a new boat type and forgets to update the Factory Method the method would throw an Error which will end the application, making the error visible!
Going back to the Factory Method Design Pattern
public class Board {
Ship[] ships = { Ships.build(BATTLESHIP),
Ships.build(AIRCRAFT), Ships.build(SUBMARINE),
Ships.build(DESTROYER), Ships.build(PATROL)};
}
How code would use the Factory?
Wait! What if someone
has a different
approach to Ships?
public class Ships {
public static Ship build(Type type) {
return new BaseShip(type.getHitPoints());
}
}
Is this a better design?
- We lack context, so who knows!
- The important thing is that by using a Factory Method you decouple the creation of Ships with the code that uses, so you are free to experiment without breaking any code!
Where should a Factory Method be? And the concrete implementations?
- Experiment to find the combination
that suits your needs - Java 8 let's you have static methods
on interfaces - You can use inner classes to hide the
concrete implementation (Iterator, Path, etc.)
public enum Type {
EMPTY, WALL, PILL, SUPER_PILL;
public static Tile of(char representation) {
switch (representation) {
case ' ':
return EMPTY;
case '#':
return WALL;
case '.':
return PILL;
case 'o':
return SUPER_PILL;
default:
throw new AssertionError(String.format(
"Unsupported cell representation %s", representation));
}
}
}
Enough with
Tiles, let's go
back to Board
What's the relationship between Tile and Board?
public class Board {
private final int height;
private final int width;
private final Map<Coordinate, Tile> board;
}
What's a Coordinate?
public class Coordinate {
private final int row;
private final int column;
private Coordinate(int row, int column) {
this.row = builder.row;
this.column = builder.column;
}
public int getRow() {
return row;
}
public int getColumn() {
return column;
}
}
Wait! Is this a coordinate
in row 1 column 2 or the other way around?
Coordinate coordinate = new Coordinate(2, 1);
Let's go back to Triangle
- A Triangle has 6 attributes
- But you can create a Triangle with less
attributes and find the other ones
(using Sine and Cosine Laws) - For the sake of simplicity let's suppose
that you just need: An angle and 2 sides,
or a side and 2 angles, or 3 sides. - But of course if you have those parameters and others, you should be able to use them!
Builder Pattern
- If the creation of objects of a class
is complex, create a class to deal with it. - By having a new class, you can offer
flexibility to how to build objects of the class
without breaking SRP - If you have a constructor with many
parameters (or parameters of the same
type) you can use an inner class with "setters" - If you are going to only know 2 patterns, let
this be the second one!
public class Triangle {
private final Side[] sides;
public static class Builder {
private final Side[] sides = new Side[3];
public Builder withA(Side a) {
side[0] = a;
return this;
}
public Builder withB(Side b) {
side[1] = b;
return this;
}
public Builder withC(Side c) {
side[2] = c;
return this;
}
public Triangle build() {
return new Triangle(this);
}
}
public Triangle(Builder builder) {
this.sides = builder.sides;
}
}
Triangle t1 = new Triangle.Builder()
.withA(Side.of(3.0))
.withB(Side.of(4.0))
.withC(Side.of(5.0))
.build();
Triangle t2 = new Triangle.Builder()
.withAlpha(Angle.degrees(60.0))
.withC(Side.of(5.0))
.withBeta(Angle.degrees(60.0))
.build();
Triangle t3 = new Triangle.Builder()
.withA(Side.of(10.0))
.withGamma(Angle.degrees(90.0))
.withB(Side.of(10.0))
.build();
What about Coordinate?
public class Coordinate {
private final int row;
private final int column;
public static class Builder {
private int row;
private int column;
public Builder withRow(int row) {
this.row = row;
return this;
}
public Builder withColumn(int column) {
this.column = column;
return this;
}
public Coordinate build() {
return new Coordinate(this);
}
}
private Coordinate(Builder builder) {
this.row = builder.row;
this.column = builder.column;
}
}
Now it's not possible to confuse rows and columns
Coordinate c1 = new Coordinate.Builder()
.withRow(1)
.withColumn(2)
.build();
but is it too much verbose for
just two attributes?
Software development
is never about
"can't" and "can"
It's about compromises
They are few design decision that don't have any redeemable quality
but they exists,
e.g. returning null!
More of Coordinate
it makes sense that a coordinate may be able to list all its neighbors, let's do that!
public class Coordinate {
private static final int[] dx = {-1, 0, 0, 1};
private static final int[] dy = { 0,-1, 1, 0};
private static final int totalNeighbors = dx.length;
private final int row;
private final int column;
// more code
public List<Coordinate> neighbors() {
List<Coordinate> neighbors = new ArrayList<>(totalNeighbors);
for (int i = 0; i < totalNeighbors; ++i) {
Coordinate neighbor = new Coordinate.Builder()
.withRow(row + dx[i])
.withColumn(column + dy[i])
.build();
neighbors.add(neighbor);
}
return Collections.unmodifiableList(neighbors);
}
}
Yes I am!
Collections.unmodifiableList(neighbors);
Let's think about this:
- You want to create a Collection that can't be modified
- But you already have a lot of implementations
of a particular interface . You want to still have
those implementations but add the restriction to
not let calls to method that modify its state. - Let me rephrase that: you want to add new behaviour to an existing class without modifying it!
- That's so important that it has a name:
the Open Closed Principle
How would you do it?
- We want to take an existing
implementation of List - Change the behaviour
of some methods - Extra points if we can
do it on runtime!
public class UnmodifiableList<E> implements List<E> {
private List<E> data:
public UnmodifiableList(List<E> component) {
this.data = component;
}
@Override
public int size() { // delegated
return data.size();
}
@Override
public boolean add(E e) { // decorated
throw new UnsupportedOperationException();
}
// more code
}
Decorator Pattern
- Create an interface to define the decorators
- Each decorator takes a decorator
as argument (usually in the constructor) - For each method you want to decorate: create a method that a delegates to the original method;
write code after and / or before the call.
If you learn OOP with Java, chances are you already have used decorators
BufferedReader br =
new BufferedReader(new InputStreamReader(System.in));
BufferedReader br2 =
new BufferedReader(new FileReader("foo.txt"));
Going back to unmodifiableList
public static <T> List<T> unmodifiableList(List<? extends T> list)
It works by the magic of Polymorphism!
Try to explain that with the typical definitions of polymorphism!
Polymorphism
- In OOP languages It's the feature that let you have a reference to an object of type T, to reference any object which is under the hierarchy tree of T
List<String> data = new ArrayList<>();
Ghosts
How to model your domain
- Define new Types with an interface
(never begin its name with I) - Create an abstract class to implement
methods common to its implementations,
that way you avoid DRY! - Create concrete classes that extend
the abstract class (Never end their
name with impl)
Ghosts
- Each ghost have a different behaviour
- Some Ghosts change their behaviour
depending on some factors
(e.g. distance to Pacman) - All Ghosts change their behaviour once
Pacman eats a Super Pill! - We need to change the implementation
of a method on runtime!
Can we use the
Decorator Pattern?
- Sometimes they are more than one way to do something, take into account
the semantics! - Design Patterns become a common language, so when someone see a Decorator at first glance they will have certain expectations
- Decorator Pattern use composition and delegation to decorate an existing method
Strategy Pattern
- You want to change the behaviour
of a class in runtime - The most common example used
in the literature is if you want to
change the sorting algorithm
That's not the best example!
A naive approach to change-behavior ghosts
public Coordinate move() {
if (this.escaping) {
// code to escape
} else {
// code to move normally
}
}
What's the problem?
- Code doesn't scale well, more behaviours
mean more else path in the code which
will make ir hard to understand! - It would possibly lead to repeating
code across ghosts
Another possible approach
public class ChaserGhost {
public Coordinate move() {
// code to chase
}
}
public class Inky extends ChaserGhost {
}
What's the problem?
- In Java classes can't extend
more than one class - Inheritance is not
correct in this case!
(Favor composition
over inheritance)
What's inheritance?
- Inheritance provides a mechanism to create an specialisation of a class!
- ChaserGhost is not an specialisation of Ghost!
GenericServlet is an specialisation of Servlet.
HttpServlet is an specialisation of GenericServlet.
MyServlet is an specialisation of HttpServlet. - In our case, we are just trying to reuse code and for
that we are using inheritance; That's not inheritance!
How do you reuse functionality without inheritance
- Composition
- Delegation
- We have done something
akin with Decorator Pattern
Strategy Pattern Step 1: Define an Interface
public class GhostBehavior {
Coordinate move(Coordinate paceman, Board board);
}
Strategy Pattern Step 2: Define implementations
public class AmbushBehavior implements GhostBehavior {
@Override
public Coordinate move(Ghost ghost, Pacman pacman, Board board) {
// code to ambush
}
}
public class ChaseBehavior implements GhostBehavior {
@Override
public Coordinate move(Ghost ghost, Pacman pacman, Board board) {
// code to chase
}
}
Strategy Pattern Step 3: Changing mechanism
public abstract class CommonGhost {
private GhostBehavior behavior;
public CommonGhost(GhostBehavior behavior) {
setBehavior(behavior);
}
public Coordinate move(Pacman paceman, Board board) {
return behavior.move(paceman, board);
}
public void setBehavior(GhostBehavior behavior) {
this.behavior = Objects.requireNotNull(behavior);
}
}
Strategy Pattern Step 4
Now the Ghosts
public class Inky {
public Inky() {
super(new ChaseBehavior());
}
}
public class Binky {
public Binky() {
super(new AmbushBehavior());
}
}
The way to change behavior doesn't
need to be a setter
- There could not be one!
- There could a method
that triggers the change! - Be creative!
How we can advise the ghosts to escape Pacman?
Ghosts need Game to
tell them when Pacman eats a Super Pill
- They need to subscribe to notifications
- Game will have a list of subscribers
- When triggered, game will call a specific
method on subscribers so they can do
something meaningful - Code should decouple classes
Observer Pattern
public class Game {
private Ghost[] ghosts = {new Inky(), new Blinky,
new Pinky(), new Clyde()};
public void run() {
// code
if (tile == Tile.SUPER_PILL) {
for (Ghost ghost: ghosts) {
ghost.setBehavior(new EscapeBehavior());
}
}
// code
}
}
public class ButtonDemo extends JPanel
implements ActionListener {
protected JButton b1, b2, b3;
public ButtonDemo() {
b1 = new JButton("Disable middle button", leftButtonIcon);
b1.setActionCommand("disable");
b2 = new JButton("Middle button", middleButtonIcon);
b3 = new JButton("Enable middle button", rightButtonIcon);
//Listen for actions on buttons 1 and 3.
b1.addActionListener(this);
b3.addActionListener(this);
//Add Components to this container, using the default FlowLayout.
add(b1);
add(b2);
add(b3);
}
public void actionPerformed(ActionEvent e) {
if ("disable".equals(e.getActionCommand())) {
b2.setEnabled(false);
b1.setEnabled(false);
b3.setEnabled(true);
} else {
b2.setEnabled(true);
b1.setEnabled(true);
b3.setEnabled(false);
}
}
}
How do Ghost
go after Pacman?
- A Graph is a collection of
nodes and vertices - A vertice connects two nodes
- If the vertices have a value associated,
the graph is said to be weighted.
Otherwise the graph is unweighted.
BFS (breadth first search)
- BFS works on unweighted graphs
- The possible states of a process
can be seen as a graph - You begin with a source node
and a destination source
int bfs(Node source, Node destination) {
Queue<Coordinate> moves = new ArrayDeque<>();
Map<Coordinate, Coordinate> antecessor = new HashMap<>();
Map<Coordinate, Integer> distance = new HashMap<>();
moves.add(origin);
antecessor.put(origin, null);
distance.put(origin, 0);
while (!moves.isEmpty()) {
Node current = moves.poll();
if (current.equals(destination)) {
return distance.get(current);
}
for (Node neighbor: current.neighbors()) {
if (!antecessor.containsKey(neighbor)) {
moves.add(neighbor);
antecesor.put(neighbor, current);
distance.put(neighbor, distance.get(current) + 1);
} }
}
}
return -1;
}
With BFS
- You can chase Pacman
- You can ambush Pacman
- You can escape from Pacman
Design Patterns
Ok, seriously...
- Again, it's constrains
- Using Design Patterns you have more flexible code, what do you lose with them?
- Maybe you are doing unrequested features?
- Maybe Design Patterns is your hammer and everything looks like a nail?
Best way to learn
them is to code!
Iterate! Don't fear
erasing all your code
and staring again!
Resources
Q&A
Thanks!
@gaijinco
cobregon@hugeinc.com
What can Pacman teach us?
By Carlos Obregón
What can Pacman teach us?
A presentation that teaches some Design Patterns from a project-based approach: a super simplified version of Pacman
- 2,407