Friday, January 8, 2016

The Adapter and the Dependency Inversion Principle


The best code believes the Earth to be flat. Possibly balanced on the backs of 4 largish elephants riding a giant turtle through space.

As far as a flat earther is concerned, travelling from Prudhoe Bay Alaska to Cape Horn is literally a straight shot from the center of our disc world to the edge. And for 99% of human thinking on travel that is just fine — certainly better than calculating the distance along a 120° arc over the surface of a sphere with a radius of 6400 km. Earth roundness is important to airlines, astronauts, and spy satellites, but most daily human existence works just fine on a flat earth. In other words, a flat earth is a useful abstraction.

That abstraction would work equally as well on the Moon or other planets. We would think of it as a flat planet abstraction on Mars or Jupiter, but the principle would be the same. And regardless of the celestial body on which we currently reside, we would think that our current home is a flat planet. That is, we would not think that flat planet is a collection of things, some of which behave slightly more or slightly less flat. Each planet that we visit would be a flat planet in our minds.

All of this is to say that flat earthers have the dependency inversion principle down cold—well, as long as they don't really believe what they preach. The dependency inversion principle states:
  1. High-level modules should not depend on low-level modules. Both should depend on abstractions.
  2. Abstractions should not depend on details. Details should depend on abstractions.
For me, it helps to mentally replace "not depend" with "be willfully ignorant of." High level modules should be willfully ignorant of low-level modules. Instead of thinking about the curve of the planet as we travel, modules should flat-earth things. Similarly, the low-level details should fit the abstraction, but the abstraction should be willfully ignorant of the details of individual planets.

All of this brings me back to the adapter pattern. I have been playing with this pattern in Dart for the past week or so, and having an octarine time of it. Before moving on, it is worth noting that the adapter pattern is a way to support dependency inversion.

My universal robot remote control example holds the high-level module stuff in the remote control. Given a robot, the remote control wants to be able to move said robot forward as much as it likes:
  universalRobot = new UniversalRemoteRobot(robot);
  universalRobot
    ..moveForward()
    ..moveForward()
    ..moveForward()
    ..moveForward()
    ..moveForward();
According to the dependency inversion principle, the remote should be willfully ignorant of the robot details. The universal remote control will have to control hundreds of different kinds of robots (lest they control us). As such, the remote control code has to willfully ignorant of the differences between Oscorp and Tyrell robots. A remote control that knows the tiny details of a hundred robots would require huge amounts of code.

So instead, the remote thinks of every single different kind of robot as an abstraction. That abstraction is the target in the adapter pattern:
/*** Target ***/
abstract class Ubot {
  String get xyLocation;
  void moveForward();
  void moveBackward();
  void moveLeft();
  void moveRight();
}
As far as the remote is concerned, all robots can move forward, backward, to the left and to the right. As long as all robots implemented this interface, we would be done as far as the dependency inversion principle was concerned: the high-level remote control treats robots as the abstraction defined in the abstract class, the remote control is willfully ignorant of the robot details, the abstract class is willfully ignorant of the robot details as well, and the individual robots would implement the abstraction.

But that is not the end of the story in this case. The Tyrell Corporation refuses to implement the same robot interface as does Oscorp, so another class is required to adapt each individual robot to our Ubot interface:
/*** Adapters ***/
class RobotAdapterToUbot implements Ubot {
  Robot _robot;
  RobotAdapterToUbot(this._robot);

  String get xyLocation => _robot.location;

  void moveForward()  { _robot.move(Direction.NORTH); }
  void moveBackward() { _robot.move(Direction.SOUTH); }
  void moveLeft()     { _robot.move(Direction.WEST); }
  void moveRight()    { _robot.move(Direction.EAST); }
}
This class is very much aware of the Ubot abstraction. As a low-level detail, it should be. At the same time, it does not flat-earth the Robot class. Its whole purpose is to adapt the low-level Robot to the abstraction. So if this adapter class cannot flat-earth Robot, doesn't it violate dependency inversion?

Well no. This is not a violation because the adapter and the robot are both low-level objects. In fact, they are at the same level, with the adapter serving as a thin wrapper for the adaptee.

I am glad to have noodled this through. I have yet to determine how to deal with concepts like the dependency inversion principle in Design Patterns in Dart. Do I include a separate chapter? Do I mention them in passing while discussing other patterns? I cannot flat-earth that, lest the book be incomplete. Regardless, it was fun looking at the relationship between the adapter pattern and the dependency inversion principle.

Or as some might put it, "Ook."

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


Day #58

No comments:

Post a Comment