Factory Method Design Pattern in .NET — Real-Time Finance Example
The Factory Method design
pattern is a classic solution for a common software engineering challenge: how
to create objects without tightly coupling the client code to their
concrete types. Instead of instantiating classes directly using new, Factory
Method defers object creation to subclasses or dedicated factory methods.
In this blog post, we'll explore the
Factory Method pattern in C# with a real-world banking transaction
example and also discuss its benefits, drawbacks, and best practices.
๐ก
What Is the Factory Method Pattern?
The Factory Method pattern provides
an interface for creating objects in a superclass but allows subclasses or
factory methods to alter the type of objects that will be created.
This promotes:
- Loose coupling
- Polymorphism
- Improved testability and
scalability
๐ฏ
Purpose
The key goal of Factory Method is to
encapsulate object creation logic, so your business logic doesn’t depend
on concrete classes. This becomes especially valuable in large systems where extending
functionality without modifying existing code is essential.
๐งฑ
Key Characteristics
- Defines a common interface or abstract class for
products
- Delegates instantiation to subclasses or static
factory methods
- Encourages polymorphism and code decoupling
✅
Pros
- Decouples client code from specific object types
- Aligns with the Open/Closed Principle
- Improves testability and flexibility
- Easier to add new functionality without modifying
existing code
⚠️
Cons
- Adds extra classes or interfaces, increasing
complexity
- May be overkill for simple or static object
creation needs
๐ฆ
Use Cases
Factory Method is ideal when you
need dynamic object creation, such as:
- Banking transactions (NEFT, IMPS, RTGS)
- Generating reports in different formats (PDF, Excel,
CSV)
- Creating various types of notifications (Email, SMS,
Push)
- UI components or theme-based widgets
๐ผ
Real-Time Finance Example: Bank Transaction Processing
Let’s imagine a banking system where
you need to process different types of transactions. Rather than hardcoding
logic for each transaction type, we can apply the Factory Method pattern for
dynamic creation.
1️ Define a Common Interface
public
interface IBankTransaction
{
void Process(decimal amount);
}
This interface ensures all
transaction types implement a common Process method.
2️ Implement Concrete Transaction Types
public
class NEFTTransaction : IBankTransaction
{
public void Process(decimal amount)
{
Console.WriteLine($"Processing
NEFT transaction of ₹{amount}");
}
}
public
class IMPOSTransaction : IBankTransaction
{
public void Process(decimal amount)
{
Console.WriteLine($"Processing
IMPS transaction of ₹{amount}");
}
}
public
class RTGSTransaction : IBankTransaction
{
public void Process(decimal amount)
{
Console.WriteLine($"Processing
RTGS transaction of ₹{amount}");
}
}
Each transaction class encapsulates
its specific logic.
๐งผ
Optional Improvement: Use Enum Instead of String
Avoiding raw strings can reduce
runtime errors:
public
enum TransactionType
{
NEFT,
IMPS,
RTGS
}
3️ Create the Factory
public
static class TransactionFactory
{
public static IBankTransaction
CreateTransaction(TransactionType type)
{
return type switch
{
TransactionType.NEFT => new
NEFTTransaction(),
TransactionType.IMPS => new
IMPOSTransaction(),
TransactionType.RTGS => new
RTGSTransaction(),
_ => throw new
ArgumentException("Invalid transaction type")
};
}
}
This factory method encapsulates the
creation logic, making the client code clean and decoupled.
4️ Client Code Usage
class
Program
{
static void Main()
{
// Imagine this comes from user input
or a config
TransactionType transactionType =
TransactionType.IMPS;
decimal amount = 5000;
IBankTransaction transaction =
TransactionFactory.CreateTransaction(transactionType);
transaction.Process(amount);
}
}
No direct reference to concrete
types — just interaction via the IBankTransaction interface.
๐
Advanced Concepts (Optional Enhancements)
✅
Dependency Injection (DI) Integration
You can also register each
implementation in an IoC container like Microsoft.Extensions.DependencyInjection
and use DI to resolve them dynamically using factories or keyed services.
✅
Unit Testing Benefits
By depending on IBankTransaction,
client code becomes highly testable:
[Test]
public
void Test_Transaction_Processing()
{
var mockTransaction = new
Mock<IBankTransaction>();
mockTransaction.Setup(x =>
x.Process(It.IsAny<decimal>()));
var transactionService = new
TransactionService(mockTransaction.Object);
transactionService.Execute(5000);
mockTransaction.Verify(x =>
x.Process(5000), Times.Once);
}
✅
Benefits Recap in This Scenario
- Extensible: New transaction types (e.g., UPI) require only a new
class and factory entry
- Testable: Easy to mock and test business logic
- Clean client code: No need to know about concrete classes
- Follows SOLID: Especially Open/Closed and Single
Responsibility principles
๐งพ
Summary
|
Aspect |
Benefit |
|
Decoupling |
Client code knows only interfaces |
|
Extensibility |
Add new types with minimal changes |
|
Testability |
Mocks can be easily injected |
|
Maintainability |
Factory centralizes object
creation |
The Factory Method pattern is
a robust and scalable design choice for applications where object creation
logic varies based on runtime input, configurations, or environment — such as
finance platforms that support multiple transaction types or reporting tools.
๐จ️
Final Thoughts
Factory Method isn’t always
necessary — don’t apply it for trivial cases. But in the right context, it promotes
flexibility, testability, and maintainability, especially in large systems
with evolving requirements.
Comments
Post a Comment