23 Factory Constructor
23.1 What is a Factory Constructor?
A factory constructor in Dart is a special type of constructor that doesn’t always create a new instance of the class. Instead, it can return an existing instance, an instance of a subclass, or even null (in nullable contexts). Think of it as a “smart constructor” that has the flexibility to decide what to return based on the input or internal logic.
This is quite different from regular constructors, which always create a new instance. In Python, you might achieve similar behavior using class methods or the __new__
method, but Dart’s factory constructors make this pattern more explicit and elegant.
23.2 Why Use Factory Constructors?
Factory constructors serve several important purposes:
Singleton Pattern: You can ensure only one instance of a class exists, which is useful for things like database connections or configuration managers.
Caching: Return cached instances instead of creating new ones every time, improving performance.
Subclass Selection: Choose which subclass to instantiate based on parameters, similar to a factory method pattern.
Validation: Perform complex validation and potentially return null or throw exceptions before object creation.
23.3 Basic Factory Constructor Syntax
Here’s the fundamental syntax:
class MyClass {
// Regular constructor
this.value);
MyClass(
// Factory constructor
factory MyClass.fromString(String input) {
// Logic to process input
return MyClass(int.parse(input));
}
final int value;
}
Notice how the factory constructor uses the factory
keyword and must explicitly return an instance using the return
statement.
23.4 Practical Example: Logger Class
Let me show you a practical example that demonstrates the power of factory constructors. We’ll create a logging system that ensures we only have one logger instance per category:
class Logger {
final String category;
// Private constructor - notice the underscore
._(this.category);
Logger
// Static map to store our logger instances
static final Map<String, Logger> _loggers = {};
// Factory constructor that implements singleton pattern per category
factory Logger(String category) {
// If we already have a logger for this category, return it
if (_loggers.containsKey(category)) {
'Returning existing logger for: $category');
print(return _loggers[category]!;
}
// Otherwise, create a new one and store it
'Creating new logger for: $category');
print(final logger = Logger._(category);
= logger;
_loggers[category] return logger;
}
void log(String message) {
'[$category] $message');
print(}
}
// Usage example
void main() {
final logger1 = Logger('Database');
final logger2 = Logger('Network');
final logger3 = Logger('Database'); // This will return the existing instance
'logger1 and logger3 are identical: ${identical(logger1, logger3)}');
print(
.log('Connection established');
logger1.log('Request sent');
logger2.log('Query executed'); // Same instance as logger1
logger3}
23.5 Factory Constructor vs Regular Constructor
Here’s a comparison that highlights the key differences:
class Shape {
final String type;
final double area;
// Regular constructor - always creates new instance
this.type, this.area);
Shape(
// Factory constructor - can return different types or cached instances
factory Shape.circle(double radius) {
final area = 3.14159 * radius * radius;
return Shape('Circle', area);
}
factory Shape.square(double side) {
final area = side * side;
return Shape('Square', area);
}
// Factory constructor with validation
factory Shape.fromArea(String type, double area) {
if (area < 0) {
throw ArgumentError('Area cannot be negative');
}
return Shape(type, area);
}
}
23.6 Advanced Example: Database Connection Pool
Here’s a more sophisticated example that shows how factory constructors can manage resource pooling, similar to connection pooling patterns you might know from other languages:
class DatabaseConnection {
final String connectionString;
final DateTime createdAt;
bool _isActive = true;
// Private constructor
._(this.connectionString) : createdAt = DateTime.now();
DatabaseConnection
// Pool of available connections
static final List<DatabaseConnection> _pool = [];
static const int maxPoolSize = 5;
// Factory constructor that manages connection pool
factory DatabaseConnection(String connectionString) {
// Try to reuse an existing connection
for (final connection in _pool) {
if (connection.connectionString == connectionString && connection._isActive) {
'Reusing existing connection');
print(return connection;
}
}
// Create new connection if pool isn't full
if (_pool.length < maxPoolSize) {
final newConnection = DatabaseConnection._(connectionString);
.add(newConnection);
_pool'Created new connection (pool size: ${_pool.length})');
print(return newConnection;
}
// If pool is full, return the oldest connection
'Pool full, returning oldest connection');
print(return _pool.first;
}
void close() {
= false;
_isActive 'Connection closed');
print(}
void query(String sql) {
if (_isActive) {
'Executing: $sql');
print(} else {
'Connection is closed');
print(}
}
}
23.7 Factory Constructors with Inheritance
Factory constructors become even more powerful when working with inheritance. They can decide which subclass to instantiate based on parameters:
abstract class Animal {
final String name;
this.name);
Animal(
// Factory constructor that chooses subclass
factory Animal.create(String type, String name) {
switch (type.toLowerCase()) {
case 'dog':
return Dog(name);
case 'cat':
return Cat(name);
default:
throw ArgumentError('Unknown animal type: $type');
}
}
void makeSound();
}
class Dog extends Animal {
String name) : super(name);
Dog(
@override
void makeSound() {
'$name says: Woof!');
print(}
}
class Cat extends Animal {
String name) : super(name);
Cat(
@override
void makeSound() {
'$name says: Meow!');
print(}
}
// Usage
void main() {
final dog = Animal.create('dog', 'Buddy');
final cat = Animal.create('cat', 'Whiskers');
.makeSound(); // Buddy says: Woof!
dog.makeSound(); // Whiskers says: Meow!
cat}
23.8 Key Points to Remember
Factory constructors in Dart provide a powerful way to control object creation. They’re particularly useful when you need to implement design patterns like Singleton, Factory Method, or Object Pool. Unlike regular constructors, they give you the flexibility to return existing instances, choose between subclasses, or perform complex validation before object creation.
The factory
keyword makes your intent clear to other developers and to the Dart analyzer, which can provide better optimization and error checking. This explicit approach aligns well with Dart’s philosophy of clear, readable code.
Would you like me to elaborate on any specific aspect of factory constructors, or shall we explore how they’re commonly used in Flutter widgets and state management?