Level: Intermediate Prerequisites: Basic knowledge of C# syntax and compiling/executing .NET applications
Table of Contents
- 3.1 The problem: code before refactoring
- 3.2 The solution: introduction of the Simple Factory
- 3.3 Full code of Demo 1
- 4.1 Why switch to the Factory Method?
- 4.2 Factory Method Architecture
- 4.3 Implementation of the Factory Method
- 4.4 Full code of Demo 2
- 5.1 Definition and objective
- 5.2 Abstract Factory Architecture
- 5.3 Implementation of the Abstract Factory
- 5.4 Full code of Demo 3
- 6.1 Why use factories in testing?
- 6.2 OrderFactory: the Factory Method for testing
- 6.3 Full code of Demo 4
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):
| Variant | Description |
|---|---|
| Simple Factory | The simplest form. A separate class whose purpose is to produce an object instance. |
| Factory Method Pattern | Introduces an abstract base class whose subclasses can override product creation. |
| Abstract Factory Pattern | Encapsulates 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
ShoppingCartmust knowCLIENT_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
ShippingProvidermust 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
ShoppingCartis no longer aware of the provider’s implementation details CreateShippingProvidermethod 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 createdGetShippingProvider: template method — callsCreateShippingProviderand 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:
- Create a
Factoriesfolder to group the factories - Rename the old
ShippingProviderFactorytoStandardShippingProviderFactory - Create the abstract class
ShippingProviderFactoryas a base class - Inherit
StandardShippingProviderFactoryfrom this new base - Create
GlobalExpressShippingProviderFactoryas a new implementation - Modify the
ShoppingCartto 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:
- Create the
IInvoiceandISummaryinterfaces with their concrete implementations - Create the
IPurchaseProviderFactoryinterface with the creation methods - Implement country-specific factories
- Modify the
ShoppingCartto acceptIPurchaseProviderFactory - Choose the correct factory in
Program.csaccording 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 overrideGetOrder(): template method which callsCreateOrder()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:
- Create class
DenmarkPurchaseProviderFactory - 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 implementIPurchaseProviderFactory- 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
| Appearance | SimpleFactory | Factory Method | Abstract Factory |
|---|---|---|---|
| Structure | Static class with method | Abstract class + subclasses | Interface + implementations |
| Extensibility | Low (modifies the if/else) | Good (adds subclass) | Excellent (adds an implementation) |
| Complexity | Low | Average | High |
| Use cases | Centralize a simple creation | Allow creative variations | Create groups of linked objects |
| Customer change | None (static) | Client receives the factory by injection | Client receives the factory by injection |
| Prerequisites | — | Inherits from Simple Factory | Can 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
- Separation of responsibilities: the client code is no longer responsible for creating objects
- Single Responsibility principle: the factory has only one reason to change
- Open/Closed principle: open to extension (new factories), closed to modification (existing code unchanged)
- Testability: factories facilitate the injection of dependencies and the creation of test objects
- Reusability: the creation logic is centralized and reusable
Counterparties and caveats
- Do not abuse the pattern: do not move each instantiation into a factory. Only apply it where it provides value.
- Increased complexity: especially for the Abstract Factory, the number of classes increases significantly
- 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
- Simple Factory: separation of client creation to make code reusable and eliminate duplication
- Factory Method Pattern: introduction of subclasses to allow extensibility without modifying existing clients
- Abstract Factory Pattern: introduction of implementations of a factory interface to create groups of linked objects
- Factory in tests: use of the pattern to reduce the boilerplate and facilitate the addition of new tests
- 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