Intermediate

C-Sharp Design Patterns Factory and Abstract Factory

This course covers the creation patterns Factory and Abstract Factory in C#. Filip Ekberg, principal consultant and CEO of an agency based in Gothenburg, Sweden, guides learners through t...

Level: Intermediate Prerequisites: Basic knowledge of C# syntax and compiling/executing .NET applications


Table of Contents

  1. Course Overview
  2. Introduction to factory patterns
  1. Demo 1 — Simple Factory (First preview)
  1. Demo 2 — Factory Method Pattern
  1. Demo 3 — Abstract Factory Pattern
  1. Demo 4 — Factory Pattern in the tests
  1. Demo 5 — Factory Provider (dynamic loading)
  1. Comparison of the three variants
  2. Advantages and rewards
  3. Summary and conclusion

1. Course Overview

This course covers the creation patterns Factory and Abstract Factory in C#. Filip Ekberg, principal consultant and CEO of an agency based in Gothenburg, Sweden, guides learners through the different variations of these patterns.

Course objectives:

  • Understand and identify the characteristics of the different variants of factory patterns
  • Learn to implement the simple factory, the factory method and the abstract factory in C#
  • Understand the advantages and trade-offs of each variant
  • Know how to implement these patterns in new and existing solutions

Expected outcome: At the end of the course, you will be able to implement these three patterns and build more extensible, maintainable and testable applications.


2. Introduction to factory patterns

2.1 What is a factory?

The simplest way to explain the concept of a factory is to look at factories in the real world. The purpose of a factory is to produce a product. For example, a telephone is produced in a factory. As a consumer, you don’t care about the manufacturing process — you only care about the finished product. An advantage of this factory is that several customers can obtain these phones without worrying about the creation process.

Translated into code:

A factory is an object for creating objects. The goal is to move the creation (instantiation) of a class to a separate object.

Important tip: Do not move every instantiation of your application into a factory. The factory is used to allow code reuse and to introduce extensibility.

2.2 The three variants of factory patterns

There are three variants (according to some sources, only two):

VariantDescription
Simple FactoryThe simplest form. A separate class whose purpose is to produce an object instance.
Factory Method PatternIntroduces an abstract base class whose subclasses can override product creation.
Abstract Factory PatternEncapsulates a group of individual factories with a common theme, without specifying their concrete classes.

Note on terminology: The term “factory pattern” is a programming idiom. Most of the time, “factory pattern” refers to the simple factory or the factory method.

2.3 Common characteristics

Whatever the variant, all three share common characteristics:

Client  -->  Creator (Factory)  -->  Product
  • Client: asks a creator (the factory) to produce a product
  • Creator: facilitates the creation of the requested product (can take parameters to determine which variant to produce)
  • Product: the result of the creation

Concrete example used in this course:

  • Customer = ShoppingCart (shopping cart in an e-commerce system)
  • Creator = ShippingProviderFactory (shipping provider factory)
  • Product = ShippingProvider (country specific shipping provider)

The cart does not need to know how to instantiate a country-specific delivery provider — that is the role of the factory.


3. Demo 1 — Simple Factory (First preview)

3.1 The problem: code before refactoring

In the initial state, the ShoppingCart directly contains the delivery provider instantiation logic. Here is the problem code:

// Business/ShoppingCart.cs — AVANT refactorisation (Start_Here)
public string Finalize()
{
    ShippingProvider shippingProvider;

    if (order.Sender.Country == "Australia")
    {
        var shippingCostCalculator = new ShippingCostCalculator(
            internationalShippingFee: 250,
            extraWeightFee: 500)
        {
            ShippingType = ShippingType.Standard
        };
        var customsHandlingOptions = new CustomsHandlingOptions
        {
            TaxOptions = TaxOptions.PrePaid
        };
        var insuranceOptions = new InsuranceOptions
        {
            ProviderHasInsurance = false,
            ProviderHasExtendedInsurance = false,
            ProviderRequiresReturnOnDamange = false
        };
        shippingProvider = new AustraliaPostShippingProvider("CLIENT_ID",
            "SECRET", shippingCostCalculator, customsHandlingOptions, insuranceOptions);
    }
    else if (order.Sender.Country == "Sweden")
    {
        var shippingCostCalculator = new ShippingCostCalculator(
            internationalShippingFee: 50,
            extraWeightFee: 100)
        {
            ShippingType = ShippingType.Express
        };
        var customsHandlingOptions = new CustomsHandlingOptions
        {
            TaxOptions = TaxOptions.PayOnArrival
        };
        var insuranceOptions = new InsuranceOptions
        {
            ProviderHasInsurance = true,
            ProviderHasExtendedInsurance = false,
            ProviderRequiresReturnOnDamange = false
        };
        shippingProvider = new SwedishPostalServiceShippingProvider("API_KEY",
            shippingCostCalculator, customsHandlingOptions, insuranceOptions);
    }
    else
    {
        throw new NotSupportedException("No shipping provider found for origin country");
    }

    order.ShippingStatus = ShippingStatus.ReadyForShippment;
    return shippingProvider.GenerateShippingLabelFor(order);
}

