Factory Pattern & Abstraction in Python

Python seems a very interesting language, where everything is on your hand. You can write code that works or write beautiful code with the popular and beloved concepts like SOLID, clean code and design patterns. I won’t make this post long and I will try to write brief concepts about Python from now on. In this post, I will be talking about the Factory pattern, how we can implement that in Python and how to create Abstraction to make things more simple.

Let’s say we have an audio player, and we can play wav and mp3 formats. So based on the parameter wav or mp3 we load files and play them. Let’s make an interface first.

from abc import ABC, abstractmethod

class AudioPlayer(ABC):

    @abstractmethod
    def load(self, file: str) -> (str):
        pass

    @abstractmethod
    def play(self) -> (str):
        pass

I have used the abc package to implement the formal interface concept. The @abstractmethod decorator implies that these methods should be overridden by concrete classes. So let’s make the players now.

class Mp3Player(AudioPlayer):

    def __init__(self):
        self.format = "mp3"
        self.file = None

    def load(self, file: str) -> (str):
        self.file = file
        return f"Loading {self.format} file named {file}"

    def play(self) -> (str):
        return f"playing {self.file}"

class WavPlayer(AudioPlayer):

    def __init__(self):
        self.format = "wav"
        self.file = None

    def load(self, file: str) -> (str):
        self.file = file
        return f"Loading {self.format} file named {file}"

    def play(self) -> (str):
        return f"playing {self.file}"

So we have the Mp3Player and Wavplayer . They implement both methods load and play . These two classes are identical here, but in real life implementation, the load should be different, maybe the play too. Now it’s time to create the factory. Here’s the magic of Python comes in play!

player_factory = {
    'mp3': Mp3Player,
    'wav': WavPlayer
}

This is amazing! You can map classes in dictionaries, so simply! In other languages, you might have to write several switch cases or if-else. Now you can directly use this factory to call our load and play. This is called a dispatcher in Python.

mp3_player = player_factory['mp3']()
print(mp3_player.load("creep.mp3"))
print(mp3_player.play())

wav_player = player_factory['wav']()
print(wav_player.load("that's_life.wav"))
print(wav_player.play())

See how we can initialize a class based on a parameter! mp3_player = player_factory[‘mp3’]() — this is really cool. So the whole code looks like this —

from abc import ABC, abstractmethod

class AudioPlayer(ABC):

    @abstractmethod
    def load(self, file: str) -> (str):
        raise NotImplementedError

    @abstractmethod
    def play(self) -> (str):
        raise NotImplementedError

class Mp3Player(AudioPlayer):

    def __init__(self):
        self.format = "mp3"
        self.file = None

    def load(self, file: str) -> (str):
        self.file = file
        return f"Loading {self.format} file named {file}"

    def play(self) -> (str):
        return f"playing {self.file}"

class WavPlayer(AudioPlayer):

    def __init__(self):
        self.format = "wav"
        self.file = None

    def load(self, file: str) -> (str):
        self.file = file
        return f"Loading {self.format} file named {file}"

    def play(self) -> (str):
        return f"playing {self.file}"

player_factory = {
    'mp3': Mp3Player,
    'wav': WavPlayer
}

mp3_player = player_factory['mp3']()
print(mp3_player.load("creep.mp3"))
print(mp3_player.play())

wav_player = player_factory['wav']()
print(wav_player.load("that's_life.wav"))
print(wav_player.play())

Now you can ask what if a user gives mp4 in player_factory initialization, what will happen. Ok, the code will crash. Here we can make an abstraction and hide all the complexity of class creation and also validating upon the parameters.

class AudioPlayerFactory:

    player_factory = {
        'mp3': Mp3Player,
        'wav': WavPlayer
    }

    @staticmethod
    def make_player(format: str):
        if format not in AudioPlayerFactory.player_factory:
            raise Exception(f"{format} is not supported")
        return AudioPlayerFactory.player_factory[format]()

Now we can just use the AudioPlayerFactory to load and play.

mp3_player = AudioPlayerFactory.make_player('mp3')
print(mp3_player.load("creep.mp3"))
print(mp3_player.play())

wav_player = AudioPlayerFactory.make_player('wav')
print(wav_player.load("that's_life.wav"))
print(wav_player.play())

mp4_player = AudioPlayerFactory.make_player('mp4')
print(mp4_player.load("what_a_wonderful_life.mp4"))
print(mp4_player.play())

You will see the Exception for the mp4 file. You can handle that in your own way. So the new code is —

from abc import ABC, abstractmethod


class AudioPlayer(ABC):

    @abstractmethod
    def load(self, file: str) -> (str):
        raise NotImplementedError

    @abstractmethod
    def play(self) -> (str):
        raise NotImplementedError


class Mp3Player(AudioPlayer):

    def __init__(self):
        self.format = "mp3"
        self.file = None

    def load(self, file: str) -> (str):
        self.file = file
        return f"Loading {self.format} file named {file}"

    def play(self) -> (str):
        return f"playing {self.file}"


class WavPlayer(AudioPlayer):

    def __init__(self):
        self.format = "wav"
        self.file = None

    def load(self, file: str) -> (str):
        self.file = file
        return f"Loading {self.format} file named {file}"

    def play(self) -> (str):
        return f"playing {self.file}"

class AudioPlayerFactory:

    player_factory = {
        'mp3': Mp3Player,
        'wav': WavPlayer
    }

    @staticmethod
    def make_player(format: str):
        if format not in AudioPlayerFactory.player_factory:
            raise Exception(f"{format} is not supported")
        return AudioPlayerFactory.player_factory[format]()

mp3_player = AudioPlayerFactory.make_player('mp3')
print(mp3_player.load("creep.mp3"))
print(mp3_player.play())

wav_player = AudioPlayerFactory.make_player('wav')
print(wav_player.load("that's_life.wav"))
print(wav_player.play())

mp4_player = AudioPlayerFactory.make_player('mp4')
print(mp4_player.load("what_a_wonderful_life.mp4"))
print(mp4_player.play())

Hope this helps you to design factories. There’s another way to hide the complexity of a factory package. I will discuss that soon. A clap will be much appreciated.