Tuesday, January 26, 2016

Degenerate Bridges


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.075
The 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