Factory Method Pattern
Overview
Consider an export job that formats the same data as CSV or JSON. A writer is the object responsible for this conversion. If only the writer changes, the export jobs can share their workflow rather than repeat it for each format.
Factory Method lets a subclass override the method that creates a product. The base workflow uses the product through its common interface.
UML
ExportJob.Run() obtains an IWriter through Create(). CsvJob and JsonJob override Create() while sharing the base Run() implementation.
History
GoF cataloged Factory Method as a creational pattern in Design Patterns in 1994. It delegates a concrete creation choice to subclasses.1
Why use it?
A new writer can reuse the workflow. The creation method identifies where the concrete product is chosen.
A static function that returns an object is often called a factory function. The specific mechanism here is overriding a creation method through inheritance; an already-injected writer may make this hierarchy unnecessary.
Example
CsvJob creates the CSV writer, and the shared ExportJob.Run() uses it.
using System;
using System.Collections.Generic;
using System.Linq;
Console.WriteLine(new CsvJob().Run());
interface IWriter { string Write(string value); }
sealed class CsvWriter : IWriter
{
public string Write(string value) => "\"" + value.Replace("\"", "\"\"") + "\"";
}
sealed class JsonWriter : IWriter
{
public string Write(string value) => System.Text.Json.JsonSerializer.Serialize(new { value });
}
// The shared workflow uses the contract returned by the creation method.
abstract class ExportJob
{
protected abstract IWriter Create();
public string Run() => Create().Write("hello");
}
// Change only product selection and inherit the execution sequence.
sealed class CsvJob : ExportJob
{
protected override IWriter Create() => new CsvWriter();
}
sealed class JsonJob : ExportJob
{
protected override IWriter Create() => new JsonWriter();
}
"hello"Run() never constructs CsvWriter directly. Choosing a job implementation is separate from the shared workflow.
An extended example
Add a JSON job and run both through ExportJob. The JSON serializer handles string escaping rather than assembling JSON by concatenation.
using System;
using System.Collections.Generic;
using System.Linq;
foreach (ExportJob job in new ExportJob[] { new CsvJob(), new JsonJob() })
Console.WriteLine(job.Run());
interface IWriter { string Write(string value); }
sealed class CsvWriter : IWriter
{
public string Write(string value) => "\"" + value.Replace("\"", "\"\"") + "\"";
}
sealed class JsonWriter : IWriter
{
public string Write(string value) => System.Text.Json.JsonSerializer.Serialize(new { value });
}
// The shared workflow uses the contract returned by the creation method.
abstract class ExportJob
{
protected abstract IWriter Create();
public string Run() => Create().Write("hello");
}
// Change only product selection and inherit the execution sequence.
sealed class CsvJob : ExportJob
{
protected override IWriter Create() => new CsvWriter();
}
sealed class JsonJob : ExportJob
{
protected override IWriter Create() => new JsonWriter();
}
"hello"
{"value":"hello"}Run() stays the same while Create() returns different products. A Factory Method can also be one of the customizable steps inside a Template Method.
TypeScript example
ExportJob owns when to create and use a writer. CsvJob and JsonJob only choose which writer to construct, so another format does not need another copy of run().
interface IWriter { write(value: string): string; }
class CsvWriter implements IWriter {
write(value: string): string { return '"' + value.replaceAll('"', '""') + '"'; }
}
class JsonWriter implements IWriter {
write(value: string): string { return JSON.stringify({ value }); }
}
// The shared workflow uses the contract returned by the creation method.
abstract class ExportJob {
protected abstract create(): IWriter;
run(): string { return this.create().write("hello"); }
}
// Change only product selection and inherit the execution sequence.
class CsvJob extends ExportJob {
protected override create(): IWriter { return new CsvWriter(); }
}
class JsonJob extends ExportJob {
protected override create(): IWriter { return new JsonWriter(); }
}
for (const job of [new CsvJob(), new JsonJob()]) console.log(job.run());"hello"
{"value":"hello"}Create and Write become create and write in this version. These writers return CSV and JSON strings rather than writing files.
Related reading
Each C# example is a separate .NET 10 console program.
Compile the TypeScript examples for ES2022 or later and run the emitted JavaScript with Node.js.
Footnotes
- Gamma, Helm, Johnson, Vlissides. Design Patterns (1994). Publisher record. Creational patterns, Factory Method. ↩