Identified problems:

  • The ShoppingCart must know CLIENT_ID, SECRET, API_KEY, cost calculators, customs and insurance options
  • This knowledge should not be in a shopping cart
  • The code is not reusable: each place that wants to create a ShippingProvider must duplicate this logic

3.2 The solution: introduction of the Simple Factory

The solution is to create a ShippingProviderFactory class whose sole purpose is to create an instance of ShippingProvider. As no state is preserved, a static method is appropriate.

Principle: Extract the creation block and place it in the factory. The ShoppingCart simply passes the country and receives a ShippingProvider.

// Apres refactorisation — ShoppingCart simplifie
public string Finalize()
{
    var shippingProvider = ShippingProviderFactory.CreateShippingProvider(order.Sender.Country);
    order.ShippingStatus = ShippingStatus.ReadyForShippment;
    return shippingProvider.GenerateShippingLabelFor(order);
}

Benefits of Simple Factory:

  • The ShoppingCart is no longer aware of the provider’s implementation details
  • CreateShippingProvider method is reusable throughout the application (in tests, in other components, etc.)
  • Creation logic is centralized in one place

3.3 Full code of Demo 1

Abstract class ShippingProvider

// Business/Models/Shipping/ShippingProvider.cs
public abstract class ShippingProvider
{
    public ShippingCostCalculator ShippingCostCalculator { get; protected set; }
    public CustomsHandlingOptions CustomsHandlingOptions { get; protected set; }
    public InsuranceOptions InsuranceOptions { get; protected set; }
    public bool RequireSignature { get; set; }

    public abstract string GenerateShippingLabelFor(Order order);
}

public class InsuranceOptions
{
    public bool ProviderHasInsurance { get; set; }
    public bool ProviderHasExtendedInsurance { get; set; }
    public bool ProviderRequiresReturnOnDamange { get; set; }
}

public class CustomsHandlingOptions
{
    public TaxOptions TaxOptions { get; set; }
}

public class ShippingCostCalculator
{
    private readonly decimal internationalShippingFee;
    private readonly decimal extraWeightFee;

    public ShippingType ShippingType { get; set; }

    public ShippingCostCalculator(decimal internationalShippingFee,
        decimal extraWeightFee,
        ShippingType shippingType = ShippingType.Standard)
    {
        this.internationalShippingFee = internationalShippingFee;
        this.extraWeightFee = extraWeightFee;
        ShippingType = shippingType;
    }

    public decimal CalculateFor(string destinationCountry, string originCountry, decimal weight)
    {
        decimal total = 10m; // Cout de base : 10$

        if (destinationCountry != originCountry)
            total += internationalShippingFee;

        if (weight > 5)
            total += extraWeightFee;

        switch (ShippingType)
        {
            case ShippingType.Express: total += 20; break;
            case ShippingType.NextDay: total += 50; break;
        }

        return total;
    }
}

public enum TaxOptions { PrePaid, DutyFree, PayOnArrival }
public enum ShippingType { Standard, Express, NextDay }
public enum ShippingStatus { WaitingForPayment, ReadyForShippment, Shipped }

The Simple Factory

// Business/Models/Shipping/ShippingProviderFactory.cs
public class ShippingProviderFactory
{
    public static ShippingProvider CreateShippingProvider(string country)
    {
        ShippingProvider shippingProvider;

        if (country == "Australia")
        {
            var shippingCostCalculator = new ShippingCostCalculator(
                internationalShippingFee: 250,
                extraWeightFee: 500)
            {
                ShippingType = ShippingType.Standard
            };
            var customsHandlingOptions = new CustomsHandlingOptions
            {
                TaxOptions = TaxOptions.PrePaid
            };
            var insuranceOptions = new InsuranceOptions
            {
                ProviderHasInsurance = false,
                ProviderHasExtendedInsurance = false,
                ProviderRequiresReturnOnDamange = false
            };
            shippingProvider = new AustraliaPostShippingProvider("CLIENT_ID",
                "SECRET", shippingCostCalculator, customsHandlingOptions, insuranceOptions);
        }
        else if (country == "Sweden")
        {
            var shippingCostCalculator = new ShippingCostCalculator(
                internationalShippingFee: 50,
                extraWeightFee: 100)
            {
                ShippingType = ShippingType.Express
            };
            var customsHandlingOptions = new CustomsHandlingOptions
            {
                TaxOptions = TaxOptions.PayOnArrival
            };
            var insuranceOptions = new InsuranceOptions
            {
                ProviderHasInsurance = true,
                ProviderHasExtendedInsurance = false,
                ProviderRequiresReturnOnDamange = false
            };
            shippingProvider = new SwedishPostalServiceShippingProvider("API_KEY",
                shippingCostCalculator, customsHandlingOptions, insuranceOptions);
        }
        else
        {
            throw new NotSupportedException("No shipping provider found for origin country");
        }

        return shippingProvider;
    }
}

