Up tonight I finally carry through on my threat to apply mirrors to the
factory method pattern in
Dart. I am unsure if my beloved mirrors can have their usual improving effect (in my eyes at least), but I am always happy to have an excuse to play with reflection and mirrors.
Tonight's excuse comes in the form of the language specific implementations mentioned in the
Gang of Four book. They mentioned that factory methods in Smalltalk often return the class to be created instead of an object. Aside from having to import the
dart:mirrors
library, it is not much more difficult to do the same thing in Dart.
The prime mover in the factory method pattern is the creator class. In the "game factory" example with which I have been toying, the
GameFactory
class creates a series of games between two players, then allows them chose any number of board games to play. The returns-a-class version might look something like:
// Creator
class GameFactory {
String playerOne, playerTwo;
GameFactory(this.playerOne, this.playerTwo);
// ...
// The factory method
Type boardGameClass([String game]) {
if (game == 'Checkers') return CheckersGame;
if (game == 'Thermo Nuclear War') return ThermoNuclearWar;
return ChessGame;
}
}
That will force the client code to reflect on the classes, which is an obnoxious thing to ask of client code. So instead, I import
dart:mirrors
and return a class mirror from the
boardGameClass()
method:
import 'dart:mirrors' show reflectClass, ClassMirror;
// Creator
class GameFactory {
// ...
ClassMirror boardGameClass([String game]) {
if (game == 'Checkers') return reflectClass(CheckersGame);
if (game == 'Thermo Nuclear War') return reflectClass(ThermoNuclearWar);
return reflectClass(ChessGame);
}
}
Let's take a look at the resulting client code next. When it worked directly with objects, the client code looked like:
main() {
var series = new GameFactory('Professor Falken', 'Joshua');
series.start();
var game;
game = series.createBoardGame('Checkers');
game.play();
game = series.createBoardGame('Thermo Nuclear War');
game.play();
// Defaults to a nice game of chess
game.createBoardGame();
game.play();
}
That is pretty simple: start by creating a game factory instance, then create a game / play the game, create a game / play a game, etc.
With mirrors, the code now becomes:
main() {
var series = new GameFactory('Professor Falken', 'Joshua');
series.start();
var game;
game = series.
boardGameClass('Checkers').
newInstance(new Symbol(''), []).
reflectee;
game.play();
game = series.
boardGameClass('Thermo Nuclear War').
newInstance(new Symbol(''), []).
reflectee;
game.play();
// Defaults to a nice game of chess
game = series.
boardGameClass().
newInstance(new Symbol(''), []).
reflectee;
game.play();
}
Look, I love mirrors more than just about anyone, but even I have to admit that is horrible. There is far too much noise obscuring the relatively simple intent of the code. I am not a fan of being forced to supply a constructor as a
Symbol
—it seems like the default constructor could be inferred (I'm sure there is a good reason it is not though). Similarly, I do not like being forced to supply an empty list of arguments to the
newInstance()
constructor method. But the truth is, even if both of those complaints went away, the
newInstance()
and
reflectee
calls would still make a mirror approach significantly less desirable than the plain-old object approach.
So mirrors are always undesirable in the factory method pattern, right? Well, not so fast. If there is a situation in which the client might want to create products with varying constructor arguments, then this mirror approach could come in handy. For instance, if we want to teach Joshua about thermonuclear war, we can create a alternate constructor for assigning different starting values for the two players:
class ThermoNuclearWar extends BoardGame {
List playerOnePieces = [ '1,000 warheads' ];
List playerTwoPieces = [ '1,000 warheads' ];
ThermoNuclearWar(): super();
ThermoNuclearWar.withWarheads(this.playerOnePieces, this.playerTwoPieces);
String get winner => "None";
}
The client code can then run through as many scenarios as it takes to teach that thermonuclear war is a very strange game indeed:
game = series.
boardGameClass('Thermo Nuclear War').
newInstance(new Symbol(''), []).
reflectee;
game.play();
game = series.
boardGameClass('Thermo Nuclear War').
newInstance(
new Symbol('withWarheads'),
[['1 warhead'], ['1 warhead']]
).
reflectee;
game.play();
game = series.
boardGameClass('Thermo Nuclear War').
newInstance(
new Symbol('withWarheads'),
[['1 warhead'], ['1,000 warheads']]
).
reflectee;
game.play();
This would result in output like:
$ ./bin/board_game.dart
*** Professor Falken vs. Joshua ***
ThermoNuclearWar
Player One starts with: 1,000 warheads
Player Two starts with: 1,000 warheads
--
Winner: None
ThermoNuclearWar
Player One starts with: 1 warhead
Player Two starts with: 1 warhead
--
Winner: None
ThermoNuclearWar
Player One starts with: 1 warhead
Player Two starts with: 1,000 warheads
--
Winner: None
Afterwards, the game series can then switch back to a nice game of chess:
// Defaults to a nice game of chess
game = series.
boardGameClass().
newInstance(new Symbol(''), []).
reflectee;
game.play();
So there is a reason for using mirrors in the factory method pattern after all. Yay! That said, they do not make as much sense as returning classes in Smalltalk. I will have to continue my search for Dart-specific features that can be brought to bear on the factory method pattern tomorrow.
Play with the code on DartPad: https://dartpad.dartlang.org/d2a79f59113c888d0788.
Day #88
‹prev | My Chain | next›