Skip to content

Instantly share code, notes, and snippets.

@TheSecretSquad
Last active July 13, 2017 09:03
Show Gist options
  • Save TheSecretSquad/d6079083c6e2ddf17a0ccba4c73b95c5 to your computer and use it in GitHub Desktop.
Save TheSecretSquad/d6079083c6e2ddf17a0ccba4c73b95c5 to your computer and use it in GitHub Desktop.
Tic Tac Toe Samples

Overview

These samples partially implement the game of tic tac toe, with specific focus on validating and placing submitted placements. The purpose of this exercise is to evaluate East Oriented code, and design tradeoffs within a small, easily-consumable domain. This is a thought experiment more than anything, so opinions are welcome.

East Oriented code hides state and implementation details allowing more declarative and domain-specific messages. It also tends toward mutable objects, because we can't query objects. Some questions to consider are:

  • Do we accept the mutability and learn to live with it as a small side-effect (pun intended) of the benefits gained from East, or do we try to keep objects immutable through some other means, while still adhering to a "tell, don't ask" style?
  • Is one of the designs more suitable than the others when considering things like coupling, cohesion, thread safety, etc.? Are they all equally valid and just a matter of personal opinion?

The samples are designed specifically to hide information within objects and to disallow getter and setter methods. The Placement object in particular, is an abstraction derived from the domain of "a player submits a placement to the game". The purpose of introducing the Placement abstraction is to a) create an abstraction that ignores details until they are needed, b) create a situation where data is hidden a layer deeper in the object graph, i.e., we have Game::submit(Placement placement) instead of Game::submit(Mark mark, Position position). It is also true that a Mark and Position are closely related, so can be passed through the system together as members of a single abstraction.

Feel free to comment on any merits or faults to the designs for any observed reasons, or personal preferences.

Note that some methods, for example the printing methods, reference classes and interfaces that are not included. The printing methods were kept to show how the objects might be printed, but the other interfaces were left out to avoid clutter.

Note also that some of the particulars of my evaluation are pointless considerations for a small application like this, but the point is to pretend we are making decisions about real code, in a real domain that requires these considerations.

Note, lastly, that objects in the examples couple to other concrete objects, but these dependencies could be broken up by interfaces. The examples don't do this to avoid clutter and make them easier to consume. In either case, the messaging patterns remain the same.

Notes on Each Sample

Sample 0 (not shown)

Sample 0 is the typical example using query methods. Full sample code is not provided here.

This would have query methods like:

bool Placement::isValid(Mark playingMark, Grid grid)

bool Mark::isValid(Mark playingMark)

bool Grid::isValid(Position position)

Using query methods exposes a boolean as an implementation detail and forces callers to rely on explicit conditional statements. Notice, however, that the queries maintain each object's immutability.

Sample 1

The typical implementation for validating a Placement would be to have a method:

bool Placement::isValid(Mark playingMark, Grid grid)

However, the goal is to eliminate state-based metaphors. If we were to read this method literally, it says "produce an integer in the range of 0 to 1 representing the validity of the placement". This is using tools outside the domain (integers/booleans) to implement a domain concept.

The solution in this sample is to tell the Placement to validate, keeping track of the result.

Placement Placement::validate(Mark playingMark, Grid grid)

The method simply returns itself instead of void, but void could have been used as well.

I noticed two effects:

  1. The method, when read literally, remains within the domain: "validate the placement".
  2. We've turned an immutable operation into a mutable one.

Item number two is the concern in this design, as pushing behavior into objects like this causes objects to tend toward mutability. We didn't really intend to change the state of the placement. Its state is its mark and position, which don't change. We really want to ask it the question "are you valid?" without altering it. While the design eliminates the state-based metaphor for the caller, we have to manage state within the object, and the caller needs to understand that this is a destructive operation. I think it's better to keep state inside an object, but managing mutability can still be troublesome. It also has the effect of making what was a thread-safe operation, non-thread-safe. We could always create a thread-safe wrapper using synchronization or locks.

// We can't send the same Placement to two games, because
// it will invalidate it for one of the games.
Game game1;
Game game2;
Placement placement = new Placement(Mark.x(), Position.topLeft());
// Unexpected results without thread synchronization
// Even if thread-safe, it still may give unexpected behavior
// because the placement needs to be in different states for each game.
// We can't have games on different threads calling placement.validate
// while another is calling placement.place.
game1.submit(placement); // Imagine these are run on separate threads
game2.submit(placement);

// We would have to be sure to pass unique instances to submit.
// Which is ok, as long as it's documented in Placement's specification.
// You would think, though, that two Placements with the same mark and position
// should be the same placement.
game1.submit(new Placement(Mark.x(), Position.TopLeft()));
game2.submit(new Placement(Mark.x(), Position.TopLeft()));

