The bridge pattern definition sounds like little more than a jargon generator seeded with object-oriented terms. From the Gang of Four book, the intent is to:
Decouple an abstraction from its implementation so that the two can vary independently.Some day I hope to be able to reduce computer science-y terms with such whimsy. Until then, I'll settle for attempting to implement the pattern in Dart.
For my first pass, I borrow the Java implementation from the Wikipedia article. It is a nice example in which the abstraction is a shape and the implementation is a drawing API. Part of the abstraction's job is to maintain a reference to the implementation, so I start with the latter.
The implementor is
DrawingApi
:abstract class DrawingApi { void drawCircle(double x, double y, double radius); }Obviously, what will vary between concrete implementors is that
drawCircle()
method. In this very simple string-based example, the only thing that changes is the first part of the string printed by drawCircle()
:class DrawingApi1 implements DrawingApi { void drawCircle(double x, double y, double radius) { print( "[DrawingApi1] " "circle at ($x, $y) with " "radius ${radius.toStringAsFixed(3)}" ); } } class DrawingApi2 implements DrawingApi { void drawCircle(double x, double y, double radius) { print( "[DrawingApi2] " "circle at ($x, $y) with " "radius ${radius.toStringAsFixed(3)}" ); } }With that out of the way, it is time to check out the abstraction. Again, the abstraction maintains a reference to an implementor. It also defines the public-facing interface:
abstract class Shape { DrawingApi _drawingApi; Shape(this._drawingApi); void draw(); // low-level void resizeByPercentage(double pct); // high-level }What I appreciate about this example is that it identifies where changes will occur. Implementator changes are low-level changes such as variations in the
DrawingApi
. Abstraction variations are considered "high-level" such as resizing the shape.The refined abstraction for
Shape
is a Circle
. It needs to store properties for a circle and supply the implementor to the Shape
superclass:class Circle extends Shape { double _x, _y, _radius; Circle(this._x, this._y, this._radius, DrawingApi api) : super(api); // ... }The private instance variables are assigned with Dart's wonderful
this
constructor shorthand. I curse at any language that does not support this. The DrawingApi
is assigned via a redirection to the superclass' constructor. That is nice & clean and could be a one-liner if I did not abhor long lines.The low-level
draw()
method delegates to the supplied implementor:class Circle extends Shape { // ... void draw() { _drawingApi.drawCircle(_x, _y, _radius); } // ... }As long as the implementor continues to support that
DrawingApi
interface, it can change as much as it likes. As for the high-level, abstraction specific resize method, it simply multiplies the radius by a percentage:class Circle extends Shape { // ... void resizeByPercentage(double pct) { _radius *= (1.0 + pct/100.0); } }And that is the pattern. If I had to change the implementation details of the resize function, I could do so with no fear of breaking the low-level implementor. The same goes for the implementor—if I need to change how drawing works or add another type of implementor, I can do so without affecting the abstraction.
Client code can create a 2 shape list, each with a different drawing implementor. I can then loop over each to resize and draw:
main() { ListThis results in:shapes = [ new Circle(1.0, 2.0, 3.0, new DrawingApi1()), new Circle(5.0, 7.0, 11.0, new DrawingApi2()) ]; shapes.forEach((shape){ shape.resizeByPercentage(2.5); shape.draw(); }); }
$ ./bin/draw.dart [DrawingApi1] circle at (1.0, 2.0) with radius 3.075 [DrawingApi2] circle at (5.0, 7.0) with radius 11.275Nice! There is still much to explore in this pattern, but that is a nice start.
Play with the code on DartPad: https://dartpad.dartlang.org/2b092185cb17ebebecef.
Day #75
No comments:
Post a Comment