ShoppingCart refactors

// Business/ShoppingCart.cs — APRES refactorisation
public class ShoppingCart
{
    private readonly Order order;

    public ShoppingCart(Order order)
    {
        this.order = order;
    }

    public string Finalize()
    {
        var shippingProvider = ShippingProviderFactory.CreateShippingProvider(order.Sender.Country);
        order.ShippingStatus = ShippingStatus.ReadyForShippment;
        return shippingProvider.GenerateShippingLabelFor(order);
    }
}

The entry point (Program.cs)

// Program.cs
static void Main(string[] args)
{
    Console.Write("Recipient Country: ");
    var recipientCountry = Console.ReadLine().Trim();

    Console.Write("Sender Country: ");
    var senderCountry = Console.ReadLine().Trim();

    Console.Write("Total Order Weight: ");
    var totalWeight = Convert.ToInt32(Console.ReadLine().Trim());

    var order = new Order
    {
        Recipient = new Address { To = "Filip Ekberg", Country = recipientCountry },
        Sender = new Address { To = "Someone else", Country = senderCountry },
        TotalWeight = totalWeight
    };

    order.LineItems.Add(new Item("CSHARP_SMORGASBORD", "C# Smorgasbord", 100m), 1);
    order.LineItems.Add(new Item("CONSULTING", "Building a website", 100m), 1);

    var cart = new ShoppingCart(order);
    var shippingLabel = cart.Finalize();
    Console.WriteLine(shippingLabel);
}

4. Demo 2 — Factory Method Pattern

4.1 Why switch to the Factory Method?

The Simple Factory has an important limitation: it is not extensible. If we want to add a new delivery provider (GlobalExpressShippingProvider), we must modify the if/else block of the existing factory, which violates the Open/Closed principle (open for extension, closed for modification).

The Factory Method Pattern solves this problem by introducing an abstract base class whose subclasses can override the creation method.

When someone talks about the “factory pattern”, they are most often referring to the Factory Method Pattern, because it is this that offers true extensibility.

4.2 Architecture of the Factory Method

ShippingProviderFactory (abstract)
├── CreateShippingProvider(country) : abstract
└── GetShippingProvider(country) : concrete
      |-- appelle CreateShippingProvider()
      |-- peut modifier le produit avant de le retourner

StandardShippingProviderFactory : ShippingProviderFactory
└── CreateShippingProvider(country) → AustraliaPostShippingProvider | SwedishPostalServiceShippingProvider

GlobalExpressShippingProviderFactory : ShippingProviderFactory
└── CreateShippingProvider(country) → GlobalExpressShippingProvider

Two distinct methods:

  • CreateShippingProvider: the factory method — abstract method overridden by subclasses to determine how the product is created
  • GetShippingProvider: template method — calls CreateShippingProvider and can apply common changes before returning the product to the customer

Example of common logic in GetShippingProvider: When the country of origin is Sweden and the supplier has insurance, we disable the required signature. This change automatically applies to all providers created via subclasses.

4.3 Implementation of the Factory Method

Refactoring steps:

  1. Create a Factories folder to group the factories
  2. Rename the old ShippingProviderFactory to StandardShippingProviderFactory
  3. Create the abstract class ShippingProviderFactory as a base class
  4. Inherit StandardShippingProviderFactory from this new base
  5. Create GlobalExpressShippingProviderFactory as a new implementation
  6. Modify the ShoppingCart to receive the factory in dependency injection

Key advantage: We can add new suppliers (e.g.: DenmarkExpressShippingProviderFactory) without modifying the existing code.

4.4 Full code of Demo 2

Abstract class ShippingProviderFactory (base)

// Business/Models/Shipping/Factories/ShippingProviderFactory.cs
public abstract class ShippingProviderFactory
{
    // La factory method — les sous-classes determinent comment le produit est cree
    public abstract ShippingProvider CreateShippingProvider(string country);

