Wednesday, January 13, 2016

A Real Remote Proxy Across Dart Isolates


My many adventures with the proxy pattern in Dart continues. After day 1, I workable, if ugly code. After day 2, I have nicer, if not-quite-a-proxy-pattern code. So tonight I hope to actually implement a somewhat nice looking version of an actual remote proxy across Dart isolates.

Let's see how I can mess that up.

The setup between the main worker and the isolate worker is pretty typical isolate setup. The main worker holds a ProxyCar and will communicate to a real Car in the isolate. It starts by creating a ReceivePort on which it can listen for messages from the isolate, spawns the isolate, then waits for the first message back from that isolate which will be a SentPort to send messages back to the isolate:
main() async {
  var r = new ReceivePort();
  var receiveStream = r.asBroadcastStream();

  await Isolate.spawn(other, r.sendPort);

  SendPort s = await receiveStream.first;
  // Remote car stuff will follow...
}
Similarly, the other() isolate that holds a real Car instance accepts the SendPort from main() and creates its own ReceivePort to send to main():
other(SendPort s) {
  var r = new ReceivePort();
  var receiveStream = r.asBroadcastStream();
  s.send(r.sendPort);

  new Car(receiveStream, s);
}
Thanks to the very nice async / await syntax in Dart, I was able to clean up a lot of initial async messiness. But now I realize that my remote proxy class does not match the real subject. The ProxyCar class returns Future instances for car actions:
class ProxyCar implements Automobile {
  // ...
  Future drive() => _send(#drive);
  Future stop()  => _send(#stop);
  // ...
}
The real subject (and the interface that both classes implement) declares start() and stop() as returning nothing:
class Car implements Automobile {
  // ...
  void drive() { state = 'driving'; }
  void stop()  { state = 'idle'; }
}
Interestingly (at least to me) is that the Dart type analyzer does not consider this a problem. I would still feel more comfortable if the remote proxy class and the real subject class both implemented the same interface.

There is nothing to be done about the ProxyCar interface. Communication across isolates is inherently asynchronous. So I will experiment with the adapter pattern, adapting a synchronous Car class to an asynchronous version. This might not be the best idea, but...

It does have the advantage of producing a nice, clear Car class:
// Adaptee
class Car implements Automobile {
  String state = 'idle';
  void drive() { state = 'driving'; }
  void stop()  { state = 'idle'; }
}
For the remote proxy, I define a new interface for both the AsyncCar and ProxyCar classes to implement, this time with futury goodness:
// Subject
abstract class AsyncAuto implements Automobile {
  String get state;
  void drive();
  void stop();
}
The AsyncCar class then become both the adapter and real subject. The async messiness as well as the send and receive ports pile into it:
// Real Subject & Adapter
class AsyncCar implements AsyncAuto {
  SendPort _s;
  ReceivePort _r;
  Car _car;
  AsyncCar(this._r, this._s) {
    _car = new Car();

    _r.listen((message) {
      print("[AsyncCar] $message");
      if (message == #drive) _car.drive();
      if (message == #stop)  _car.stop();
      _s.send(state);
    });
  }

  String get state => _car.state;
  Future drive() => new Future((){ _car.drive(); });
  Future stop()  => new Future((){ _car.stop(); });
}
Not much has changed in there from the previous two nights aside from state, drive(), and stop() methods which now forward requests to a concrete Car instance. The drive() and stop() methods also return Future instances for completeness.

Nothing in the ProxyCar class needs to change other than the interface that it implements, which the new AsyncAuto interface:
// Proxy Subject
class ProxyCar implements AsyncAuto {
  // ...
}
Last, I change the isolate to create an AsyncCar:
other(SendPort s) {
  var r = new ReceivePort();
  var receiveStream = r.asBroadcastStream();
  s.send(r.sendPort);

  new AsyncCar(receiveStream, s);
}
With that, I have more traditional version of the remote proxy in which both the remote subject and real subject implement the same interface. I appreciate that the Car class is very clean now. Aside from that, I am unsure that I have bought myself any improvements in code readability or maintainability. So, for now, I would have to rate the benefit of this exercise inconclusive... but fun.


Day #63

No comments:

Post a Comment