tl;dr
Generally speaking, use a factory in situations where you don’t necessarily want to return a new instance of the class itself and / or want to hide the implementation details of the object creation from the caller.
Use cases:
- You want to return a pre-existing instance of your class (e. g. cache)
- You want to return a subclass instance instead of the class itself
- You have complex initialization of a
final
variable you don’t want to happen in the initializer list
Having inspected some of the framework classes, third party packages or Dart / Flutter docs, you might have stumbled across the factory
keyword and wondered what that means.
In this article, we are going to clarify:
- The meaning of the keyword
- When you should use it
- The difference between
factory
and a generative constructor - The differences between
factory
andstatic
Factory pattern
Before we go deeper into the syntax and the semantics of the factory
keyword, let’s examine the origins of it.
Because the factory
keyword in Dart is actually only syntactic sugar to express something that is language-agnostic. That is the underlying pattern which is called the factory pattern or factory method pattern.
Instead of using a default constructor, a method defined on every class and named the same, the idea is to let the creation happen somewhere else.
Cat()
) is nothing else but a static
method defined on a class (Cat
) whose return type must be of the same type (Cat
). The main difference compared to a “normal” static
function on a class is the inability to change its return type.The main benefits of using the factory design pattern are the following:
- The responsibility of creating objects does not lie within the class itself but in a separate class (the factory class) which implements an interface
- Object creation being bound to the caller class lacks flexibility as you are unable to change the concrete object instantiation independently from the class which implies a coupling. This coupling is not given when using the pattern
In other words: A Breeder
does not need to know how to instantiate a Cat
because Cat
s are being produced in factories 🙀🏭. The breeder only says makeACat()
and the factory returns a new Cat
.
This has the advantage that the Breeder
does not change his behavior if the way Cat
s are produced, changes.
To conclude: It can be used to create objects without exposing the underlying implementation details to the caller.
Constructor types
If this confuses you, don’t worry - we will get to some examples right away. But first, let’s clarify some notions:
In Dart there are generative constructors and factory constructors, which may be respectively named or unnamed.
An example for a generative constructor is the default constructor that is being created automatically for every class.
Let’s consider an exemplary class in order to get a better understanding of the different constructor types:
In our example, there is a Cat
class with only one property: color
of type Color
.
The constructor types would look as follows:
Generative | Factory | |
---|---|---|
Unnamed |
|
|
Named |
|
|
Be aware that you are not allowed to create a factory
constructor being named like an already existing constructor - either generative or factory and no matter if named or unnamed.
The only exception for this is when you define an unnamed factory
constructor without having explicitly defined an unnamed generative constructor. When you don’t define an unnamed constructor, it will be generated for you automatically and when you defined a factory
constructor then, it will be overridden.
The keyword in Dart
The factory
keyword is not a 1:1 accurate implementation of how you might know the pattern from classic OOP languages like C++ or Java.
That’s because the idea there is to have a separate class that handles the object creation (like a CatFactory
😸).
By using a factory
constructor, however, you still have the logic of the object creation inside the same class. With the exception of instantiating subclasses, which is also possible with factory
constructors.
When you should use it
"Use the factory keyword when implementing a constructor that doesn’t always create a new instance of its class. For example, a factory constructor might return an instance from a cache, or it might return an instance of a subtype. Another use case for factory constructors is initializing a final variable using logic that can’t be handled in the initializer list."
The docs basically mention three use cases:
- Returning an instance from a cache
- Returning an instance of a subtype
- Initializing a final variable
Let’s explain the docs’ example one by one.
Instance from a cache
Let’s pretend we have a ColorComputer
that takes very long to compute the color of the cat. We also have a CatCache
that stores the last created colored cat to prevent having to execute the heavyColorComputation()
everytime a Cat.colored
is instantiated.
1class Cat {
2 Cat(this.color);
3
4 factory Cat.colored(CatCache catCache) {
5 Cat? cachedCat = catCache.getCachedColorCat();
6
7 if (cachedCat != null) {
8 return cachedCat;
9 }
10
11 Color color = ColorComputer().heavyColorComputation();
12 return Cat(color);
13 }
14
15 final Color color;
16}
As you can see, we can use the factory
constructor for this use case because we might return an existing instance of Cat
(if the cache hits).
Complex final variable initialization
If you have a more complicated initialization of a final
variable that can’t really be handled inside the initializer list, you can use a factory
constructor instead.
1class Cat {
2 Cat._({
3 required this.id,
4 required this.name,
5 required this.age,
6 required this.color
7 });
8
9 final int id;
10 final String name;
11 final int age;
12 final Color color;
13
14 factory Cat.fromJson(Map<String, dynamic> json) {
15 DateTime now = DateTime.now();
16 late Color color;
17
18 if (now.hour < 12) {
19 color = const Color(0xFF000000);
20 }
21 else {
22 color = const Color(0xFFFFFFFF);
23 }
24
25 return Cat._(
26 id: json['id'],
27 name: json['name'],
28 age: json['age'],
29 color: color,
30 );
31 }
32
33 void meow() {
34 print('Meow!');
35 }
36
37 void whoAmI() {
38 print('I am $name ($id) and I am $age years old. My color is $color.');
39 }
40}
Here, the initialization of the color
variable requires a bit of logic. Because there are multiple statements to be made, this better be handled inside a factory
constructor.
Let’s call the fromJson
constructor and inspect its output:
1const String myJson = '{"id": 5, "name": "Herbert", "age": 7}';
2final Cat decodedCat = Cat.fromJson(jsonDecode(myJson));
3
4decodedCat.meow();
5decodedCat.whoAmI();
This results in the following output:
"Meow!
I am Herbert (5) and I am 7 years old. My color is Color(0xffffffff)."— decodedCat
Instance of a subtype
Another use case for a factory
constructor is to return an instance of a derived class. This is not possible with the generative constructor.
It can be useful if the logic for deciding, which subclass to return, is always the same throughout your application. Instead of duplicating it, you might consider encapsulating it in a central place.
1abstract class Cat {
2 Cat({required this.age});
3
4 int age;
5
6 factory Cat.makeCat(bool aggressive, int age) {
7 if (aggressive || age < 3) {
8 return AggressiveCat(age: age);
9 }
10
11 return DefensiveCat(age: age);
12 }
13
14 void fight();
15}
16
17class AggressiveCat extends Cat {
18 AggressiveCat({required super.age});
19
20 @override
21 void fight() {
22 print('Where dem enemies at?!');
23 }
24}
25
26class DefensiveCat extends Cat {
27 DefensiveCat({required super.age});
28
29 @override
30 void fight() {
31 print('Nah, I\'m staying!');
32 }
33}
Difference between a generative constructor and a factory
constructor
What we have learned: a generative constructor always returns a new instance of the class, which is why it doesn’t need the return
keyword.
A factory
constructor on the other hand is bound to a lot looser constraints. For a factory
constructor, it suffices if the class it returns is the same type as the class or it satisfies its interface (e. g. a subclass). This could be a new instance of the class, but could also be an existing instance of the class (as seen in the cache example above).
A factory can use control flow to determine what object to return, and must therefore utilize the return
keyword. In order for a factory to return a new class instance, it must first call a generative constructor.
All of the minor and major differences are listed in the following breakdown:
- Factory constructors can invoke another constructor (and needs to if it doesn’t return an existing instance)
- Factory constructors are unable to use an initializer list (because they do not directly create a new instance)
- Factory constructors as opposed to a generative constructor are permitted to return an existing object
- Factory constructors are permitted to return a subclass
- Factory constructors don’t need to initialize instance variables of the class
- A derived class cannot invoke a factory constructor of the superclass. As a consequence, a class providing solely factory constructors can’t be extended.
- The compiler will complain otherwise: “The generative constructor is expected, but a factory was found”
- Generative constructors can not set final properties in the constructor body
- Generative constructors can be
const
and don’t need to be redirecting
Difference between static
and factory
You might ask yourself: “But why do I need this keyword? Can’t I just use usual static methods?!”.
In fact, there is not much difference between a static
method and a factory
constructor. Although, the syntax differs a bit.
Generally speaking, a static
method has looser constraints but also less syntactic sugar. That’s because every factory
constructor is (technically) a static
method, but not every static
method is a factory
constructor. So if you define a factory
constructor, the compiler knows your intention and can support you.
The biggest difference is probably that the return type of a factory
constructor is set to the current class or derived classes while for a static method you can provide any return type.
If we use one of the above examples, we can see that we can achieve the same result with a static
method:
1class Cat {
2 Cat._({
3 required this.id,
4 required this.name,
5 required this.age,
6 required this.color
7 });
8
9 final int id;
10 final String name;
11 final int age;
12 final Color color;
13
14 static Cat catfromJson(Map<String, dynamic> json) {
15 DateTime now = DateTime.now();
16 late Color color;
17
18 if (now.hour < 12) {
19 color = const Color(0xFF000000);
20 }
21 else {
22 color = const Color(0xFFFFFFFF);
23 }
24
25 return Cat._(
26 id: json['id'],
27 name: json['name'],
28 age: json['age'],
29 color: color,
30 );
31 }
32
33 factory Cat.fromJson(Map<String, dynamic> json) {
34 DateTime now = DateTime.now();
35 late Color color;
36
37 if (now.hour < 12) {
38 color = const Color(0xFF000000);
39 }
40 else {
41 color = const Color(0xFFFFFFFF);
42 }
43
44 return Cat._(
45 id: json['id'],
46 name: json['name'],
47 age: json['age'],
48 color: color,
49 );
50 }
51
52 void meow() {
53 print('Meow');
54 }
55
56 void whoAmI() {
57 print('I am $name ($id) and I am $age years old. My color is $color.');
58 }
59}
In terms of code readability it is still a good practice to use a factory
constructor instead of static
methods when creating instances. This makes the purpose (object creation) more obvious.
To give you a complete overview, I have listed all of the differences in the following breakdown:
- A
factory
constructor as opposed to astatic
method can only return instance of the current class or subclasses - A
static
method can beasync
. Sincefactory
constructors need to return an instance of the current or subclass, it is unable to return a Future - A
static
method can not be unnamed whereasfactory
constructors can - If you specify a named
factory
constructor, the default constructor is automatically removed - Factory constructors can use a special syntax for redirecting
- It’s not mandatory for a factory constructor to specify generic parameters
- Factory constructors can be declared
const
- A factory constructor can not return a nullable type.
- When generating dartdoc documentation, the
factory
constructors will be listed under “Constructors”.static
method will be found elsewhere at the bottom of the documentation
So the devil is in the details but from a low-level perspective, it doesn’t matter if you use a static
method or factory
constructor.
Conclusion
The factory
keyword can be helpful if the instance creation of a class exceeds a certain complexity. This can be the case for caching or when using otherwise complex logic.
Apart from that, nothing goes against using a static
method. Although factory
constructors are exactly meant for instance creation whereas static
method have a lot broader field of use.
Ultimately, it comes down to personal preference.
Comment this 🤌