Today I continue my exploration of the Factory Method for Design Patterns in Dart. The pattern is a simple enough pattern and I was able to find a ready example in some my own Dart code last night.
Today I would like to explore the specific implementation of the creator class in the pattern. The canonical example from the Gang of Four book is similar that that I used last night from Hipster MVC. The creator class is abstract, expecting the subclass to define the method that creates a
HipsterModel
instance:abstract class HipsterCollection extends IterableBase { HipsterModel modelMaker(attrs); String get url; // ... }A example of concrete creator class comes from Dart Comics, which defines the factory method as:
class Comics extends HipsterCollection { modelMaker(attrs) => new ComicBook(attrs); get url => '/comics'; }That is simple enough and a nice example of the pattern. It is especially nice that the example comes from a simpleton's MVC framework, which ought to be accessible for my intended audience.
When I first created the Hipster MVC framework, Dart was still in its infancy and there was no way to store and use classes at run-time. The dynamic language part of me was more than a little disappointed, but the Factory Method pattern eased my concerns. A little.
Since then, Dart has evolved quite a bit—to the point that is has a
dart:mirrors
package built into the core language library. Tonight, I would like to use that package to implement the Factory Method pattern using an alternate approach given by the Gang of Four—by storing the class information in the subclass and having the baseclass create new instances from that information.The concrete creator class then becomes something like:
class Comics extends HipsterCollection { // modelMaker(attrs) => new ComicBook(attrs); Type get modelClass => ComicBook; get url => '/comics'; }I like that very much. Perhaps I am too much a dynamic language person at heart, but returning just the class feels better.
There is a trade-off, especially in Dart. The resulting abstract creator gets a little more complicated. There is not much left that is abstract—just the concrete instance of
HipsterModel
that will be used to create instance in the collection. The added complexity comes in the form the no-longer-abstract modelMaker()
, which has to obtain a class mirror of the type, and then use that to get an instance of the particular class:import 'dart:mirrors'; // ... abstract class HipsterCollection extends IterableBase { Type get modelClass; HipsterModel modelMaker(attrs) { ClassMirror modelMirror = reflectClass(modelClass); return modelMirror.newInstance(const Symbol(''), [attrs]).reflectee; } String get url; // ... }OK, that's not too horrible, but it still requires explanation whereas the abstract Factory Method approach did not.
Before jumping into the explanation, there is still a little more cleanup possible. Now that the creator method is in the baseclass, the concrete class no longer needs getter methods. Instead, I can use plain-old instance variables for things like the
modelClass
:class Comics extends HipsterCollection { Type modelClass = ComicBook; String url = '/comics'; }The base constructor class then looks like:
abstract class HipsterCollection extends IterableBase { // ... Type modelClass; HipsterModel modelMaker(attrs) { ClassMirror modelMirror = reflectClass(modelClass); return modelMirror.newInstance(const Symbol(''), [attrs]).reflectee; } String url; // ... }That helps, especially since this approach still has a messy
modelMaker()
factory method. This approach reflects on the current modelClass
, which comes from a concrete subclass of HipsterCollection
like Comics
. In this case, it reflects on the ComicBook
class to get a class mirror of that class. To get an instance from that mirror, I use the newInstance()
method. That does not return a new instance, but rather an instance mirror. To get the actual instance, I go through the reflectee
property.That explanation is relatively clean. The noise comes from the two arguments to the
newInstance()
method. The first is a symbol… of an empty string. The second is the attributes that will be used to create a ComicBook
instance—but wrapped in a list. The second argument is a little easier to explain—it is simply the list of arguments supplied to a constructor. In this case, there is a single argument—the attributes of a ComicBook
.More difficult to understand—but with the promise of something useful in the Factory Method pattern—is the first argument to
newInstance()
. The empty string Symbol
is how we tell newInstance()
to use the unnamed, default constructor for ComicBook
—e.g. new ComicBook({'title': 'Sandman', 'author': 'Neil Gaiman'})
. Given that we use this constructor by default, how might “other constructors” be useful in the context of Factory Method?One of the consequences and potential drawbacks of the Factory Method pattern is that it makes code evolution problematic. If the constructor signature needs to change at some point down the line, all of the concrete classes that rely on the current implementation would have to change as well. Or, if the concrete classes require a change, then any other code that relies on the current default constructor would have to change as well. I believe that Dart's named constructors will help with that—and I will explore them tomorrow.
Day #101
Hi, Chris.
ReplyDeleteThere is very high cost for using mirrors. See https://groups.google.com/a/dartlang.org/forum/#!topic/misc/8tTD3rzaBfM and especially heated debate at https://groups.google.com/a/dartlang.org/forum/#!topic/misc/z7s5ZUDQZzo
Not so elegant but working solution for factory method is create global map - Type to Closure
For example something like:
typedef HipsterModel modelMaker(attrs);
...
Map factory = new Map();
...
register new class:
factory[ComicBook] = (attr) => new ComicBook(attr);
factory[Magazine] = (attr) => new Magazine(attr);
create instance:
newBook = factory[ComicBook](attr)
Thanks! That's a very good point that I can't afford to overlook. Although there are probably some circumstances in which mirrors are used seldom enough that it's OK to do use them in Factory Method, this is probably not one of them. It's quite possible that the collection will need to instantiate a large number of models when fetched from a RESTful backend.
DeleteThanks also for the Map suggestion -- I'll play with it in a day or two :)
Blogspot ate the type information, Map would be of type Type->modelMaker
ReplyDelete