    // Methode template — applique une logique commune apres la creation
    public ShippingProvider GetShippingProvider(string country)
    {
        var provider = CreateShippingProvider(country);

        // Logique commune : pour la Suede avec assurance, pas de signature requise
        if (country == "Sweden" && provider.InsuranceOptions.ProviderHasInsurance)
        {
            provider.RequireSignature = false;
        }

        return provider;
    }
}

StandardShippingProviderFactory

// Business/Models/Shipping/Factories/StandardShippingProviderFactory.cs
public class StandardShippingProviderFactory : ShippingProviderFactory
{
    public override ShippingProvider CreateShippingProvider(string country)
    {
        ShippingProvider shippingProvider;

        if (country == "Australia")
        {
            var shippingCostCalculator = new ShippingCostCalculator(
                internationalShippingFee: 250, extraWeightFee: 500)
            { ShippingType = ShippingType.Standard };

            var customsHandlingOptions = new CustomsHandlingOptions
            { TaxOptions = TaxOptions.PrePaid };

            var insuranceOptions = new InsuranceOptions
            { ProviderHasInsurance = false, ProviderHasExtendedInsurance = false,
              ProviderRequiresReturnOnDamange = false };

            shippingProvider = new AustraliaPostShippingProvider("CLIENT_ID",
                "SECRET", shippingCostCalculator, customsHandlingOptions, insuranceOptions);
        }
        else if (country == "Sweden")
        {
            var shippingCostCalculator = new ShippingCostCalculator(
                internationalShippingFee: 50, extraWeightFee: 100)
            { ShippingType = ShippingType.Express };

            var customsHandlingOptions = new CustomsHandlingOptions
            { TaxOptions = TaxOptions.PayOnArrival };

            var insuranceOptions = new InsuranceOptions
            { ProviderHasInsurance = true, ProviderHasExtendedInsurance = false,
              ProviderRequiresReturnOnDamange = false };

            shippingProvider = new SwedishPostalServiceShippingProvider("API_KEY",
                shippingCostCalculator, customsHandlingOptions, insuranceOptions);
        }
        else
        {
            throw new NotSupportedException("No shipping provider found for origin country");
        }

        return shippingProvider;
    }
}

GlobalExpressShippingProviderFactory

// Business/Models/Shipping/Factories/GlobalExpressShippingProviderFactory.cs
public class GlobalExpressShippingProviderFactory : ShippingProviderFactory
{
    public override ShippingProvider CreateShippingProvider(string country)
    {
        return new GlobalExpressShippingProvider();
    }
}

ShoppingCart with dependency injection

// Business/ShoppingCart.cs
public class ShoppingCart
{
    private readonly Order order;
    private readonly ShippingProviderFactory shippingProviderFactory;

    public ShoppingCart(Order order, ShippingProviderFactory shippingProviderFactory)
    {
        this.order = order;
        this.shippingProviderFactory = shippingProviderFactory;
    }

    public string Finalize()
    {
        // Le ShoppingCart ne sait plus quelle factory concrète est utilisee
        var shippingProvider = shippingProviderFactory.CreateShippingProvider(order.Sender.Country);
        order.ShippingStatus = ShippingStatus.ReadyForShippment;
        return shippingProvider.GenerateShippingLabelFor(order);
    }
}

Program.cs — choice of factory

// Program.cs
var cart = new ShoppingCart(order, new StandardShippingProviderFactory());
// ou : new ShoppingCart(order, new GlobalExpressShippingProviderFactory());
var shippingLabel = cart.Finalize();
Console.WriteLine(shippingLabel);

Explanation: The program decides which factory implementation to pass to the ShoppingCart. We could, for example, pass GlobalExpressShippingProviderFactory to VIP users and StandardShippingProviderFactory to everyone else.


5. Demo 3 — Abstract Factory Pattern

5.1 Definition and objective

The Abstract Factory Pattern provides a way to encapsulate a group of individual factories that have a common theme, without specifying their concrete classes.

Warning: Even if the Factory Method Pattern uses an abstract class, do not confuse it with the Abstract Factory Pattern. They are two different things.

When to use the Abstract Factory? Not all applications that use the Simple Factory or Factory Method need the Abstract Factory. We apply it when we want to create groups of related objects that share a common theme.

In our example: The shopping cart now needs to create several objects linked to an order:

  • A shipping provider (ShippingProvider)
  • An invoice (IInvoice)
  • An order summary (ISummary)

These three objects have a common theme: they all depend on an order (Order). And their implementation varies depending on the country (Australia vs Sweden).

5.2 Architecture of the Abstract Factory

IPurchaseProviderFactory (interface)
├── CreateShippingProvider(order) → ShippingProvider
├── CreateInvoice(order) → IInvoice
└── CreateSummary(order) → ISummary

