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.
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.
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:
- The method, when read literally, remains within the domain: "validate the placement".
- 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.
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:
- Methods gain an extra dependency, increasing coupling.
- We gain back thread-safety, as methods, e.g.
Placement::validate
, don't alter the objects that are computing the answer. - 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.
In this sample, just about everything is immutable.
I noticed two effects:
- Instead of methods coupling to a new dependency to use as a continuation, methods of different classes are coupled to returning collaborating object types.
- 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.
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.
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:
If you translate this using traditional OOP, you do something like:
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 returnnull
, but in this case, the developper must use defensive code everywhere this method is usedAn 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:
To avoid defensive code, you can use the null pattern object, but in this case, you can not detect a problem:
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:
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).