Note that the methods:

Mark Mark::validate(Mark playingMark, Placement placement)

and

Grid Grid::validate(Position position, Placement placement)

are immutable operations because the objects don't change state. They pass their validation result to the placement object. Sample 2 takes this concept further by passing more objects as continuations.

Sample 2

This sample is reminiscent of Continuation-Passing Style (CPS). Results are not returned and objects don't change state. Instead, the results of operations are sent to an object passed in the initial method call specifically to accept the result. This object in turn does the same.

In this sample, we accept that the Game will change state. We intend for the game to change state by calling the mutator:

Game Game::submit(Placement placement)

We also accept that the Grid will change state when we finally place a valid Placement on it (they change state when we intend for them to change state).

The difference is operations that are simply computing an answer to a question, that shouldn't change any object's state, don't change those objects while they are computing.

I noticed three effects:

  1. Methods gain an extra dependency, increasing coupling.
  2. We gain back thread-safety, as methods, e.g. Placement::validate, don't alter the objects that are computing the answer.
  3. We have more methods to coordinate communication.

The question here is: Are the extra dependencies and coupling worth it?

The thread-safety would allow a developer to reuse the Placement to validate multiple games, without having to worry about the Placement's state changing.

// We accept that the Game will capture the end result of our continuations,
// eventually changing state, but nothing else (placement, mark, or grid) will.
Game game1;
Game game2;

Placement placement = new Placement(Mark.x(), Position.TopLeft());
// Not a problem to share the placement
game1.submit(placement);
game2.submit(placement);

// The Game objects will each effectively call
placement.validate(aPlayingMark, aGrid, game1)
placement.validate(aPlayingMark, aGrid, game2)

You'll notice a few extra methods on the Mark, Grid, and Placement to coordinate the process of validating and continuing. I also included symmetric methods on the Mark and Grid. The reason can be seen from the implementation of

Placement Placement::validate(Mark playingMark, Grid grid, Game game) {
    mark.validateAndContinue(playingMark, this, grid, game);
    return this;
}

Here we ask the Mark to initiate the validation, but only implementing this would preclude a developer from validating by starting with the Grid first. So, the validateAndContinue methods on Grid and Mark begin the validation and continue until one of their validate methods are called.

This adds back the flexibility we had in Sample 1, where we could implement Placement::validate as:

(method implementation inlined for clarity)

public Placement Placement::validate(Mark playingMark, Grid grid) {
    mark.validate(mark, this);
    grid.validate(position, this);
}

or

public Placement Placement::validate(Mark playingMark, Grid grid) {
    grid.validate(position, this);
    mark.validate(mark, this);
}

because Placement collects the results.

The difference is it requires a few extra methods in Sample 2 to do this.

Ultimately, I wonder if Sample 2's communication pattern is too confusing to follow easily.

Sample 3

In this sample, just about everything is immutable.

I noticed two effects:

  1. Instead of methods coupling to a new dependency to use as a continuation, methods of different classes are coupled to returning collaborating object types.
  2. Overhead of creating lots of new objects.

In this case, methods need to return collaborating types to maintain a "tell, don't ask" style and maintain immutability, giving it similar coupling issues as Sample 2.

Sample 4

This sample is inspired by a comment from @mageekguy, as well as his companion project here https://github.com/estvoyage/ticTacToe. What is interesting about this design is that it maintains the immutability of objects like Game and Grid, but instead of returning modified objects, it forwards the modified objects to a receiver, in the case of the Grid::place method, which would normally be a mutator, or producer (producing a modified result and leaving the original unmodified), it now forwards the new Grid back to the Game:

public Grid place(Mark mark, Position position, Game game) {
    game.gridUpdated(new Grid(newPositionsWith(mark, position)));
    return this;
}

The Game::gridUpdated(Grid newGrid) method works similarly. In @mageekguy's comment, he used the idea of a Gamer. I chose to go with a Player, being a proxy for a player (or a proxy for the interface a real player is using to read the state of the game and interact with the game).

The messaging relationship between the Game and Player in this sample requires that the player is in some control of when it takes its turn, so I had to rework the test placement submission in Main. Since there is no artificial intelligence here, I have the very obvious PlannedPlacements::iAmNotAnAISoDeferNextPlayToPlan(Game game) method. The PlannedPlacements object just runs through the same test script as the other samples until there are no more placements. This is designed this way to allow forcing incorrect and out of order placements in the test.

In this sample, the only object changing state is the ConsolePlayer, which is at the edge of the application, near the user interface.

