I enjoyed last night's example implementation of the bridge pattern, though it feels unfamiliar. The Gang of Four book mentions a degenerative case—that sounds right up my alley!
In last night's example, the "refined abstraction" in the pattern was a
Circle
(Shape
being the abstraction). In addition to supporting position and size arguments, the constructor also accepted an instance of an implementation. The example uses drawing circles on the screen as the implementation being supported:class Circle extends Shape { double _x, _y, _radius; Circle(this._x, this._y, this._radius, DrawingApi api) : super(api); // ... }The
draw()
method of Circle
then delegates responsibility to the drawing API (it bridge the abstraction and implementation):class Circle extends Shape { // ... void draw() { _drawingApi.drawCircle(_x, _y, _radius); } // ... }The degenerate case does not support multiple implementations. Last night's approach accepted a
DrawingApi
instance so that multiple types of DrawingApi
objects could be used (e.g. one for the console, one for the browser, etc.). But if there is only one concrete implementor, then the abstraction itself can create that implementor:abstract class Shape { DrawingApi1 _drawingApi = new DrawingApi1(); void draw(); }In this case, the
Circle
refined abstraction does not have to worry about creating or handling the DrawingApi
, it can just do circle things:class Circle extends Shape { double _x, _y, _radius; Circle(this._x, this._y, this._radius); void draw() { _drawingApi.drawCircle(_x, _y, _radius); } }The
draw()
method still works through the DrawingApi1
implementor, the only difference here is that the _drawingApi
instance is created in the Shape
abstraction class.Since there is only one implementor, there is no need for an interface. The abstraction depends directly on the concrete
DrawingApi1
implementor, which remains unchanged from yesterday's print-to-stdout barebones example:class DrawingApi1 { void drawCircle(double x, double y, double radius) { print( "[DrawingApi] " "circle at ($x, $y) with " "radius ${radius.toStringAsFixed(3)}" ); } }Client code can then create and draw a circle with something like:
new Circle(1.0, 2.0, 3.0)
..draw();
That results in the desired, bridged output:$ ./bin/draw.dart [DrawingApi] circle at (1.0, 2.0) with radius 3.075The question is why would I want to do something like this instead of putting drawing code directly inside
Circle
's draw()
method?The Gang of Four suggest that a change in the drawing implementation should not force client code to recompile. Dart compilation does not work that way—any change anywhere necessitates that everything get recompiled. I would think the intent behind the Gang of Four's assertion was the single responsibility principle and that the point is still valid in Dart. Per the SRP, a class should only have one reason to change. If the drawing code existed directly inside the
draw()
method, then Circle
would change whenever new features are added to describe a circle and whenever the manner in which drawing occurs changes.I still do not recall ever having used even this simple case. That will mean a challenge coming up with a more real-world example for either this or the regular case. Still, it does seem worth noodling through.
Play with the code on DartPad: https://dartpad.dartlang.org/0ca4f1ac6ee8caad731e.
Day #76
No comments:
Post a Comment