Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to declare factory constructor in abstract classes?

Tags:

I want to declare, but not define a factory constructor in an abstract class.

In my case, I want to create a method that accepts any class that implements a String toJson() method as well as a fromJson(Map<String, dynamic> data) factory constructor.

Is there any way to achieve that in Dart? I'm looking for something like the following, which is not valid Dart code:

abstract class JsonSerializable {
  factory fromJson(Map<String, dynamic> data);
  String toJson();
}
like image 782
Marcel Avatar asked Jun 06 '19 20:06

Marcel


People also ask

Can we declare constructor in abstract class?

We can declare a constructor with no arguments in an abstract class. It will override the default constructor, and any subclass creation will call it first in the construction chain.

What is a factory constructor?

A factory constructor is a constructor that can be used when you don't necessarily want a constructor to create a new instance of your class. This might be useful if you hold instances of your class in memory and don't want to create a new one each time (or if the operation of creating an instance is costly).

What is abstract class Dart?

Abstract classes in Dart are classes that contain one or more abstract methods. Note: Abstract methods are those methods that don't have any implementation. It should also be noted that a class in Dart can be declared abstract using the "abstract" keyword followed by the class declaration.

What is named factory and default constructors?

A named constructor can only generate the instance of the current class. A factory constructor can decide which instance to return on runtime, it can return either the instance of the current class or any of the instances of its descendants class.


2 Answers

I'm afraid that it doesn't work the way you want it to.

Constructors are not part of an interface. They act more like static members. So, you can't add a factory to the interface, and code wouldn't have any way to call the factory constructor given a type variable extending this type anyway.

So, since constructors cannot be part of interfaces, constructors also cannot be abstract. Being abstract simply means "make the member part of the interface, but no implementation is added to class".

You can declare the factory as a normal method, but then you would only be able to call it when you already have an instance, which likely isn't what you want with a constructor.

The only way to pass code around is as functions or objects with methods. So, if you want to parameterize something by a type which is JsonSerializable, and you want to be able to create such an object, you need to pass a factory function along:

  T deserialize<T extends JsonSerializable>(
    String json,
    T factory(Map<String, dynamic> data),
  ) {
    return factory(jsonDecode(json) as Map<String, dynamic>);
  }

You an then call it with:

var myValue = deserialize(jsonString, (x) => MyClass.fromJson(x));

(If MyClass.fromJson had been a static function instead of a constructor, you could just write deserialize(jsonString, MyClass.fromJson), but Dart doesn't yet have constructor tear-offs).

like image 119
lrn Avatar answered Sep 22 '22 15:09

lrn


As suggested in the accepted answer, I ended up creating a Serializer<T> type that got implemented by a serializer for each class:

enter image description here

Turns out, this has several benefits over just having toJson/fromJson on the classes directly:

  • It decouples the serialization logic from the actual classes. That means better code readability because classes only contain methods that relate directly to the class — serializers can even be put into their own files.
  • Currently, extensions can't create constructors. So having serializers separately makes it possible to write serializers for existing classes, like String or Flutter's Color, where you can't just add a fromColor constructor.
  • Both these points combined mean it also works well with code generation — the classes are hand-written and the serializer can be generated in a separate file.

Code example:

class Fruit {
  Fruit(this.name, this.color);

  final String name;
  final String color;
}

// in another file

class FruitSerializer extends Serializer<Fruit> {
  Map<String, dynamic> toJson(Fruit fruit) {
    return ...;
  }

  Fruit fromJson(Map<String, dynamic> data) {
    return Fruit(...);
  }
}

An then also pass the serializer to the code that needs it:

someMethod<T>(Serializer<T> serializer, T value) {
  ...
}

someMethod(FruitSerializer(), someFruit);
final fruit = recreateFruit(FruitSerializer());

Obviously, you can't pass an object that can't be serialized to the code, because the method expects a Serializer<T>.

like image 35
Marcel Avatar answered Sep 19 '22 15:09

Marcel