AustraliaPurchaseProviderFactory : IPurchaseProviderFactory
├── CreateShippingProvider → utilise StandardShippingProviderFactory
├── CreateInvoice → retourne GSTInvoice
└── CreateSummary → retourne CsvSummary

SwedenPurchaseProviderFactory : IPurchaseProviderFactory
├── CreateShippingProvider → utilise GlobalExpressShippingProviderFactory (international)
│                             ou StandardShippingProviderFactory (local)
├── CreateInvoice → retourne NoVATInvoice (international) ou VATInvoice (local)
└── CreateSummary → retourne EmailSummary

Invoice types:

  • GSTInvoice: Goods and Services Tax (Australia)
  • VATInvoice: Value added tax (Sweden, local delivery)
  • NoVATInvoice: No VAT (Sweden, international delivery)

Reuse of existing factories: One of the strengths of this pattern is that implementations of IPurchaseProviderFactory can reuse existing factories (like StandardShippingProviderFactory).

5.3 Implementation of the Abstract Factory

Steps:

  1. Create the IInvoice and ISummary interfaces with their concrete implementations
  2. Create the IPurchaseProviderFactory interface with the creation methods
  3. Implement country-specific factories
  4. Modify the ShoppingCart to accept IPurchaseProviderFactory
  5. Choose the correct factory in Program.cs according to the sending country

5.4 Full code of Demo 3

IInvoice interface and implementations

// Business/Models/Commerce/Invoice/IInvoice.cs
public interface IInvoice
{
    byte[] GenerateInvoice();
}

// GSTInvoice.cs — facture avec taxe GST (Australie)
public class GSTInvoice : IInvoice
{
    public byte[] GenerateInvoice()
    {
        return Encoding.Default.GetBytes("Hello world from a GST Invoice");
    }
}

// VATInvoice.cs — facture avec TVA (Suede, livraison locale)
public class VATInvoice : IInvoice
{
    public byte[] GenerateInvoice()
    {
        return Encoding.Default.GetBytes("Hello world from a VAT Invoice");
    }
}

// NoVATInvoice.cs — facture sans TVA (Suede, livraison internationale)
public class NoVATInvoice : IInvoice
{
    public byte[] GenerateInvoice()
    {
        return Encoding.Default.GetBytes("Hello world from a NO VAT invoice");
    }
}

ISummary interface and implementations

// Business/Models/Commerce/Summary/ISummary.cs
public interface ISummary
{
    string CreateOrderSummary(Order order);
    void Send();
}

// EmailSummary.cs — resume envoye par email (Suede)
public class EmailSummary : ISummary
{
    public string CreateOrderSummary(Order order)
    {
        return "This is an email summary";
    }
    public void Send() { }
}

// CsvSummary.cs — resume en format CSV (Australie)
public class CsvSummary : ISummary
{
    public string CreateOrderSummary(Order order)
    {
        return "This is a CSV summary";
    }
    public void Send() { }
}

Interface IPurchaseProviderFactory

// Business/IPurchaseProviderFactory.cs
public interface IPurchaseProviderFactory
{
    ShippingProvider CreateShippingProvider(Order order);
    IInvoice CreateInvoice(Order order);
    ISummary CreateSummary(Order order);
}

AustraliaPurchaseProviderFactory

// Business/AustraliaPurchaseProviderFactory.cs
public class AustraliaPurchaseProviderFactory : IPurchaseProviderFactory
{
    public IInvoice CreateInvoice(Order order)
    {
        // L'Australie utilise toujours une facture GST
        return new GSTInvoice();
    }

    public ShippingProvider CreateShippingProvider(Order order)
    {
        // Reutilisation de la factory existante
        var shippingProviderFactory = new StandardShippingProviderFactory();
        return shippingProviderFactory.GetShippingProvider(order.Sender.Country);
    }

    public ISummary CreateSummary(Order order)
    {
        // En Australie, le resume est toujours envoye en CSV
        return new CsvSummary();
    }
}

SwedenPurchaseProviderFactory

// Business/SwedenPurchaseProviderFactory.cs
public class SwedenPurchaseProviderFactory : IPurchaseProviderFactory
{
    public IInvoice CreateInvoice(Order order)
    {
        // Livraison internationale depuis la Suede → pas de TVA
        if (order.Recipient.Country != order.Sender.Country)
        {
            return new NoVATInvoice();
        }
        // Livraison locale → TVA incluse
        return new VATInvoice();
    }

