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() {
List 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();
});
} This results in:$ ./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