Saturday, January 23, 2016

A Closer Look at the Proxy Annotation in Dart


I don't think I used it once. I enjoyed exploring the proxy pattern in Dart. Trying to use isolates for simple remote proxies was probably ill-advised, but aside from that, my exploration went swimmingly. Except one thing that I was sure would happen never did.

Not once did I use the @proxy annotation for any of my proxy implementations. I am pretty sure that I understand what the @proxy annotation does, but "pretty sure" pretty much always translates into some mistake on my part. And since I never once had to use it when implementing a variety of proxy classes, there is a good chance that I have a knowledge gap.

I had always assumed that @proxy annotated a class indicating that the class supported methods even if not specifically declared. Since Dart is optionally typed, this would be a static type analysis warning, not a compile or runtime issue. But I specifically run all my code through dartanalyzer before using it, so how did I avoid it?

There would have been no need for the annotation with my websocket remote car implementation. All of the methods that were declared in the interface:
abstract class AsyncAuto {
  String get state;
  Future drive();
  Future stop();
}
Were explicitly declared in the remote proxy class:
class ProxyCar implements AsyncAuto {
  // ...
  String get state => _state;
  Future drive() => _send('drive');
  Future stop()  => _send('stop');
  // ...
}
But how did I manage to avoid @proxy with my noSuchMethod() / protection proxy class? In that example, I was still working with cars, but the subject of the pattern, the Automobile interface, only declared a single method:
abstract class Automobile {
  void drive();
}
The proxy class in this example does not explicitly declare the drive method even though it implements the Automobile interface:
class ProxyCar implements Automobile {
  Driver _driver;
  Car _car;

  ProxyCar(this._driver);

  Car get car => _car ??= new Car();

  dynamic noSuchMethod(i) {
    if (_driver.age <= 16)
      throw new IllegalDriverException(_driver, "too young");

    return reflect(car).delegate(i);
  }
}
Without explicitly declaring drive(), I need @proxy, right? Well.. no. When I run dartanalyzer against this library and some client code, I get no issues in either:
$ dartanalyzer bin/drive.dart lib/car.dart
Analyzing [bin/drive.dart, lib/car.dart]...
No issues found
No issues found
I also get no errors when using this DartPad, so this seems to be expected and / or desired behavior. So what is the point of @proxy then? As far as I can tell, it comes in handy when I lack a subject in the proxy pattern. That is, when you do not have an interface to implement, then warning will be issued. For example, if I remove the implements clause from the ProxyCar implementation, but leave the same noSuchMethod() implementation in place:
class ProxyCar {
  // ...
  dynamic noSuchMethod(i) {
    if (_driver.age <= 16)
      throw new IllegalDriverException(_driver, "too young");

    return reflect(car).delegate(i);
  }
}
Then my client code:
  // ...
  car = new ProxyCar(new Driver(25));
  car.drive();
  // ...
Generates warnings:
$ dartanalyzer bin/drive.dart lib/car.dart
Analyzing [bin/drive.dart, lib/car.dart]...
[hint] The method 'drive' is not defined for the class 'ProxyCar' (/home/chris/repos/design-patterns-in-dart/proxy/bin/drive.dart, line 11, col 7)
1 hint found.
In this situation, I can eliminate the warning with a @proxy annotation before the class:
@proxy
class ProxyCar {
  // ...
}
But, as long as I have an interface to implement and a noSuchMethod() method declared, @proxy is not necessary. In The Dart Programming Language, Gilad Bracha has a nice explanation of why one might want a proxy without a classic subject. I may take a closer look at that tomorrow. For now, I have a better understanding of @proxy and why it usually is not necessary.

Play with the non-annotated code on DartPad: https://dartpad.dartlang.org/3577ddd84876ebf310c4.

Day #73

No comments:

Post a Comment