// Game
public class Game {
private final Grid grid;
private final PlacementError placementError;
private Mark playingMark;
public Game(PlacementError placementError) {
grid = new Grid();
playingMark = Mark.x();
this.placementError = placementError;
}
public Game submit(Placement placement) {
return validate(placement)
.placePlacement(placement)
.switchPlayers();
}
private Game validate(Placement placement) {
placement.validate(playingMark, grid);
return this;
}
private Game placePlacement(Placement placement) {
placement.place(this, placementError);
return this;
}
public Game place(Placement placement) {
placement.place(grid);
return this;
}
private Game switchPlayers() {
playingMark = playingMark.opponent();
return this;
}
public Game printOn(GameMedia gameMedia) {
grid.printOn(gameMedia);
return this;
}
}
// Placement
public class Placement {
private final Mark mark;
private final Position position;
private boolean canPlayMark;
private boolean canPlayPosition;
public Placement(Mark mark, Position position) {
this.mark = mark;
this.position = position;
canPlayMark = false;
canPlayPosition = false;
}
public Placement validate(Mark playingMark, Grid grid) {
return canPlayMark(playingMark).canPlayPosition(grid);
}
private Placement canPlayMark(Mark playingMark) {
mark.validate(playingMark, this);
return this;
}
private Placement canPlayPosition(Grid grid) {
grid.validate(position, this);
return this;
}
public Placement validMark() {
canPlayMark = true;
return this;
}
public Placement validPosition() {
canPlayPosition = true;
return this;
}
public Placement place(Game game, PlacementError placementError) {
if(canPlayMark && canPlayPosition)
game.place(this);
else
placementError.report(this);
return this;
}
public Placement place(Grid grid) {
grid.place(mark, position);
return this;
}
public Placement printOn(PlacementMedia placementMedia) {
placementMedia.printMarkAndPosition(mark, position);
return this;
}
}
// Mark
public class Mark {
private static Mark X = new Mark("X");
private static Mark O = new Mark("O");
private static Mark Empty = new Mark(" ");
private final String symbol;
private Mark(String symbol) {
this.symbol = symbol;
}
public void printOn(MarkMedia markMedia) {
markMedia.printMark(symbol);
}
public static Mark x() {
return Mark.X;
}
public static Mark o() {
return Mark.O;
}
public static Mark empty() {
return Mark.Empty;
}
public Mark opponent() {
return this.equals(x()) ? Mark.o() : Mark.x();
}
public Mark validate(Mark playingMark, Placement placement) {
if(this.equals(playingMark)) {
placement.validMark();
}
return this;
}
}
// Grid
public class Grid {
private final HashMap<Position, Mark> gridPositions;
private final Mark nullMark;
public Grid() {
this.nullMark = Mark.empty();
gridPositions = new HashMap<Position, Mark>();
}
public Grid validate(Position position, Placement placement) {
if(isPositionAvailable(position)) {
placement.validPosition();
}
return this;
}
private boolean isPositionAvailable(Position position) {
return !gridPositions.containsKey(position) && gridPositions.get(position) == null;
}
public Grid place(Mark mark, Position position) {
gridPositions.put(position, mark);
return this;
}
public Grid printOn(GridMedia gridMedia) {
gridMedia.printRow(markAt(Position.TopLeft()), markAt(Position.TopCenter()), markAt(Position.TopRight()));
gridMedia.printRowDivider();
gridMedia.printRow(markAt(Position.MiddleLeft()), markAt(Position.MiddleCenter()), markAt(Position.MiddleRight()));
gridMedia.printRowDivider();
gridMedia.printRow(markAt(Position.BottomLeft()), markAt(Position.BottomCenter()), markAt(Position.BottomRight()));
gridMedia.printBreak();
return this;
}
private Mark markAt(Position position) {
Mark mark = gridPositions.get(position);
return mark != null ? mark : nullMark;
}
}
// Main
public class Main {
public static void main(String[] args) {
new Main().run();
}
private void run() {
ConsoleGameMedia consoleGameMedia = new ConsoleGameMedia();
Game game = new Game(new ConsolePlacementError());
game.submit(new Placement(Mark.x(), Position.TopLeft()));
game.printOn(consoleGameMedia);
game.submit(new Placement(Mark.o(), Position.MiddleCenter()));
game.printOn(consoleGameMedia);
game.submit(new Placement(Mark.o(), Position.TopRight())); // Try out of turn player, valid position
game.printOn(consoleGameMedia);
game.submit(new Placement(Mark.x(), Position.TopLeft())); // Try correct player, invalid position
game.printOn(consoleGameMedia);
}
}
// Output
X | |
---+---+---
| |
---+---+---
| |
X | |
---+---+---
| O |
---+---+---
| |
Invalid placement: O at TR
X | |
---+---+---
| O |
---+---+---
| |
Invalid placement: X at TL
X | |
---+---+---
| O |
---+---+---
| |
// Game
public class Game {
private final Grid grid;
private final PlacementError placementError;
private Mark playingMark;
public Game(PlacementError placementError) {
grid = new Grid();
playingMark = Mark.x();
this.placementError = placementError;
}
public Game submit(Placement placement) {
placement.validate(playingMark, grid, this);
return this;
}
public Game validPlacement(Placement placement) {
place(placement).switchPlayers();
return this;
}
private Game place(Placement placement) {
placement.place(grid);
return this;
}
public Game invalidPlacement(Placement placement) {
placementError.report(placement);
return this;
}
private Game switchPlayers() {
playingMark = playingMark.opponent();
return this;
}
public Game printOn(GameMedia gameMedia) {
grid.printOn(gameMedia);
return this;
}
}
// Placement
public class Placement {
private final Mark mark;
private final Position position;
public Placement(Mark mark, Position position) {
this.mark = mark;
this.position = position;
}
public Placement validate(Mark playingMark, Grid grid, Game game) {
mark.validateAndContinue(playingMark, this, grid, game);
return this;
}
public Placement validMarkContinueWith(Grid grid, Game game) {
grid.validate(position, this, game);
return this;
}
public Placement validPositionContinueWith(Mark playingMark, Game game) {
mark.validate(playingMark, this, game);
return this;
}
public Placement validPosition(Game game) {
game.validPlacement(this);
return this;
}
public Placement validMark(Game game) {
game.validPlacement(this);
return this;
}
public Placement place(Grid grid) {
grid.place(mark, position);
return this;
}
public Placement printOn(PlacementMedia placementMedia) {
placementMedia.printMarkAndPosition(mark, position);
return this;
}
}
// Mark
public class Mark {
private static Mark X = new Mark("X");
private static Mark O = new Mark("O");
private static Mark Empty = new Mark(" ");
private final String symbol;
private Mark(String symbol) {
this.symbol = symbol;
}
public void printOn(MarkMedia markMedia) {
markMedia.printMark(symbol);
}
public static Mark x() {
return Mark.X;
}
public static Mark o() {
return Mark.O;
}
public static Mark empty() {
return Mark.Empty;
}
public Mark opponent() {
return this.equals(x()) ? Mark.o() : Mark.x();
}
public Mark validateAndContinue(Mark playingMark, Placement placement, Grid grid, Game game) {
if(this.equals(playingMark)) {
placement.validMarkContinueWith(grid, game);
}
else {
game.invalidPlacement(placement);
}
return this;
}
public Mark validate(Mark playingMark, Placement placement, Game game) {
if(this.equals(playingMark)) {
placement.validMark(game);
}
else {
game.invalidPlacement(placement);
}
return this;
}
}
// Grid
public class Grid {
private final HashMap<Position, Mark> gridPositions;
private final Mark nullMark;
public Grid() {
this.nullMark = Mark.empty();
gridPositions = new HashMap<Position, Mark>();
}
public Grid validate(Position position, Placement placement, Game game) {
if(isPositionAvailable(position)) {
placement.validPosition(game);
}
else {
game.invalidPlacement(placement);
}
return this;
}
public Grid validateAndContinue(Position position, Placement placement, Mark playingMark, Game game) {
if(isPositionAvailable(position)) {
placement.validPositionContinueWith(playingMark, game);
}
else {
game.invalidPlacement(placement);
}
return this;
}
private boolean isPositionAvailable(Position position) {
return !gridPositions.containsKey(position) && gridPositions.get(position) == null;
}
public Grid place(Mark mark, Position position) {
gridPositions.put(position, mark);
return this;
}
public Grid printOn(GridMedia gridMedia) {
gridMedia.printRow(markAt(Position.TopLeft()), markAt(Position.TopCenter()), markAt(Position.TopRight()));
gridMedia.printRowDivider();
gridMedia.printRow(markAt(Position.MiddleLeft()), markAt(Position.MiddleCenter()), markAt(Position.MiddleRight()));
gridMedia.printRowDivider();
gridMedia.printRow(markAt(Position.BottomLeft()), markAt(Position.BottomCenter()), markAt(Position.BottomRight()));
gridMedia.printBreak();
return this;
}
private Mark markAt(Position position) {
Mark mark = gridPositions.get(position);
return mark != null ? mark : nullMark;
}
}
// Main
public class Main {
public static void main(String[] args) {
new Main().run();
}
private void run() {
ConsoleGameMedia consoleGameMedia = new ConsoleGameMedia();
Game game = new Game(new ConsolePlacementError());
game.submit(new Placement(Mark.x(), Position.TopLeft()));
game.printOn(consoleGameMedia);
game.submit(new Placement(Mark.o(), Position.MiddleCenter()));
game.printOn(consoleGameMedia);
game.submit(new Placement(Mark.o(), Position.TopRight())); // Try out of turn player, valid position
game.printOn(consoleGameMedia);
game.submit(new Placement(Mark.x(), Position.TopLeft())); // Try correct player, invalid position
game.printOn(consoleGameMedia);
}
}
// Output
X | |
---+---+---
| |
---+---+---
| |
X | |
---+---+---
| O |
---+---+---
| |
Invalid placement: O at TR
X | |
---+---+---
| O |
---+---+---
| |
Invalid placement: X at TL
X | |
---+---+---
| O |
---+---+---
| |
// Game
public class Game {
private final Grid grid;
private final PlacementError placementError;
private final Mark playingMark;
public Game(PlacementError placementError) {
grid = new Grid();
playingMark = Mark.x();
this.placementError = placementError;
}
private Game(Grid grid, Mark playingMark, PlacementError placementError) {
this.grid = grid;
this.playingMark = playingMark;
this.placementError = placementError;
}
public Game submit(Placement placement) {
placement = placement.validated(playingMark, grid);
return placement.placed(this, placementError);
}
public Game withPlaced(Placement placement) {
Grid placedGrid = grid.withPlacement(placement);
return new Game(placedGrid, playingMark.opponent(), placementError);
}
public Game printOn(GameMedia gameMedia) {
grid.printOn(gameMedia);
return this;
}
}
// Placement
public class Placement {
private final Mark mark;
private final Position position;
private final boolean canPlayMark;
private final boolean canPlayPosition;
public Placement(Mark mark, Position position) {
this(mark, position, false, false);
}
private Placement(Mark mark, Position position, boolean canPlayMark, boolean canPlayPosition) {
this.mark = mark;
this.position = position;
this.canPlayMark = canPlayMark;
this.canPlayPosition = canPlayPosition;
}
public Placement validated(Mark playingMark, Grid grid) {
Placement validatedPlacement = this;
validatedPlacement = mark.validatedPlacement(playingMark, validatedPlacement);
validatedPlacement = grid.validatedPlacement(position, validatedPlacement);
return validatedPlacement;
}
public Placement withValidMark() {
return new Placement(mark, position, true, canPlayPosition);
}
public Placement withValidPosition() {
return new Placement(mark, position, canPlayMark, true);
}
public Game placed(Game game, PlacementError placementError) {
if(canPlayMark && canPlayPosition)
return game.withPlaced(this);
placementError.report(this);
return game;
}
public Grid placedGrid(Grid grid) {
return grid.with(mark, position);
}
public Placement printOn(PlacementMedia placementMedia) {
placementMedia.printMarkAndPosition(mark, position);
return this;
}
}
// Mark
public class Mark {
private static Mark X = new Mark("X");
private static Mark O = new Mark("O");
private static Mark Empty = new Mark(" ");
private final String symbol;
private Mark(String symbol) {
this.symbol = symbol;
}
public void printOn(MarkMedia markMedia) {
markMedia.printMark(symbol);
}
public static Mark x() {
return Mark.X;
}
public static Mark o() {
return Mark.O;
}
public static Mark empty() {
return Mark.Empty;
}
public Mark opponent() {
return this.equals(x()) ? Mark.o() : Mark.x();
}
public Placement validatedPlacement(Mark playingMark, Placement placement) {
if(this.equals(playingMark)) {
return placement.withValidMark();
}
return placement;
}
}
// Grid
public class Grid {
private final HashMap<Position, Mark> gridPositions;
private final Mark nullMark;
public Grid() {
this(new HashMap<Position, Mark>());
}
private Grid(HashMap<Position, Mark> gridPositions) {
this.nullMark = Mark.empty();
this.gridPositions = gridPositions;
}
public Placement validatedPlacement(Position position, Placement placement) {
if(isPositionAvailable(position)) {
return placement.withValidPosition();
}
return placement;
}
public Grid withPlacement(Placement placement) {
return placement.placedGrid(this);
}
private boolean isPositionAvailable(Position position) {
return !gridPositions.containsKey(position) && gridPositions.get(position) == null;
}
public Grid with(Mark mark, Position position) {
HashMap<Position, Mark> newGridPositions = new HashMap<Position, Mark>(gridPositions);
newGridPositions.put(position, mark);
return new Grid(newGridPositions);
}
public Grid printOn(GridMedia gridMedia) {
gridMedia.printRow(markAt(Position.TopLeft()), markAt(Position.TopCenter()), markAt(Position.TopRight()));
gridMedia.printRowDivider();
gridMedia.printRow(markAt(Position.MiddleLeft()), markAt(Position.MiddleCenter()), markAt(Position.MiddleRight()));
gridMedia.printRowDivider();
gridMedia.printRow(markAt(Position.BottomLeft()), markAt(Position.BottomCenter()), markAt(Position.BottomRight()));
gridMedia.printBreak();
return this;
}
private Mark markAt(Position position) {
Mark mark = gridPositions.get(position);
return mark != null ? mark : nullMark;
}
}
// Main
public class Main {
public static void main(String[] args) {
new Main().run();
}
private void run() {
ConsoleGameMedia consoleGameMedia = new ConsoleGameMedia();
Game game = new Game(new ConsolePlacementError());
game = game.submit(new Placement(Mark.x(), Position.TopLeft()));
game.printOn(consoleGameMedia);
game = game.submit(new Placement(Mark.o(), Position.MiddleCenter()));
game.printOn(consoleGameMedia);
game = game.submit(new Placement(Mark.o(), Position.TopRight())); // Try out of turn player, valid position
game.printOn(consoleGameMedia);
game = game.submit(new Placement(Mark.x(), Position.TopLeft())); // Try correct player, invalid position
game.printOn(consoleGameMedia);
}
}
// Output
X | |
---+---+---
| |
---+---+---
| |
X | |
---+---+---
| O |
---+---+---
| |
Invalid placement: O at TR
X | |
---+---+---
| O |
---+---+---
| |
Invalid placement: X at TL
X | |
---+---+---
| O |
---+---+---
| |
// Game
public class Game {
private final Grid grid;
private final Map<Mark, Player> players;
private final Mark playingMark;
public Game(Map<Mark, Player> players) {
this(new Grid(), players, Mark.x());
}
public Game(Grid grid, Map<Mark, Player> players, Mark playingMark) {
this.grid = grid;
this.playingMark = playingMark;
this.players = players;
}
public Game submit(Placement placement) {
placement.validate(playingMark, grid, this);
return this;
}
public Game validPlacement(Placement placement) {
placement.place(grid, this);
return this;
}
public Game gridUpdated(Grid newGrid) {
Mark nextPlayingMark = playingMark.opponent();
Game newGame = new Game(newGrid, players, nextPlayingMark);
for(Player p : players.values())
p.updatedGameWithCurrentPlayer(newGame, players.get(nextPlayingMark));
return this;
}
public Game invalidPlacementByMark(Placement placement, Mark mark) {
players.get(mark).invalidPlacement(placement);
return this;
}
public Game printOn(GameMedia gameMedia) {
grid.printOn(gameMedia);
return this;
}
public Game start() {
players.get(playingMark).start(this);
return this;
}
}
// Placement
public class Placement {
private final Mark mark;
private final Position position;
public Placement(Mark mark, Position position) {
this.mark = mark;
this.position = position;
}
public Placement validate(Mark playingMark, Grid grid, Game game) {
mark.validateAndContinue(playingMark, this, grid, game);
return this;
}
public Placement validMarkContinueWith(Grid grid, Game game) {
grid.validate(position, this, game);
return this;
}
public Placement validPositionContinueWith(Mark playingMark, Game game) {
mark.validate(playingMark, this, game);
return this;
}
public Placement validPosition(Game game) {
game.validPlacement(this);
return this;
}
public Placement invalidPosition(Game game) {
game.invalidPlacementByMark(this, mark);
return this;
}
public Placement validMark(Game game) {
game.validPlacement(this);
return this;
}
public Placement invalidMark(Game game) {
game.invalidPlacementByMark(this, mark);
return this;
}
public Placement place(Grid grid, Game game) {
grid.place(mark, position, game);
return this;
}
public Placement printOn(PlacementMedia placementMedia) {
placementMedia.printMarkAndPosition(mark, position);
return this;
}
}
// Mark
public class Mark {
private static Mark X = new Mark("X");
private static Mark O = new Mark("O");
private static Mark Empty = new Mark(" ");
private final String symbol;
private Mark(String symbol) {
this.symbol = symbol;
}
public void printOn(MarkMedia markMedia) {
markMedia.printMark(symbol);
}
public static Mark x() {
return Mark.X;
}
public static Mark o() {
return Mark.O;
}
public static Mark empty() {
return Mark.Empty;
}
public Mark opponent() {
return this.equals(x()) ? Mark.o() : Mark.x();
}
public Mark validateAndContinue(Mark playingMark, Placement placement, Grid grid, Game game) {
if(this.equals(playingMark)) {
placement.validMarkContinueWith(grid, game);
}
else {
placement.invalidMark(game);
}
return this;
}
public Mark validate(Mark playingMark, Placement placement, Game game) {
if(this.equals(playingMark)) {
placement.validMark(game);
}
else {
placement.invalidMark(game);
}
return this;
}
}
// Grid
public class Grid {
private final HashMap<Position, Mark> gridPositions;
private final Mark nullMark;
public Grid() {
this(new HashMap<Position, Mark>());
}
public Grid(HashMap<Position, Mark> gridPositions) {
this.nullMark = Mark.empty();
this.gridPositions = gridPositions;
}
public Grid validate(Position position, Placement placement, Game game) {
if(isPositionAvailable(position)) {
placement.validPosition(game);
}
else {
placement.invalidPosition(game);
}
return this;
}
public Grid validateAndContinue(Position position, Placement placement, Mark playingMark, Game game) {
if(isPositionAvailable(position)) {
placement.validPositionContinueWith(playingMark, game);
}
else {
placement.invalidPosition(game);
}
return this;
}
private boolean isPositionAvailable(Position position) {
return !gridPositions.containsKey(position) && gridPositions.get(position) == null;
}
public Grid place(Mark mark, Position position, Game game) {
game.gridUpdated(new Grid(newPositionsWith(mark, position)));
return this;
}
private HashMap<Position, Mark> newPositionsWith(Mark mark, Position position) {
HashMap<Position, Mark> newPositions = new HashMap<Position, Mark>(gridPositions);
newPositions.put(position, mark);
return newPositions;
}
public Grid printOn(GridMedia gridMedia) {
gridMedia.printRow(markAt(Position.TopLeft()), markAt(Position.TopCenter()), markAt(Position.TopRight()));
gridMedia.printRowDivider();
gridMedia.printRow(markAt(Position.MiddleLeft()), markAt(Position.MiddleCenter()), markAt(Position.MiddleRight()));
gridMedia.printRowDivider();
gridMedia.printRow(markAt(Position.BottomLeft()), markAt(Position.BottomCenter()), markAt(Position.BottomRight()));
gridMedia.printBreak();
return this;
}
private Mark markAt(Position position) {
Mark mark = gridPositions.get(position);
return mark != null ? mark : nullMark;
}
}
// ConsolePlayer
public class ConsolePlayer implements PlacementMedia, MarkMedia, PositionMedia, Player {
private final GameMedia gameMedia;
private String symbol;
private String position;
private PlannedPlacements plannedPlacements;
public ConsolePlayer(PlannedPlacements plannedPlacements) {
this.plannedPlacements = plannedPlacements;
gameMedia = new ConsoleGameMedia();
}
@Override
public void invalidPlacement(Placement placement) {
placement.printOn(this);
System.out.printf("Invalid placement: %s at %s", symbol, position);
System.out.println();
System.out.println();
}
@Override
public void printMarkAndPosition(Mark mark, Position position) {
mark.printOn(this);
position.printOn(this);
}
@Override
public void printPosition(String position) {
this.position = position;
}
@Override
public void printMark(String symbol) {
this.symbol = symbol;
}
@Override
public void updatedGameWithCurrentPlayer(Game newGame, Player player) {
newGame.printOn(gameMedia);
plannedPlacements.iAmNotAnAISoDeferNextPlayToPlan(newGame);
}
@Override
public void start(Game game) {
plannedPlacements.iAmNotAnAISoDeferNextPlayToPlan(game);
}
}
// Main
public class Main {
public static void main(String[] args) {
new Main().run();
}
private final Game game;
public Main() {
Map<Mark, Player> players = new HashMap<Mark, Player>();
PlannedPlacements plannedPlacements = new PlannedPlacements();
players.put(Mark.x(), new ConsolePlayer(plannedPlacements));
players.put(Mark.o(), new ConsolePlayer(plannedPlacements));
this.game = new Game(players);
}
private void run() {
game.start();
}
public static class PlannedPlacements {
private final LinkedList<Placement> plannedPlacements;
public PlannedPlacements() {
plannedPlacements = new LinkedList<Placement>();
plannedPlacements.add(new Placement(Mark.x(), Position.TopLeft()));
plannedPlacements.add(new Placement(Mark.o(), Position.MiddleCenter()));
plannedPlacements.add(new Placement(Mark.o(), Position.TopRight())); // Try out of turn player, valid position
plannedPlacements.add(new Placement(Mark.x(), Position.TopLeft())); // Try correct player, invalid position
}
public void iAmNotAnAISoDeferNextPlayToPlan(Game game) {
if(plannedPlacements.isEmpty())
System.exit(0);
game.submit(plannedPlacements.pop());
}
}
}
// Output
X | |
---+---+---
| |
---+---+---
| |
X | |
---+---+---
| O |
---+---+---
| |
Invalid placement: O at TR
X | |
---+---+---
| O |
---+---+---
| |
Invalid placement: X at TL
X | |
---+---+---
| |
---+---+---
| |
@mageekguy
Copy link

