The code is available on GitHub here
The AbstractFactory pattern is used to create a family of related objects. In order to achieve this consistency our factory will have many abstract methods that each of them, return the appropriate instance of a product through its subclasses. Those subclasses will extend our factory in order to return the desired instances. This way we ensure that each implementation of the abstract factory instantiates their own family of related objects.
On the other side, the client will call each of the abstract create methods of the factory and synthesize an algorithm. The client will be unaware of the specific instances of the factory and the products, making it decoupled from the implementations.
Next, we will use an example in order to better understand the AbstractFactory.
A fancy Logger example
In this example will build a fancy Logger, which is not just a simple logger, but analyzes, serializes and then writes the message. The objective is to have different implementation for analyzing, serializing and writing the messages depending on the environment the application is running. More specifically, in development we want to write a message to console in a CSV format and count the times each character is present on the message. However, in production we want the messages to be send by http in JSON format and with a token analyzer which counts the words in the message. From these requirements we can extract two related families of objects shown in the table below.
Environment | MessageSender | MessageSerializer | Analyzer |
---|---|---|---|
Development | ConsoleMessageSender | CsvMessageSerializer | CharacterAnalyzer |
Production | HttpMessageSender | JsonMessageSerializer | TokenAnalyzer |
Another benefit we have when implementing the AbstractFactory pattern, is that it makes easy in the future to add another family, without changing any of the existing classes and logic. This relies upon a principle, also known as Open Closed principle, that makes the factory and other key components open for extension, but closed for modification.
But how we can ensure that the LoggerClient will get the correct instances to work with? Designing the proper abstract factory contract will help us achieve the desired consistency.
Building the abstract factory
First, the contract of our abstract factory will consist of three abstract methods that return the proper instances.
public abstract class MessageFactory
{
public abstract MessageSerializer CreateMessageSerializer(string message);
public abstract Analyzer CreateAnalyzer();
public abstract MessageSender CreateSender();
}
In the code above, we return a MessageSerializer which serializes the messages to CSV, JSON or other formats, an Analyzer which analyzes the messages by counting character occurrences or implements other metrics and a MessageSender that dispatches the message to console, using HTTP or any other way.
Making the Serializer, Analyzer and Sender contracts
Next, we build the contracts (abstract classes or interfaces) for each of the objects the factory returns, the MessageSerializer, Analyzer and MessageSender.
public abstract class MessageSerializer
{
public string Message { get; }
protected MessageSerializer(string message)
{
Message = message;
}
public abstract string Description { get; }
public abstract string GetSerializedMessage();
}
public abstract class Analyzer
{
public abstract string Analyze(string message);
}
public abstract class MessageSender
{
public abstract void Send(string message);
}
All of them are abstract and this is key in order to achieve extensibility, if we want to add a new implementation in the future without changing the rest of the application.
The client
First of all, the client (LoggerClient) is decoupled from the actual implementations of each object and references the MessageFactory throughout constructor injection. It is also aware only of the abstract classes that the factory returns, making it a dependent to abstractions. This is also a way to achieve the Dependency Inversion Principle which states:
Depend upon abstractions. Do not depend upon concrete classes
The LoggerClient will call each create method of the factory to synthesize the desired algorithm, which is to analyze the message before it is serialized and finally send it to a destination. The code of the LoggerClient is shown below.
public class LoggerClient
{
private readonly MessageFactory _factory;
public LoggerClient(MessageFactory factory)
{
this._factory = factory;
}
public void Log(string message)
{
var messageSerializer = this._factory.CreateMessageSerializer(message);
var analyzer = this._factory.CreateAnalyzer();
var result = analyzer.Analyze(messageSerializer.Message);
Console.WriteLine("Analyze result: " + result);
var sender = this._factory.CreateSender();
sender.Send(messageSerializer.GetSerializedMessage());
}
}
Implementations for Serializer, Analyzer and MessageSenders contracts
In addition, we provide the implementations for each of the contracts the abstract factory returns.
Serializers
The serializers are responsible to serialize the message in another format. In our example, we implement a dummy CSV and JSON format.
public class CsvMessageSerializer : MessageSerializer
{
public CsvMessageSerializer(string message) : base(message)
{
}
public override string Description => "I am a CSV message serializer.";
public override string GetSerializedMessage()
{
return Message.Replace(" ", ",");
}
}
public class JsonMessageSerializer : MessageSerializer
{
public JsonMessageSerializer(string message) : base(message)
{
}
public override string Description => "I am a JSON message serializer.";
public override string GetSerializedMessage()
{
return "json: " + Message;
}
}
Analyzers
The analyzers provide us with useful information and insights about the messages. In our example, we just count the characters and the words of the message.
public class CharacterAnalyzer : Analyzer
{
public override string Analyze(string message)
{
string res = "";
foreach (var group in message.GroupBy(c => c))
{
res += string.Format("{0}={1} ", group.Key, group.Count());
}
return res;
}
}
public class TokenAnalyzer : Analyzer
{
public override string Analyze(string message)
{
return "Word count: " + message.Split(' ').Count();
}
}
And last, the MessageSenders
A MessageSender dispatches the message to a destination, such as console or to an endpoint via HTTP.
public class ConsoleMessageSender : MessageSender
{
public override void Send(string message)
{
Console.WriteLine("sending message:" + message + " via console");
}
}
public class HttpMessageSender : MessageSender
{
public override void Send(string message)
{
Console.WriteLine("sending message:" + message + " via HTTP");
}
}
AbstractFactory implementations
Lastly, we create two subclasses of the factory, one for development and one for production, which returns the proper implementations depending on the requirements we have.
public class DevelopmentMessageFactory : MessageFactory
{
public override MessageSerializer CreateMessageSerializer(string message)
{
return new CsvMessageSerializer(message);
}
public override Analyzer CreateAnalyzer()
{
return new CharacterAnalyzer();
}
public override MessageSender CreateSender()
{
return new ConsoleMessageSender();
}
}
class ProductionMessageFactory : MessageFactory
{
public override MessageSerializer CreateMessageSerializer(string message)
{
return new JsonMessageSerializer(message);
}
public override Analyzer CreateAnalyzer()
{
return new TokenAnalyzer();
}
public override MessageSender CreateSender()
{
return new HttpMessageSender();
}
}
Usage example
At last! We have all parts in place. The AbstractFactory with its implementations (DevelopmentMessageFactory and ProductionMessageFactory) the LoggerClient and finally, the MessageSenders, Analyzers and MessageSerializers. Following, we show how the LoggerClient can use the factory in order to obtain the proper instances. When the application is running in development we inject the DevelopmentMessageFactory and when we are on production we inject the ProductionMessageFactory instead.
public static void Main()
{
var client = new LoggerClient(new DevelopmentMessageFactory());
client.Log("a test message.");
client = new LoggerClient(new ProductionMessageFactory());
client.Log("another test message.");
}
The execution output of the code above is shown below.
Analyze result: a=2 'space'=2 t=2 e=3 s=3 m=1 g=1 .=1
sending message:a,test,message. via console
Analyze result: Word count: 3
sending message:json: another test message. via HTTP
Recommendations
For more about Factory patterns you can check also the SimpleFactory and FactoryMethod articles.
Also if you want to level up, refresh or better understand design patterns and software design in general, I also recommend reading the following books:
A must in Design Patterns
A very well written book with plenty of examples and insights