Today I take a look at the bridge pattern with Dart mixins. The Gang of Four book includes a brief discussion on multiple inheritance and the bridge pattern. This has me curious about mixins, which can serve a similar purpose to multiple inheritance.
I continue to work with the shape drawing example from Wikipedia. In it, a
Shape
is the the abstraction in need of bridging to an implementation—drawing in this case. The abstract Shape
class requires a drawing API object be supplied when constructing a concrete implementation, along with two methods that must be defined by that same subclass:abstract class Shape { DrawingApi _drawingApi; Shape(this._drawingApi); void draw(); // low-level void resizeByPercentage(double pct); // high-level }In a "refined abstraction," the low-level
draw()
is the method that needs the bridge to the implementor. For a Circle
, the draw()
method might invoke a drawCircle()
method on a concrete drawing API object:// Refined Abstraction class Circle extends Shape { double _x, _y, _radius; Circle(this._x, this._y, this._radius, DrawingApi api) : super(api); // low-level i.e. Implementation specific void draw() { _drawingApi.drawCircle(_x, _y, _radius); } // ... }The client then create a concrete
DrawingApi
object (like DrawingApi1
) to supply to Circle
's constructor: new Circle(1.0, 2.0, 3.0, new DrawingApi1())
..draw();
That works great and is a nice example of a bridge pattern. But it is a hassle to have to construct a concrete DrawingApi
class every time I construct a Circle
. What's to prevent me from using DrawingApi1
(or DrawingApi2
) as a mixin? The answer is nothing!Since the abstraction no longer needs to track the implementor, it can be simplified to an interface requiring two methods:
abstract class Shape { void draw(); void resizeByPercentage(double pct); }Then I mixin
DrawingApi1
to Circle
by extending Shape
with DrawingApi1
:class Circle extends Shape with DrawingApi1 { double _x, _y, _radius; Circle(this._x, this._y, this._radius); void draw() { /* ... */ } void resizeByPercentage(double pct) { /* ... */ } }With access to the methods of
DrawingApi1
, I can now call drawCircle()
directly from draw()
:class Circle extends Shape with DrawingApi1 { // ... void draw() { drawCircle(_x, _y, _radius); } // ... }That in turn simplifies my client code since it longer needs to worry about creating the drawing API object:
new Circle(1.0, 2.0, 3.0)
..draw();
When this code is, just as in the prior implementation, it produces the following output:$ ./bin/draw.dart [DrawingApi1] circle at (1.0, 2.0) with radius 3.000So it is possible to use mixins to implement the bridge pattern in Dart… except not quite.
This will work for the degenerate case explored last night in which there is only one concrete drawing implementor. Both the implementor and abstraction can vary independently, which is ultimately the goal of the pattern.
What does not work is the same thing that the Gang of Four mentions as a shortcoming of the multiple inheritance approach in C++. The binding between the abstraction and implementor are now permanent. If I needed to vary the implementation at runtime, I am out of luck. Even mirrors will not help—at least not until the
mixin
property of ClassMirror
is read-write.In the end, mixins in the bridge pattern are a fun experiment. They may even be a useful approach to use in degenerate, single implementor implementations. For a full featured bridge pattern, mixins will not serve as a legitimate solution.
Play with the code on DartPad: https://dartpad.dartlang.org/b0050955ee95746956da.
Day #77
No comments:
Post a Comment