    public ShippingProvider CreateShippingProvider(Order order)
    {
        ShippingProviderFactory shippingProviderFactory;

        // Livraison internationale → GlobalExpress
        if (order.Sender.Country != order.Recipient.Country)
        {
            shippingProviderFactory = new GlobalExpressShippingProviderFactory();
        }
        else
        {
            shippingProviderFactory = new StandardShippingProviderFactory();
        }

        return shippingProviderFactory.GetShippingProvider(order.Sender.Country);
    }

    public ISummary CreateSummary(Order order)
    {
        // La Suede envoie un resume par email (traduit en suedois dans une vraie app)
        return new EmailSummary();
    }
}

ShoppingCart with IPurchaseProviderFactory

// Business/ShoppingCart.cs
public class ShoppingCart
{
    private readonly Order order;
    private readonly IPurchaseProviderFactory purchaseProviderFactory;

    public ShoppingCart(Order order, IPurchaseProviderFactory purchaseProviderFactory)
    {
        this.order = order;
        this.purchaseProviderFactory = purchaseProviderFactory;
    }

    public string Finalize()
    {
        // Creation du fournisseur de livraison
        var shippingProvider = purchaseProviderFactory.CreateShippingProvider(order);

        // Generation et envoi de la facture
        var invoice = purchaseProviderFactory.CreateInvoice(order);
        invoice.GenerateInvoice();

        // Generation et envoi du resume
        var summary = purchaseProviderFactory.CreateSummary(order);
        summary.Send();

        order.ShippingStatus = ShippingStatus.ReadyForShippment;
        return shippingProvider.GenerateShippingLabelFor(order);
    }
}

Program.cs — factory selection depending on country

// Program.cs
IPurchaseProviderFactory purchaseProviderFactory;

if (order.Sender.Country == "Sweden")
{
    purchaseProviderFactory = new SwedenPurchaseProviderFactory();
}
else if (order.Sender.Country == "Australia")
{
    purchaseProviderFactory = new AustraliaPurchaseProviderFactory();
}
else
{
    throw new NotSupportedException("Sender country has no purchase provider");
}

var cart = new ShoppingCart(order, purchaseProviderFactory);
var shippingLabel = cart.Finalize();
Console.WriteLine(shippingLabel);

6. Demo 4 — Factory Pattern in tests

6.1 Why use factories in testing?

A common goal is to achieve high test coverage. One of the obstacles to writing tests is boilerplate code: creating the objects necessary for the tests is often repetitive. Factory patterns allow you to:

  • Centralize the creation of test objects (orders, false objects, mocks)
  • Reuse this code in multiple test methods
  • Write less code while maintaining better quality

If you have already written tests, you have probably used a similar pattern at the factory without formally knowing it.

In this example, all tests in the ShoppingCartTests need to create an Order before interacting with the ShoppingCart. This is the ideal case for an OrderFactory.

6.2 OrderFactory: the Factory Method for testing

The OrderFactory uses the Factory Method Pattern:

  • CreateOrder(): abstract method (factory method) that subclasses override
  • GetOrder(): template method which calls CreateOrder() and systematically adds two items

Two implementations:

  • StandardOrderFactory: sender and recipient in Sweden (local delivery)
  • InternationalOrderFactory: sender in Denmark, recipient in Sweden (international delivery)

6.3 Full code of Demo 4

OrderFactory and its subclasses

// Tests/OrderFactory.cs
public abstract class OrderFactory
{
    // Factory method — les sous-classes determinent comment l'order est cree
    protected abstract Order CreateOrder();

    // Methode template — ajoute systematiquement 2 articles a toute commande
    public Order GetOrder()
    {
        var order = CreateOrder();

        order.LineItems.Add(new Item("CSHARP_SMORGASBORD", "C# Smorgasbord", 100m), 1);
        order.LineItems.Add(new Item("CONSULTING", "Building a website", 100m), 1);

        return order;
    }
}

// Commande locale : expediteur et destinataire en Suede
public class StandardOrderFactory : OrderFactory
{
    protected override Order CreateOrder()
    {
        return new Order
        {
            Recipient = new Address { To = "Filip Ekberg", Country = "Sweden" },
            Sender = new Address { To = "Someone else", Country = "Sweden" },
            TotalWeight = 100
        };
    }
}

// Commande internationale : expediteur au Danemark, destinataire en Suede
public class InternationalOrderFactory : OrderFactory
{
    protected override Order CreateOrder()
    {
        return new Order
        {
            Recipient = new Address { To = "Filip Ekberg", Country = "Sweden" },
            Sender = new Address { To = "Someone else", Country = "Denmark" },
            TotalWeight = 100
        };
    }
}

Tests using factories

