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
  MyClass(this.value);
  
  // 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
  Logger._(this.category);
  
  // 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)) {
      print('Returning existing logger for: $category');
      return _loggers[category]!;
    }
    
    // Otherwise, create a new one and store it
    print('Creating new logger for: $category');
    final logger = Logger._(category);
    _loggers[category] = logger;
    return logger;
  }
  
  void log(String message) {
    print('[$category] $message');
  }
}

// Usage example
void main() {
  final logger1 = Logger('Database');
  final logger2 = Logger('Network');
  final logger3 = Logger('Database'); // This will return the existing instance
  
  print('logger1 and logger3 are identical: ${identical(logger1, logger3)}');
  
  logger1.log('Connection established');
  logger2.log('Request sent');
  logger3.log('Query executed'); // Same instance as logger1
}

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
  Shape(this.type, this.area);
  
  // 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
  DatabaseConnection._(this.connectionString) : createdAt = DateTime.now();
  
  // 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) {
        print('Reusing existing connection');
        return connection;
      }
    }
    
    // Create new connection if pool isn't full
    if (_pool.length < maxPoolSize) {
      final newConnection = DatabaseConnection._(connectionString);
      _pool.add(newConnection);
      print('Created new connection (pool size: ${_pool.length})');
      return newConnection;
    }
    
    // If pool is full, return the oldest connection
    print('Pool full, returning oldest connection');
    return _pool.first;
  }
  
  void close() {
    _isActive = false;
    print('Connection closed');
  }
  
  void query(String sql) {
    if (_isActive) {
      print('Executing: $sql');
    } else {
      print('Connection is closed');
    }
  }
}

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;
  
  Animal(this.name);
  
  // 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 {
  Dog(String name) : super(name);
  
  @override
  void makeSound() {
    print('$name says: Woof!');
  }
}

class Cat extends Animal {
  Cat(String name) : super(name);
  
  @override
  void makeSound() {
    print('$name says: Meow!');
  }
}

// Usage
void main() {
  final dog = Animal.create('dog', 'Buddy');
  final cat = Animal.create('cat', 'Whiskers');
  
  dog.makeSound(); // Buddy says: Woof!
  cat.makeSound(); // Whiskers says: Meow!
}

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?