Saturday, December 26, 2015

Typedef and the Dart Command Pattern


I fancied myself done with the command pattern in Dart. Anders Holmgren had other ideas. In the comments on last night's "final" command pattern article, he had a number of insightful suggestions that I have yet to consider.

So consider I do, tonight.

First up, I had not been constraining my function or object commands. Just about everywhere I use commands, I allow it to be a generic Function:
class Button {
  String name;
  Function command;
  Button(this.name, this.command);

  void press() {
    print("[pressed] $name");
    command.call();
    History.add(command);
  }
}
It should be noted here that any class that supports a call() method is a Function. So command here can be a function or function-like object.

As can be seen in the press() method, the function or object never takes any arguments. Furthermore, it never returns a value, so its return type should be void. Although the following Button would result in a run-time error, the Dart analyzer thinks that it is perfectly OK:
new Button("Test", (String arg) => "$arg $arg $arg");
The anonymous function should neither accept an argument nor should it return a value as it does with the hash rocket symbol.

Thankfully, Anders has me covered here. I should declare a typedef that describes my command interfaces:
typedef void _Command();
Anything with a type of _Command will now be required by the analyzer to return nothing and accept no arguments.

I replace all Function types with _Command, including in my Button invoker:
class Button {
  String name;
  _Command command;
  Button(this.name, this.command);

  void press() { /* ... */ }
}
Now the analyzer complains about my "Test" button:
[warning] The argument type '(String) → String' cannot be assigned to the parameter type '_Command'
So typedefs are a great suggestion.

What I would like to do next is replace the Function in the Command class declaration:
abstract class Command implements Function {
  void call();
}
I would like to explicitly tie my Command class with the void-no-argument _Command typedef:
abstract class Command implements _Command {
  void call();
}
That will not work however because:
[error] Classes can only implement other classes
I am not entirely out of luck, however. Even though there is no way to explicitly tie Command to my typdef, it is already implicitly associated. Any function-like thing that accepts no arguments and returns void is a _Command typedef. Thus:
abstract class Command implements Function { /* ... */ }
abstract class UndoableCommand implements Command { /* ... */ }
class MoveNorthCommand implements UndoableCommand {
  // ...
  void call() { robot.move(Direction.NORTH); }
}

main() {
  // ...
  var moveNorth = new MoveNorthCommand(robot);
  print("MoveNorthCommand is a _Command: ${moveNorth is _Command}");
}
Results in:
MoveNorthCommand is a _Command: true
So Dart already knows that anything implementing Command—or even declaring a void-no-arg call()—is a _Command.

In other words, there is no way to explicitly constrain a function class to a typedef interface... other than using a clever trick that Anders employs in the UndoCommand declaration. Even though I cannot extend or implement a typedef in a class declaration, I can extend a type in a generic:
abstract class UndoableCommand<T extends _Command> implements Command {
  void call();
  T get undoCommand;
}
The type of undoCommand is now restricted (upper-limitted?) to be a no-arg-void-return _Command. I remain unsure about that, however as it seems a bit... obscure. Why should it work to extend a generic type when extending the source class with the same thing does not? Regardless, it is certainly a nifty tool to keep in my toolbelt.

Much thanks to Anders for his pointers. I think I still have another point or two that I still need to cover. Tomorrow.


Day #45

No comments:

Post a Comment