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 design tradeoffs within a small, easily-consumable domain. The ultimate goal is to determine: Is one design 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.
A 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. It's 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. 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 threadsafe operation, non-threasafe. We could always create a threadsafe wrapper using synchronization or locks.
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 reminiscient 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.
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 threadsafety, 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 threadsafety 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.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
an 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 an 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 needs to return collaborating types to maintain a "tell, don't ask" style and maintain immutability.
Is this a better option compared to just adding synchronization to Sample 1?