// Tests/ShoppingCartTests.cs
[TestClass]
public class ShoppingCartTests
{
    // Test 1 : finaliser sans factory → exception attendue
    [TestMethod]
    [ExpectedException(typeof(NullReferenceException))]
    public void FinalizeOrderWithoutPurchaseProvider_ThrowsException()
    {
        var orderFactory = new StandardOrderFactory();
        var order = orderFactory.GetOrder();
        var cart = new ShoppingCart(order, null); // Pas de factory !
        var label = cart.Finalize(); // Doit lever une exception
    }

    // Test 2 : finaliser avec SwedenPurchaseProviderFactory → succes
    [TestMethod]
    public void FinalizeOrderWithSwedenPurchaseProvider_GeneratesShippingLabel()
    {
        var orderFactory = new StandardOrderFactory();
        var order = orderFactory.GetOrder();
        var purchaseProviderFactory = new SwedenPurchaseProviderFactory();
        var cart = new ShoppingCart(order, purchaseProviderFactory);
        var label = cart.Finalize();
        Assert.IsNotNull(label);
    }

    // Test 3 : finaliser deux fois → exception attendue
    [TestMethod]
    [ExpectedException(typeof(InvalidOperationException))]
    public void FinalizeAlreadyFinalizedOrder_ThrowsInvalidOperationException()
    {
        var cart = CreateShoppingCart();
        cart.Finalize();
        var label = cart.Finalize(); // Deuxieme appel → exception
        Assert.IsNotNull(label);
    }

    // Methode helper privee — reutilisee par plusieurs tests
    private ShoppingCart CreateShoppingCart(IPurchaseProviderFactory purchaseProviderFactory = null)
    {
        var orderFactory = new StandardOrderFactory();
        var order = orderFactory.GetOrder();
        var provider = purchaseProviderFactory ?? new SwedenPurchaseProviderFactory();
        return new ShoppingCart(order, provider);
    }
}

7. Demo 5 — Factory Provider (dynamic loading)

7.1 The concept of the Factory Provider

A very common pattern combined with factory patterns is to build a provider that dynamically loads all available factories in the application, via reflection, and returns the correct instance based on user input.

Major advantage: To add support for a new country (e.g.: Denmark), simply:

  1. Create class DenmarkPurchaseProviderFactory
  2. Recompile the application

No further changes are necessary in the rest of the code. The next time an order comes from Denmark, the correct factory will be automatically found and instantiated.

7.2 Full code of Demo 5

PurchaseProviderFactoryProvider

// Business/PurchaseProviderFactoryProvider.cs
public class PurchaseProviderFactoryProvider
{
    private IEnumerable<Type> factories;

    public PurchaseProviderFactoryProvider()
    {
        // Chargement par reflection de toutes les classes implementant IPurchaseProviderFactory
        factories = Assembly.GetAssembly(typeof(PurchaseProviderFactoryProvider))
            .GetTypes()
            .Where(t => typeof(IPurchaseProviderFactory).IsAssignableFrom(t));
    }

    public IPurchaseProviderFactory CreateFactoryFor(string name)
    {
        // Recherche de la factory dont le nom contient le nom passe en parametre
        var factory = factories
            .Single(x => x.Name.ToLowerInvariant().Contains(name.ToLowerInvariant()));

        // Creation d'une instance via l'Activator
        var instance = (IPurchaseProviderFactory)Activator.CreateInstance(factory);

        return instance;
    }
}

Program.cs — using Factory Provider

// Program.cs
IPurchaseProviderFactory purchaseProviderFactory;

// Plus de if/else ! Le provider charge dynamiquement la bonne factory
var factoryProvider = new PurchaseProviderFactoryProvider();
purchaseProviderFactory = factoryProvider.CreateFactoryFor(order.Sender.Country);

if (purchaseProviderFactory == null)
{
    throw new NotSupportedException("Sender country has no purchase provider");
}

var cart = new ShoppingCart(order, purchaseProviderFactory);
var shippingLabel = cart.Finalize();
Console.WriteLine(shippingLabel);

Explanation of the mechanism:

  • Assembly.GetAssembly(...) gets the current assembly
  • .GetTypes() lists all assembly types
  • .Where(t => typeof(IPurchaseProviderFactory).IsAssignableFrom(t)) only filters types that implement IPurchaseProviderFactory
  • For “Sweden”, the factory found will be SwedenPurchaseProviderFactory (because its name contains “Sweden”)
  • Activator.CreateInstance(factory) creates an instance of this factory by reflection

8. Comparison of the three variants

