Factory Method in Flutter
The Factory Method, one of the well-known “Gang of Four” design patterns.
🚫 The Limitation of Using Constructors for Object Creation
When we learn programming, we usually create objects using constructors. But is this always the best idea? 🤔
Not really. The big issue? You must know the exact class type to instantiate. 🏗️
⚠️ Why Is This a Problem?
While constructors seem convenient, what if your app needs to handle objects you haven’t thought of yet? 🤯
🎮 Example: Tower Defense Game
Let’s say you’re building a tower defense game 🏰, where users choose different towers. You might not know which towers they’ll prefer — or which new ones you’ll add in the future. 💡
🔧 Traditional Approach
- You create a Splash Tower 💦 using one constructor.
- A Long-Range Tower 🎯 has another constructor.
- Same for a Slow Tower 🐢.
Seems fine, right? ✅ But when you want to add more towers later, it becomes problematic. 🆕
❓ The Problem
This method makes your code rigid. 🛠️ You’ll need to constantly update constructors for new types, leading to tight coupling and maintenance headaches. 🚧
🛠️ The Solution: Factory Method
The Factory Method is a creational design pattern that abstracts the process of creating instances of specific classes, allowing flexibility in deciding which class to instantiate. So it provides a way to create objects without revealing the instantiation process to the client, ensuring better encapsulation and flexibility.
There are two main and crucial points in understanding this pattern.
- Objects are created by calling a factory method instead of calling a
constructor. - Objects are created through an
abstraction, not a concretion.
So in the factory method design pattern, we create objects without exposing the creation logic to the caller. And the caller refers to the newly created object through a common interface.
Points to use Factory Method Pattern in the code:
- Complex Object Creation: When object creation involves complex logic that you don’t want to repeat throughout your codebase.
- Code Decoupling: When you want to decouple the code that uses objects from the concrete classes that create them.
- Flexibility: When you anticipate needing to add new types of objects without changing the code that uses them.
- SOLID Principles Violation: When your code violates the SOLID principles, particularly the Open/Closed Principle (O), which states that a class should be open for extension but closed for modification.
Example
Let’s consider a simple example using a factory method pattern:
import 'dart:collection';
abstract class Shape {
double calculateArea();
}
class Circle implements Shape {
final double radius;
Circle(this.radius);
@override
double calculateArea() {
return 3.14 * radius * radius;
}
}
class Square implements Shape {
final double side;
Square(this.side);
@override
double calculateArea() {
return side * side;
}
}
class ShapeFactory {
// Cache to store created objects
static final HashMap<String, Shape> _cache = HashMap();
// Factory method with caching
static Shape getShape(String type, double dimension) {
if (_cache.containsKey(type)) {
print('Returning cached $type');
return _cache[type]!;
} else {
Shape shape;
switch (type) {
case 'circle':
shape = Circle(dimension);
break;
case 'square':
shape = Square(dimension);
break;
default:
throw Exception('Unknown shape type');
}
_cache[type] = shape; // Cache the created object
print('Creating and caching new $type');
return shape;
}
}
}
void main() {
// First time creation - not cached yet
Shape circle1 = ShapeFactory.getShape('circle', 5);
print('Circle 1 Area: ${circle1.calculateArea()}');
// Second time - should return the cached object
Shape circle2 = ShapeFactory.getShape('circle', 5);
print('Circle 2 Area: ${circle2.calculateArea()}');
// First time creation for square - not cached yet
Shape square1 = ShapeFactory.getShape('square', 4);
print('Square 1 Area: ${square1.calculateArea()}');
}Explanation of the Example:
- Cache Mechanism: A
HashMapis used to store cached objects. The key is the type of shape (e.g., "circle" or "square"), and the value is theShapeobject. - Factory Method with Caching: The
getShapemethod first checks if an object of the requested type is already in the cache:
. If it is, the cached object is returned, avoiding the need to create a new instance.
. If it isn’t, a new object is created, added to the cache, and then returned. - Efficiency Gain: When you request the same type of shape multiple times, the factory returns the cached object instead of creating a new one. This can save time and resources, especially in cases where object creation is costly.
- Output of the Example:
. The first call toShapeFactory.getShape('circle', 5)creates a newCircleobject and caches it.
. The second call toShapeFactory.getShape('circle', 5)returns the cachedCircleobject.
. WhenShapeFactory.getShape('square', 4)is called for the first time, a newSquareobject is created and cached.
Caching objects in the factory can be particularly useful when:
- Object Creation is Expensive: If creating an object is resource-intensive (e.g., involves complex initialization or accessing external resources), caching allows you to reuse an already created instance.
- Memory Management: By reusing objects, you reduce memory usage and potential overhead associated with creating and destroying objects frequently.
- Consistency: Ensures that the same instance is used in multiple places, which can be important for managing state or shared resources.
Benefits of Using the Factory Pattern:
- Decoupling Object Creation: The code that uses the shapes (
mainfunction) is decoupled from the concrete classes likeCircle,Square, orRectangle. It only interacts with theShapeinterface. - Centralized Object Creation: All the logic for creating different types of shapes is centralized in the
ShapeFactory, making it easy to modify or extend. - Extensibility: If you need to add a new shape (e.g.,
Triangle), you just need to:
. Implement theShapeinterface in the new class.
. Update theShapeFactorywith the new creation logic. - Simplified Code Maintenance: Changes to object creation logic are isolated within the factory, making the code easier to maintain and extend.
- Single Responsibility Principle. You can move the creation code into one place in the program, making the code easier to support.
- Open/Closed Principle. You can introduce new subtypes into the program without breaking existing client code. This results in Clean code.
🚫 When not to use:
There are no specific restrictions as to when NOT to use it. 🛠️ Feel free to apply it based on your needs!
In Simple Words: The Factory pattern and polymorphism, you would indeed use Shape as a type. By defining an abstract class or interface Shape, you create a common contract that all concrete shape classes (like Circle, Square, Rectangle) must follow. This allows you to treat all these different shapes as a Shape type, enabling polymorphism.
Define an interface for creating an object, but let subclasses decide which class to instantiate. Factory Method lets a class defer instantiation to subclasses.
Did you know you can clap for an article up to 50 times? Give it a try!
I welcome your feedback in the comments. I would also appreciate the opportunity to connect with you on LinkedIn!
