Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Creating an interface for construction

Tags:

dart

A few times now I've run into a use case where I need to define an interface for how classes construct themselves. One such example could be if I want to make an Interface Class that defines the interface by which objects can serialize and unserialize themselves (for input into a database, to be sent as JSON, etc). You might write something like this:

abstract class Serializable {
  String serialize();
  Serializable unserialize(String serializedString);
}

But now you have a problem, as serialize() is properly an instance method, and unserialize() should instead be a static method (which isn't inheritable or enforced by the Interface) or a constructor (which also isn't inheritable).

This leaves a state where classes that impliment the Serializable interface are required to define a serialize() method, but there is no way to require those classes to define a static unserialize() method or Foo.fromSerializedString() constructor.

If you make unserialize() an instance method, then unserializing an implementing class Foo would look like:

Foo foo = new Foo();
foo = foo.unserialize(serializedString);

which is rather cumbersome and ugly.

The only other option I can think of is to add a comment in the Serializable interface asking nicely that implementing classes define the appropriate static method or constructor, but this is obviously prone to error if a developer misses it and also hurts code completion.

So, is there a better way to do this? Is there some pattern by which you can have an interface which forces implementing classes to define a way to construct themselves, or something that gives that general effect?

like image 829
Michael Fenwick Avatar asked Dec 22 '14 19:12

Michael Fenwick


3 Answers

You will have to use instance methods if you want the inheritance guarantees. You can do a bit nicer than manual instantiation though, by using reflection.

abstract class Serializable {

  static Serializable fromSerializedString(Type type, String serializedString) {
    ClassMirror cm = reflectClass(type);
    InstanceMirror im = cm.newInstance(const Symbol(''), []);
    var obj = im.reflectee;
    obj.unserialize(serializedString);
    return obj;
  }

  String serialize();
  void unserialize(String serializedString);
}

Now if someone implements Serializable they will be forced to provide an unserialize method:

class Foo implements Serializable {

  @override
  String serialize() {
    // TODO: implement serialize
  }

  @override
  void unserialize(String string) {
    // TODO: implement unserialize
  }
}

You can get an instance like so:

var foo = Serializable.fromSerializedString(Foo, 'someSerializedString');

This might be a bit prettier and natural than the manual method, but keep in mind that it uses reflection with all the problems that can entail.

If you decide to go with a static method and a warning comment instead, it might be helpful to also provide a custom Transformer that scans through all classes implementing Serializable and warn the user or stops the build if any don't have a corresponding static unserialize method or constructor (similar to how Polymer does things). This obviously wouldn't provide the instant feedback the an editor could with instance methods, but would be more visible than a simple comment in the docs.

like image 177
Pixel Elephant Avatar answered Oct 13 '22 06:10

Pixel Elephant


I think this example is a more Dart-like way to implement the encoding and decoding. In practice I don't think "enforcing" the decode signature will actually help catch bugs, or improve code quality. If you need to make the decoder types pluggable then you can make the decoders map configurable.

const Map<String,Function> _decoders = const {
  'foo': Foo.decode,
  'bar': Bar.decode
};

Object decode(String s) {
  var obj = JSON.decode(s);
  var decoder = _decoders[obj['type']];
  return decoder(s);
}

abstract class Encodable {
  abstract String encode();
}

class Foo implements Encodable {
   encode() { .. }
   static Foo decode(String s) { .. }
}

class Bar implements Encodable {
   encode() { .. }
   static Foo decode(String s) { .. }
}

main() {
   var foo = decode('{"type": "foo", "i": 42}');
   var bar = decode('{"type": "bar", "k": 43}');
}
like image 38
Greg Lowe Avatar answered Oct 13 '22 06:10

Greg Lowe


A possible pattern I've come up with is to create a Factory class that utilize instance methods in a slightly less awkward way. Something like follows:

typedef Constructable ConstructorFunction();

abstract class Constructable {
  ConstructorFunction constructor;
}

abstract class Serializable {
  String serialize();
  Serializable unserialize(String serializedString);
}

abstract class SerializableModel implements Serializable, Constructable {
}

abstract class ModelFactory extends Model {    
  factory ModelFactory(ConstructorFunction constructor) {
    return constructor();
  }

  factory ModelFactory.fromSerializedString(ConstructorFunction constructor, String serializedString) {
    Serializable object = constructor();
    return object.unserialize(serializedString);
  }
}

and finally a concrete implementation:

class Foo extends SerializableModel {
  //required by Constructable interface
  ConstructorFunction constructor = () => new Foo();

  //required by Serializable interface
  String serialize() => "I'm a serialized string!";
  Foo unserialize(String serializedString) {
    Foo foo = new Foo();
    //do unserialization work here to populate foo
    return foo;
  };
}

and now Foo (or anything that extends SerializableModel can be constructed with

Foo foo = new ModelFactory.fromSerializedString(Foo.constructor, serializedString);

The result of all this is that it enforces that every concrete class has a method which can create a new instance of itself from a serialized string, and there is also a common interface which allows that method to be called from a static context. It's still creating an extra object whose whole purpose is to switch from static to instance context, and then is thrown away, and there is a lot of other overhead as well, but at least all that ugliness is hidden from the user. Still, I'm not yet convinced that this is at all the best way to achieve this.

like image 44
Michael Fenwick Avatar answered Oct 13 '22 07:10

Michael Fenwick