Monday, January 25, 2016

The Bridge Pattern in Dart


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.275
Nice! 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