mageekguy commented Jul 2, 2017

Hi Peter!
Firstly, VERY interesting work, i will do the same thing since several month, but no time to do it seriously.
So, a big thanks for your job.
Secondly, i think that there is a missing sample, aka "we use messages to communicate between each actors".
In my opinion, we have two actors, aka the gamer and the board.
So, the board must accept coordinates from the gamer, and the gamer must accept feedback from the board ("you win", "you lose", "i'm currently in this state" and maybe some other messages related to errors like invalid coordinate, but we don't use them for now).
If we translate these messages as interface, we have:

<?php

interface board
{
   function gamersAre(gamer $aGamer, gamer $anAnotherGamer);
   function coordinateFromUserIs(gamer $gamer, coordinates $coordinates);
}

interface gamer
{
   function runningBoardIs(board $board);
   function lostBoardIs(board $board);
   function wonBoardIs(board $board);
}

We can now define some scenario based upon these messages:

$O is an instance of a class which implements gamer interface
$X is an instance of a class which implements gamer interface
$board is an instance of a class which implements board interface
$board receive the message $board->gamersAre($O, $X);
$board says to $O: $O->runningBoardIs($board);
$O says to $board: $board->coordinateFromGamerIs($O, new coordinate\x3(0,0));
$board says to $X: $X->runningBoardIs($board);
$X says to $board: $board->coordinateFromGamerIs($X, new coordinate\x3(0,1));
$board says to $O: $O->runningBoardIs($board);
$O says to $board: $board->coordinateFromGamerIs($O, new coordinate\x3(1,1));
$board says to $X: $X->runningBoardIs($board);
$X says to $board: $board->coordinateFromGamerIs($X, new coordinate\x3(1,0));
$board says to $O: $O->runningBoardIs($board);
$O says to $board: $board->coordinateFromGamerIs($O, new coordinate\x3(2,2));
$board says to $O: $O->wonBoardIs($board);
$board says to $X: $X->lostBoardIs($board);

Are you ok with that?

@mageekguy
Copy link

mageekguy commented Jul 13, 2017

What is the interest of east programming? the receiver takes control and he's free to react… or not!

In real life, then you receive a message, as a postcard for example, you have several options:

  1. Ignoring it (just putting it on your fridge and never answer to the sender) ;
  2. Replying to the sender using its own language ;
  3. Having some problems due to a degraded postcard (maybe the rain clear message on it, for example, or, if the receiver is a postman, the address is invalid).

If you translate this using traditional OOP, you do something like:

class sender
{
   function getPostcard() :\sender\postcard
   {
       …
   }
}

class receiver
{
    function newPostcard(\sender\postcard $postcard) :\postcard\receiver\answer
    {
        …
    }
}

$answer = $receiver->newPoscard($sender->getPostcard());

In this case, the sender has the control, i.e. the receiver MUST use its language, because it must handle the \sender\postalCard type.
So, if the sender must handle an another type of postal card because he must interact with a completely different type of sender (i.e a sender which not inherit from \sender, a new method is mandatory.
Moreover, the receiver must react to the postal card, because the method newPostalCard() must return an instance of \sender\postalCard\answer.
But… how to ignore the postcard, if the method newPostcard() must return an instance of \postcard\receiver\answer?
And…how to handle the a problem generated by the postcard?
The method newPoscard() can return null, but in this case, the developper must use defensive code everywhere this method is used

if (($answer = $receiver->newPoscard($sender->getPostcard()) === null)
{
   // do something here to handle the problem
}

An another solution is to throw an exception, but same problem, the developper must use defensive code, and moreover, there is an another coupling between sender and receiver via the exception type:

try
{
   $receiver->newPoscard($sender->getPostcard());
}
catch (\postcard\receiver\exception\ignored $exception)
{
   // do something here to handle ignored postcard
}
catch (\postcard\receiver\exception\noMessage $exception)
{
   // do something here to handle the problem
}

To avoid defensive code, you can use the null pattern object, but in this case, you can not detect a problem:

$receiver->newPoscard($sender->getPostcard())->doSomethingHereWithAnswer();

Or, you can return a specific instance of \postcard\receiver\answer to manage the problem, but maybe the receiver can not have access to some informations to build the answer (and it's a little bit crappy, in my opinion).

With traditional OOP, sender has control, and there is a strong coupling between the sender and the receiver, via the return type of sender::getPostcard() and the eventual exceptions that the method can throw (and these types can vary between implementation…).

With east oriented OOP, the receiver has the control, because it's the receiver which define the communication protocol between him and the sender:

class sender
{
   function receiverOfPostcardIs(receiver $receiver) :void
   {
       …
   }
}

class receiver
{
    function newPostcard(\receiver\postcard $postcard) :void
    {
        …
    }
}

So, because the receiver has control and defined communication's rules, all is possible, because he has no dependency with the sender, he is a standalone cell which can react to messages independently from its environment.
The receiver may not react to the postcard, because newPostcard() has no return type.
However, the receiver can send a message to postcard instance to have the sender address (but maybe there is no sender address on the postcard…) or to some other objects defined as one of its attribute via constructor.
So, it's possible to say that east oriented OOP code respect the Dependency inversion principle of SOLID (and all over design principle of SOLID, but it's an another story).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment