DEV Community

Azizul Haque Ananto
Azizul Haque Ananto

Posted on • Originally published at Medium

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
Enter fullscreen mode Exit fullscreen mode

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}"
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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())
Enter fullscreen mode Exit fullscreen mode

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())
Enter fullscreen mode Exit fullscreen mode

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]()
Enter fullscreen mode Exit fullscreen mode

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())
Enter fullscreen mode Exit fullscreen mode

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())
Enter fullscreen mode Exit fullscreen mode

Hope this helps you to design factories 😀

Top comments (3)

Collapse
 
ananto30 profile image
Azizul Haque Ananto

Neat! Very clean-code! This seems another approach to the pattern. You tried to separate the class creation based on the file extensions. But you had to use if-else. That's what I tried to show, that using python dispatcher, we can map functions or classes to certain enums, strings, int or whatever your approach will be. We can implement in many other ways. That's the beauty of programming and Python 😃
Thanks for sharing.

 
ananto30 profile image
Azizul Haque Ananto

Yes, I completely agree with you. I haven't looked into the Protocols yet. But there are new libraries written with nominal typing. Can you please give a hint of why would you like to avoid patterns in FP, what about extensions and SOLID principles?

 
ananto30 profile image
Azizul Haque Ananto

WOW! This is truly amazing! I'm living in a cave I think, need to learn a lot about FP. That's why I started to write, I need comments of experts on my thinkings. Thanks a lot. And obviously I will look into F#, though I started to learn Rust this year. Again thank you for your valuable opinion and awesome examples.