Monday, January 7, 2013

Enforcing Instance Variable Definition in a Dart Subclass

‹prev | My Chain | next›

While slogging through Dart for Hipsters changes, I stumbled across a welcome change: I can define an instance variable in a subclass and use it in the superclass. I had been writing a test to verify that the following would not work, per some of the discussion in the book:
class HipsterCollection implements Collection {
  var url;
  fetch() {
    // HTTP Request url from subclass
  }
  get starredUrl => "*** ${url} ***";
  // ...
}

// This failed in earlier versions of Dart...
class ComicsCollection extends HipsterCollection {
  var url = '/comics';
}
Back in the day, this was not allowed. Presumably thanks to Dart's lazily evaluated instance variables, this works. Hooray! The only thing more fun than deleting code is deleting writing.

Since the assignment is no longer a compile-time constant, I had to rewrite my test as:
  group("[superclass access]", (){
    test('cannot access subclass ivar', (){
      var comics_collection = new ComicsCollection();
      expect(
        comics_collection.starredUrl,
        equals('*** /comics ***')
      );
    });
  });
That is obviously a big win for Dart as the previous behavior confused me to no end.

Unanswered is how do I declare the url instance variable as must-override? With methods, I can declare the class as abstract and provide no body, but that will not work with instance variables. The following results in a compile-time error:
abstract class HipsterCollection implements Collection {
  abstract var url;
  // ...
}
Dart does not allow abstract fields:
➜  mvc_library git:(master) ✗ dart test/test.dart         
'file:///Code/mvc_library/test/superclass_access_to_subclass_ivar.dart': Error: line 9 pos 19: keyword 'abstract' not allowed in field declaration
  abstract var url;
                  ^
I could create a constructor in the baseclass that throws an error if the url instance variable is not defined:
abstract class HipsterCollection implements Collection {
  var url;
  HipsterCollection() {
    if (url == null) throw new ArgumentError("url required");
  }
  // ...
}
A "broken" subclass would not define the url instance variable:
class BrokenCollection extends HipsterCollection {}
This would result in a run-time exception:
    test('subclass must have a url', (){
      expect(
        ()=> new BrokenCollection(),
        throwsArgumentError
      );
    });
That passes, so there is a possible solution. Is it the best solution? As much as the dynamic typist in me hates to admit it, it would be preferable if Dart's typing system could catch this.

Even if I cannot make abstract instance variables in Dart, I can certainly make abstract setters and getters:
abstract class HipsterCollection implements Collection {
  void set url(v);
  String get url;
  // ...
}
Methods without bodies inside of an abstract class are abstract methods. The dart_analyzer requires that such methods be overridden in the concrete subclass. Since instance variables in Dart automatically define getters and setters, defining an url in the subclass would be the same as defining both the setter (.url=) and getter (.url) methods in the concrete subclass.

Thus the typing system now considers the empty BrokenClass to be invalid:
➜  mvc_library git:(master) ✗ dart_analyzer test/test.dart
file:/Code/mvc_library/test/superclass_access_to_subclass_ivar.dart:27:7: Concrete class BrokenCollection has unimplemented member(s) 
    # From HipsterCollection:
        String url
    26: 
    27: class BrokenCollection extends HipsterCollection {}
              ~~~~~~~~~~~~~~~~
Since dart_analyzer is reporting this as a single error (String url is unimplemented) and not two (getter and setter are not implemented), it would seem that this is the proper way of dealing with the requirement that a subclass must define an instance variable.

I could retain the constructor that throws an error in addition to this approach, but heaven help me, I have learned to trust the Dart typing system. Besides, cluttering up a class definition with explicit run-time assertions does not aid readability. Plus it seems a silly thing to do in a strongly typed language.


Day #623

2 comments:

  1. Using abstract getters and setters is the best you can do. It beats having a field in the superclass, since that would add an unused memory cell to each object.
    Quite often you only really need an abstract getter in the superclass.

    ReplyDelete
    Replies
    1. Thanks! Good to know I'm doing it the Dart Way™ :)

      I did notice that I got the same dart_analyzer warning with just the abstract getter in the superclass. I feel better being explicit about the setter as well, but agree that's just personal preference.

      Delete