Sunday, January 3, 2016

Asynchronous Delegator Adapters and the Simplest Commands that Love Them


So, pluggable adapters in Dart (or any languages for that matter)... I think I finally understand them.

The strange thing is that plugable adapters discussion in the Gang of Four book make complete sense now. I never really understood that part of the adapter pattern chapter before. Maybe it's simply that my brain is incapable of processing UML diagrams. Whatever the reason, it makes sense now—hopefully the previous two night's posts can attest to that.

There is one thing missing in last night's delegating, pluggable adapter pattern: different delegator and adapter interfaces. I think it is likely perfectly acceptable for both delegator and adapter to implement the same interface. It probably helps to illustrate the pattern to have them implement two different interfaces.

So my universal remote control for robots needs an interface that can be adapted to multiple robots. The Ubot interface describes the interface:
/*** Target ***/
abstract class Ubot {
  String get xyLocation;
  void moveForward();
  void moveBackward();
  void moveLeft();
  void moveRight();
}
In my quest for robot control dominance, I currently have adapters for two different kinds of robots: the standard Robot class and the newer Bot class robots. Astromechs are next, but that's tale for another day. The adapters implement the Ubot interface:
/*** Adapters ***/
class RobotAdapterToUbot implements Ubot {
  Robot _robot;
  RobotAdapterToUbot(this._robot);

  String get xyLocation => _robot.location;

  void moveForward()  { _robot.move(Direction.NORTH); }
  // More move methods here...
}

class BotAdapterToUbot implements Ubot {
  Bot _bot;
  BotAdapterToUbot(this._bot);

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

  void moveForward()  { _bot.goForward(); }
  // More move methods here...
}
As of last night, the universal robot also implements the Ubot interface. Since it implements the same interface, all that it really does is delegate to the appropriate adapter in the constructor after which all methods invoke the method of the same name in the adapter:
/*** A common interface delegator ***/
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);
    else _ubot = new NullAdapterToUbot(robot);
  }

  String get xyLocation => _ubot.xyLocation;

  void moveForward()  { _ubot.moveForward(); }
  // More move methods here...
}
Tonight, I remove that common interface and switch back to an asynchronous version of the universal remote control. Previously, that had been the adapter in the pattern that I was using, now it becomes the delegator to the appropriate adapter:
class UniversalRemoteRobot {
  Ubot _ubot;

  UniversalRemoteRobot(robot) { _delegateRobot(robot); }

  void _delegateRobot(robot) {
    if (robot is Robot) _ubot = new RobotAdapterToUbot(robot);
    else if (robot is Bot) _ubot = new BotAdapterToUbot(robot);
    else _ubot = new NullAdapterToUbot();
  }

  // Asynchronous methods go here...
}
The asynchronous methods, which move the robot until the movement is canceled (by pressing stop, releasing the move button, the robot crashing, etc.), can still be implemented with Timer objects:
const oneSecond = const Duration(seconds: 1);

class UniversalRemoteRobot {
  // ...
  Timer moveForward() =>
    new Timer.periodic(oneSecond, (_){ _ubot.moveForward(); });
  // ...
}
That moveForward() method will start a timer that moves the adapted robot forward once every second. That is a little messy already, which will make the class messy once the other four directions are implemented. It would get even worse if I had to store the timer so that all active commands can be stopped:
class UniversalRemoteRobot {
  // ...
  Timer moveForward() {
    var t = new Timer.periodic(oneSecond, (_){ _ubot.moveForward(); });
    _activeControls.add(t);
    return t;
  }
  // ...
}
Doing that four times would be crazy. So I need to pass the command (receiver and action) to a helper method. That sounds like a job for the command pattern!

Except I would prefer to avoid using two patterns in the same example code. So do I send an anonymous function to a timer method?
class UniversalRemoteRobot {
  // ...
  Timer _executeControlledCommand(command) {
    var t = new Timer.periodic(oneSecond, (_){ command(); });
    _activeControls.add(t);
    return t;
  }
}
That helper would execute the command on the delegated/adapted robot, add the timer to a list, then return the timer so that it could be canceled. But anonymous functions? Mirrors? Actual command objects?

It turns out that there is a Dart command object that I neglected to cover in my first pass at researching the topic. I can simply pass the method and I get the receiver for free:
class UniversalRemoteRobot {
  // ...
  Timer moveForward()  => _executeControlledCommand(_ubot.moveForward);
  // Other move methods here...

  Timer _executeControlledCommand(command) {
    var t = new Timer.periodic(oneSecond, (_){ command(); });
    _activeControls.add(t);
    return t;
  }
}
I really am a JavaScript hostage. It never occurred to me that passing an object's method would bring the object along without having to resort to some call() or apply() calling context silliness. Who would have guessed that it would just work like one would expect?

Well, I should have expected that it would work. And work it does. Now in the client context, I can create either a Robot or a Bot:
  var robot = new Robot();
  // This works exactly the same:
  // var robot = new Bot();
And supply that to a new instance of UniversalRemoteRobot, starting it along the way:
  var universalRobot = new UniversalRemoteRobot(robot);

  print("Start moving $robot.");
  var btnCtrl = universalRobot.moveForward();
I can then cancel that 10 seconds later:
  // Simulate releasing the button after 10 seconds...
  new Timer(
    new Duration(seconds: 10),
    (){
      btnCtrl.cancel();
      print("The robot is now at: ${universalRobot.xyLocation}.");
    }
  );
With that, I have an — ahem — asynchronous, non-common interface, delegating class to pluggable adapters. Or something like that.

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



Day #53

No comments:

Post a Comment