Saturday, January 2, 2016

Delegated Pluggable Adapter Pattern


I am reasonably happy with last night's pluggable adapter pattern in Dart. But there are always nagging concerns.

My current solution builds a large registry of methods and arguments, organized by type. In this case, my pluggable adapters support a universal remote control for robots, so the registry is organized by robot class:
  static Map<Type, Map> _registry = {
    Robot: {
      'forward':  [#move, [Direction.NORTH]],
      'backward': [#move, [Direction.SOUTH]],
      'left':     [#move, [Direction.WEST]],
      'right':    [#move, [Direction.EAST]]
    },
    Bot: {
      'forward':  [#goForward, []],
      'backward': [#goBackward, []],
      'left':     [#goLeft, []],
      'right':    [#goRight, []]
    }
  };
I am a little concerned that this would grow past manageable state once the universal remote control supports more than a dozen robots or so. I also imagine that I will wind up adding conditionals for more than one type of robot in the various movement methods that I am adapting. Currently, they are a relatively clean mirror-based implementation:
  void moveForward() {
    var _ = _registry[_robot.runtimeType]['forward'];
    reflect(_robot).invoke(_[0], _[1]);
  }
But, like I said, I can see that getting messy after a while.

So instead, I try moving the adapted methods back into individual classes, one for each kind of robot. So the Robot adapter, which its move() method and direction arguments will look like:
class RobotAdapterToUbot implements Ubot {
  Robot _robot;
  RobotAdapterToUbot(this._robot);

  String get xyLocation => _robot.location;

  void moveForward()  { _robot.move(Direction.NORTH); }
  // other move methods here...
}
Similarly, the adapter for Bot, which sports go-* methods for movements will look like:
class BotAdapterToUbot implements Ubot {
  Bot _bot;
  BotAdapterToUbot(this._bot);

  String get xyLocation => "${_bot.x}, ${_bot.y}";

  void moveForward()  { _bot.goForward(); }
  // other move methods here...
}
I like this solution for cases in which there is variation in adaptee interfaces. For example, the Bot adapter supports a specialized xyLocation getter without having to perform a type check in a single adapter class.

These adapters need to support the same interface, Ubot in this case, so that a delegating class can invoke the same method regardless of adaptee. The delegating class will provide the object that gets used in client code. To start with, I make that delegating class implement the same Ubot interface:
class UniversalRemoteRobot implements Ubot {
  Ubot _ubot;
  UniversalRemoteRobot(robot) {
    if (robot is Robot) _ubot = new RobotAdapterToUbot(robot);
    else if (robot is Bot) _ubot = new BotAdapterToUbot(robot);
  }

  String get xyLocation => _ubot.xyLocation;

  void moveForward()  { _ubot.moveForward(); }
  // other move methods here...
}
This class sets the delegated adapter in the constructor based on the adaptee type. If UniversalRemoteRobot is constructed for a Robot, then the RobotAdapterToUbot is used. For a Bot, the delegated adapter is a BotAdapterToUbot. As long as new robot types are assigned adapters to the Ubot interface, it should be easy to add them—they just need to be delegated in the constructor.

This is, I believe, the exact structure of the second pluggable adapter from the Gang of Four book, though their example had the delegating class and the adapters supporting different interfaces. I may try something like that tomorrow. For now, it does not get much easier than having the moveForward() in the delegator invoke the same method in the delegated adapter.

With that, my universal remote control works exactly the same for Bot and Robot instances:
  var r = new Bot();
  // This works exactly the same:
  // var r = new Robot();

  var universalRobot = new UniversalRemoteRobot(r);
  print("Start moving the robot.");
  universalRobot
    ..moveForward()
    ..moveForward()
    ..moveForward()
    ..moveForward()
    ..moveForward();
  print("The robot is now at: ${universalRobot.xyLocation}.");
I still think last night's mirror-based approach has its place—especially in situations where I am assured of simple method invocations on the adaptees. But this delegated pluggable adapter approach feels like a better all-purpose solution.

I will likely put that sentiment to the test. Tomorrow.

Play with the code on DartPad: https://dartpad.dartlang.org/2c87242d40969a9a3c2c.


Day #52

No comments:

Post a Comment