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.