AppearanceSimpleFactoryFactory MethodAbstract Factory
StructureStatic class with methodAbstract class + subclassesInterface + implementations
ExtensibilityLow (modifies the if/else)Good (adds subclass)Excellent (adds an implementation)
ComplexityLowAverageHigh
Use casesCentralize a simple creationAllow creative variationsCreate groups of linked objects
Customer changeNone (static)Client receives the factory by injectionClient receives the factory by injection
PrerequisitesInherits from Simple FactoryCan reuse Factory Method

Progress diagram:

Probleme initial        Simple Factory          Factory Method         Abstract Factory
------------------      ------------------      ------------------     ------------------
ShoppingCart            ShoppingCart            ShoppingCart           ShoppingCart
  cree direct             utilise static          recoit factory         recoit IFactory
  ShippingProvider        factory.Create()        via injection          via injection
                                                                         
if (pays == "AU") {...}  ShippingProvider-       Abstract factory       IPurchase-
else if (pays == "SE")   Factory.Create-         + Standard-            ProviderFactory
{...}                    ShippingProvider         ShippingFactory        CreateShipping()
                         (pays)                  + GlobalExpress-       CreateInvoice()
                                                  Factory               CreateSummary()

9. Advantages and rewards

General advantages of factory patterns

  1. Separation of responsibilities: the client code is no longer responsible for creating objects
  2. Single Responsibility principle: the factory has only one reason to change
  3. Open/Closed principle: open to extension (new factories), closed to modification (existing code unchanged)
  4. Testability: factories facilitate the injection of dependencies and the creation of test objects
  5. Reusability: the creation logic is centralized and reusable

Counterparties and caveats

  1. Do not abuse the pattern: do not move each instantiation into a factory. Only apply it where it provides value.
  2. Increased complexity: especially for the Abstract Factory, the number of classes increases significantly
  3. Readability: if poorly applied, the code can become difficult to navigate

“Just as with any pattern, if you abuse it, the code will become unreadable.” —Filip Ekberg


10. Summary and conclusion

What the course covered

  1. Simple Factory: separation of client creation to make code reusable and eliminate duplication
  2. Factory Method Pattern: introduction of subclasses to allow extensibility without modifying existing clients
  3. Abstract Factory Pattern: introduction of implementations of a factory interface to create groups of linked objects
  4. Factory in tests: use of the pattern to reduce the boilerplate and facilitate the addition of new tests
  5. Factory Provider: dynamic loading by reflection for maximum extensibility

Key principles to remember

  • A factory is an object for creating objects
  • The goal is to decouple the client from concrete creation
  • The three variants advance in complexity and extensibility
  • The pattern can be applied in any object-oriented language
  • If you have ever written tests with helper objects, you have probably already used this pattern without naming it

Final project structure (Demo 5)

Business/
├── IPurchaseProviderFactory.cs          ← Interface de l'Abstract Factory
├── AustraliaPurchaseProviderFactory.cs  ← Implementation pour l'Australie
├── SwedenPurchaseProviderFactory.cs     ← Implementation pour la Suede
├── PurchaseProviderFactoryProvider.cs   ← Provider par reflection
├── ShoppingCart.cs                      ← Client (ne connait que l'interface)
└── Models/
    ├── Commerce/
    │   ├── Invoice/
    │   │   ├── IInvoice.cs
    │   │   ├── GSTInvoice.cs
    │   │   ├── VATInvoice.cs
    │   │   └── NoVATInvoice.cs
    │   ├── Summary/
    │   │   ├── ISummary.cs
    │   │   ├── EmailSummary.cs
    │   │   └── CsvSummary.cs
    │   ├── Item.cs
    │   ├── Order.cs
    │   ├── Payment.cs
    │   └── PaymentProvider.cs
    └── Shipping/
        ├── ShippingProvider.cs           ← Classe de base abstraite
        ├── AustraliaPostShippingProvider.cs
        ├── SwedishPostalServiceShippingProvider.cs
        ├── GlobalExpressShippingProvider.cs
        └── Factories/
            ├── ShippingProviderFactory.cs         ← Base abstraite (Factory Method)
            ├── StandardShippingProviderFactory.cs ← Implementation standard
            └── GlobalExpressShippingProviderFactory.cs ← Implementation GlobalExpress

When to choose which variant?

  • Simple Factory: when you have a single creation point with simple conditional logic that you want to centralize
  • Factory Method: when you anticipate variations in the way of creating an object type and want to allow extension without modification
  • Abstract Factory: when you have families of related objects that must be created together and vary according to a common context (country, theme, environment, etc.)

Search Terms

c-sharp · design · patterns · factory · abstract · testing · architecture · c# · .net · development · method · program.cs · interface · pattern · provider · shoppingcart · advantages · class · factories · implementations · ipurchaseproviderfactory · orderfactory · tests · variants

Interested in this course?

Contact us to book it or get a custom training plan for your team.