Confession: I never understood a thing about pluggable adapters in the Gang of Four book. I have read that section on more than one occasion and not understood a damn word. So it seems like good grist for a blog post.
Part of my confusion is that I am not 100% clear on what is meant by the term. When first introduced in the book, a pluggable adapter describes "classes with built-in interface adaption." I am unclear on what "built-in" means and what gets it (the adapter or adaptee). Later, they describe a reusable widget that needs to work with similar objects even if they have different interfaces. I am going to focus on that second idea for now. Maybe I can figure out what they mean in the first definition for a follow-up post.
I am still working with my
Robot
Dart class that moves in different directions via a single move()
method:enum Direction { NORTH, SOUTH, EAST, WEST } class Robot { int x=0, y=0; String get location => "$x, $y"; void move(direction) { print(" I am moving $direction"); switch (direction) { case Direction.NORTH: y++; break; case Direction.SOUTH: y--; break; case Direction.EAST: x++; break; case Direction.WEST: x--; break; } } }A competing robot manufacturer might have a different robot movement API that looks like:
class Bot { int x=0, y=0; void goForward() { y++; } void goBackward() { y--; } void goLeft() { x--; } void goRight() { x++; } }So how do I go about defining an interface that can work with either of these? I think that if I can answer that, I will have a pluggable adapter.
The universal remote control that I am building requires a target interface that looks like:
abstract class Ubot { void moveForward(); void moveBackward(); void moveLeft(); void moveRight(); }When I previously only needed to support the
Robot
class, I could do so with a plain-old adapter like:class UbotRobot implements Ubot { Robot _robot; UbotRobot(this._robot); void moveForward() => _robot.move(Direction.NORTH); void moveBackward() => _robot.move(Direction.SOUTH); void moveLeft() => _robot.move(Direction.WEST); void moveRight() => _robot.move(Direction.EAST); }One way that I can convert that to support the
Bot
class as well is to add a conditional to each move-related method:class UbotRobot { var _robot; UbotRobot(this._robot); bool get isRobot => _robot is Robot; bool get isBot => _robot is Bot; void moveForward() { if (isRobot) _robot.move(Direction.NORTH); if (isBot) _robot.goForward(); } // ... }That actually does the trick. In the client code, I can now use a
Robot
interchangeably with Bot
when instantiating the universal UBot
instance: var robot = new Bot(); // new Robot() also works
var universalRobot = new UbotRobot(robot);
Regardless of which robot is used, the movement commands work, thanks to the adapter: print("Start moving the robot.");
universalRobot
..moveForward()
..moveForward()
..moveForward()
..moveForward()
..moveForward();
print("The robot is now at: ${universalRobot.location}.");
With the loquacious Robot
, this results in:$ ./bin/play_robot.dart Start moving the robot. I am moving Direction.NORTH I am moving Direction.NORTH I am moving Direction.NORTH I am moving Direction.NORTH I am moving Direction.NORTH The robot is now at: 0, 5.While the taciturn
Bot
produces:$ ./bin/play_robot.dart Start moving the robot. The robot is now at: 0, 5.Both kinds of robots are controlled by the same universal remote class.
It would be a pain to maintain an adapter like this. When the next robot is supported, I would have to make changes to each method. Worse, each method would eventually grow to contain one line for every supported interface. Better is to create a registry that contains enough information to invoke the appropriate commands on the adaptees. I think this must be what the Gang of Four were talking about with the three different versions of pluggable adapters.
I am unsure that I understand each of the types that they discuss. I do understand... mirrors! So I import Dart mirrors and declare a registry with sufficient information to issue move commands to each type:
import 'dart:mirrors'; class UbotRobot implements Ubot { var _robot; UbotRobot(this._robot); 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, []] } }; // ... }With that, I can reflect on the robot when invoking the appropriate movement method (with arguments):
class UbotRobot implements Ubot { // ... void moveForward() { var _ = _registry[_robot.runtimeType]['forward']; reflect(_robot).invoke(_[0], _[1]); } // ... }Happily, that works as desired. Don't believe me? Try the code on DartPad: https://dartpad.dartlang.org/09b8a2b1f6da874c7c68!
I think I have a idea of what pluggable adapters are now. I may explore the concept a little further tomorrow. Hopefully I can better understand those implementation discussions from the book now!
Day #51
No comments:
Post a Comment