Design Pattern Primer: The Factory

When writing object oriented code we end up needing to create a lot of different objects. Sometimes the process to create those objects is complex or maybe we don't know exactly which type we will need to create. Situations like these are where using the Factory pattern comes in handy.

Design Pattern Primer: The Factory

When writing object oriented code we end up needing to create a lot of different objects. Sometimes the process to create those objects is complex or maybe we don't know exactly which type we will need to create.

Situations like these are where using the Factory pattern comes in handy.

The factory pattern allows us to isolate the object creation away from the consumers, keep complex object creation logic maintained in a single, reusable spot, & change which concrete implementation is used without impacting other parts of the code.


Real World Example

Here is an example of a factory that I used within an audio transcription service I built.

Audio comes in many different file types; .mp3, .mp4, or .wav for example. Before my service can transcribe the audio, it must be in a unified format: .wav.

Because of the .wav file type restriction, we need to to convert the audio to the expected format before we can begin transcribing.

The approach I took was to create a set of classes that can perform each type of file conversion we might encounter. One class for each potential file type that was uploaded, each of which implements a common interface: IAudioConverter.

Here is where our factory object comes into play.

We don't want to have to have every client that might need to convert audio duplicate the logic for knowing which IAudioConverter implementation should be used for each different filet type. Instead, we centralize this logic into a factory manages that information & can create the correct IAudioConverter for our calling code.

This keeps the concerns of the calling code simpler & abstracts away the logic of creating the object so that if it was to change, it would only change in a single place.

Our factory simply needs to know which file type you are trying to convert from & it manages the rest.

Let's look at the code.

We will start by defining the IAudioConverter interface. We know the classes that implement this interface have two responsibilities. First, they need to expose a method that tells the factory if it can convert the requested audio type. Second, it needs to perform the audio conversion for the transcription service.

public interface IAudioConverter
{
    bool CanConvert(AudioFileType audioType);

    AudioFile Convert(AudioFile originalAudio);
}

Next up is our factory. The factory will keep track of all known IAudioConverters & provide a method to get the correct IAudioConverter instance based on a specific AudioFileType.

public static class AudioConverterFactory
{
    // The collection of known IAudioConverter implementations
    private readonly IEnumerable<IAudioConverter> _converters = new IAudioConverter[]
    {
        new WavToWavConverter(),
        new MpToWavConverter()
    }

    
    public static IAudioConverter GetConverter(AudioFileType audioType)
    {
        // Find the audio converter that can be used for the audioType
        // There should be a 1-to-1 relationship for AudioFileType & IAudioConverter 
        var converter = _converters
            .SingleOrDefault(converter => converter.CanConvert(audioType));

        // If we don't find one, we cannot convert the audio
        if(converter == null)
        {
            throw new UnknownAudioFileTypeException(audioType);
        } 

        // Return the converter
        return converter;
    }
}

Our factory is pretty straight forward. We also added some logic to throw an exception if the factory is unable to find an IAudioConverter for the requested AudioFileType.

Now, we will look at the transcription service that will be using the factory to retrieve a concrete IAudioConverter implementation.

public class TranscriptionService
{
    public Transcript Transcribe(AudioFile audio)
    {
        // First, convert the audio to the correct format
        IAudioConverter converter = AudioConverterFactory.GetConverter(audio.AudioFileType);
        
        AudioFile convertedAudio = converter.Convert(audio);
        
        // TODO: Transcribe converted audio...
    }
}

The transcription service only needs to know that it requires an IAudioConverter & that the AudioConverterFactory can give it one. Once it has the IAudioConverter implementation, it just has to call the Convert method passing in its audio file.

This flow remains the same for every audio file; there is no conditional logic to check the audio format. This is to simplify the flow so that as developers read the code, they do not have to keep up with branching statements.

VoilĂ ! We have ourselves an excellent solution to the problem of not knowing which implementation of IAudioConverter is needed until the audio file is provided.


Hopefully this article helps demystify the Factory pattern a bit so you can use it to keep your own code bases clean & mantainable.