Factory Pattern & Abstraction in Python
Tags: python
Published: 2020-03-25 06:17:10

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](https://en.wikipedia.org/wiki/Factory_method_pattern), how we can implement that in Python and how to create [Abstraction](https://en.wikipedia.org/wiki/Abstraction_(computer_science)) 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.

```python
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.

```python
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!

```python
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.

```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 —

```python
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.

```python
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.

```python
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 —

```python
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 😀