Factory Pattern

.NET Design Patterns and Concepts

Introduction to the Factory Pattern

The Factory Pattern is a creational design pattern that provides an interface for creating objects in a superclass, but allows subclasses to alter the type of objects that will be created. It's a powerful way to encapsulate object creation logic, promoting loose coupling and making your code more flexible and maintainable.

Instead of directly instantiating objects using their constructors, you delegate this responsibility to a factory. This factory class or method is responsible for determining which concrete class to instantiate based on certain conditions or parameters.

Why Use the Factory Pattern?

  • Decoupling: It decouples the client code from the concrete classes it needs to instantiate. The client only needs to know about the abstract product and the factory, not the specific implementation details.
  • Flexibility: It makes it easy to introduce new product types without modifying the client code. You can simply add a new concrete product and update the factory to create it.
  • Maintainability: Centralizing object creation logic in a factory simplifies maintenance and reduces the chances of errors.
  • Testability: Decoupling makes it easier to mock dependencies and unit test your code.

Types of Factory Patterns

There are a few variations of the factory pattern:

1. Simple Factory (or Static Factory Method)

A single class that contains a method to create objects. This method often takes a parameter to determine which concrete class to instantiate. While not a "GoF" pattern, it's a common and useful simplification.

C#
using System; // Product Interface public interface IProduct { void Operation(); } // Concrete Products public class ConcreteProductA : IProduct { public void Operation() { Console.WriteLine("ConcreteProductA.Operation()"); } } public class ConcreteProductB : IProduct { public void Operation() { Console.WriteLine("ConcreteProductB.Operation()"); } } // Simple Factory public static class SimpleProductFactory { public static IProduct CreateProduct(string type) { switch (type) { case "A": return new ConcreteProductA(); case "B": return new ConcreteProductB(); default: throw new ArgumentException("Invalid product type"); } } } // Client Code public class Client { public void DoSomething() { IProduct productA = SimpleProductFactory.CreateProduct("A"); productA.Operation(); IProduct productB = SimpleProductFactory.CreateProduct("B"); productB.Operation(); } }

2. Factory Method

Defines an interface for creating an object, but lets subclasses decide which class to instantiate. The Creator class declares the factory method, which returns an object of the Product type. Concrete Creators implement the factory method to return an instance of a specific Concrete Product.

Factory Method Pattern Diagram
C#
using System; // Product Interface public interface IDocument { void Open(); } // Concrete Products public class Report : IDocument { public void Open() { Console.WriteLine("Opening Report Document."); } } public class Resume : IDocument { public void Open() { Console.WriteLine("Opening Resume Document."); } } // Creator Abstract Class public abstract class Application { // The Factory Method public abstract IDocument CreateDocument(); public void NewDocument() { IDocument doc = CreateDocument(); doc.Open(); } } // Concrete Creators public class ReportApplication : Application { public override IDocument CreateDocument() { return new Report(); } } public class ResumeApplication : Application { public override IDocument CreateDocument() { return new Resume(); } } // Client Code public class Client { public void RunApplication(Application app) { app.NewDocument(); } }

3. Abstract Factory

Provides an interface for creating families of related or dependent objects without specifying their concrete classes. It's useful when you need to create multiple, interconnected objects.

Abstract Factory Pattern Diagram
C#
using System; // Abstract Products public interface IChair { void SitOn(); } public interface ITable { void PlaceItemOn(); } // Concrete Products A public class ModernChair : IChair { public void SitOn() => Console.WriteLine("Sitting on Modern Chair."); } public class ModernTable : ITable { public void PlaceItemOn() => Console.WriteLine("Placing item on Modern Table."); } // Concrete Products B public class VictorianChair : IChair { public void SitOn() => Console.WriteLine("Sitting on Victorian Chair."); } public class VictorianTable : ITable { public void PlaceItemOn() => Console.WriteLine("Placing item on Victorian Table."); } // Abstract Factory public interface IFurnitureFactory { IChair CreateChair(); ITable CreateTable(); } // Concrete Factories public class ModernFurnitureFactory : IFurnitureFactory { public IChair CreateChair() => new ModernChair(); public ITable CreateTable() => new ModernTable(); } public class VictorianFurnitureFactory : IFurnitureFactory { public IChair CreateChair() => new VictorianChair(); public ITable CreateTable() => new VictorianTable(); } // Client Code public class Client { public void FurnishRoom(IFurnitureFactory factory) { IChair chair = factory.CreateChair(); ITable table = factory.CreateTable(); chair.SitOn(); table.PlaceItemOn(); } }

When to Use the Factory Pattern

  • When a class cannot anticipate the class of objects it must create.
  • When a class wants its subclasses to specify the objects it creates.
  • When you need to manage object creation logic in a centralized place.
  • When you are working with hierarchies of objects and need to create instances of subclasses dynamically.

Benefits and Drawbacks

Benefits

  • Improved code flexibility and extensibility.
  • Reduced coupling between client code and concrete implementations.
  • Centralized object creation, making maintenance easier.
  • Supports the Open/Closed Principle.

Drawbacks

  • Can introduce more classes and complexity, especially with the Abstract Factory pattern.
  • The Simple Factory can become a monolithic "god object" if not managed well.
  • Subclasses might need to implement abstract factory methods, adding boilerplate.