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