Skip to content

Facade Design Pattern

Video Lecture

Section Video Links
Facade Overview Facade Overview Facade Overview 
Facade Use Case Facade Use Case Facade Use Case 
Python Decimal Python Decimal Python Decimal 
Python Type Hints Python Type Hints Python Type Hints 

Overview

Sometimes you have a system that becomes quite complex over time as more features are added or modified. It may be useful to provide a simplified API over it. This is the Facade pattern.

The Facade pattern essentially is an alternative, reduced or simplified interface to a set of other interfaces, abstractions and implementations within a system that may be full of complexity and/or tightly coupled.

It can also be considered as a higher-level interface that shields the consumer from the unnecessary low-level complications of integrating into many subsystems.

Facade UML Diagram

Facade Design Pattern

Source Code

./facade/facade_concept.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
# pylint: disable=too-few-public-methods
"The Facade pattern concept"

class SubSystemClassA:
    "A hypothetically complicated class"
    @staticmethod
    def method():
        "A hypothetically complicated method"
        return "A"

class SubSystemClassB:
    "A hypothetically complicated class"
    @staticmethod
    def method(value):
        "A hypothetically complicated method"
        return value

class SubSystemClassC:
    "A hypothetically complicated class"
    @staticmethod
    def method(value):
        "A hypothetically complicated method"
        return value

class Facade():
    "A simplified facade offering the services of subsystems"
    @staticmethod
    def sub_system_class_a():
        "Use the subsystems method"
        return SubSystemClassA().method()

    @staticmethod
    def sub_system_class_b(value):
        "Use the subsystems method"
        return SubSystemClassB().method(value)

    @staticmethod
    def sub_system_class_c(value):
        "Use the subsystems method"
        return SubSystemClassC().method(value)

# The Client
# call potentially complicated subsystems directly
print(SubSystemClassA.method())
print(SubSystemClassB.method("B"))
print(SubSystemClassC.method({"C": [1, 2, 3]}))

# or use the simplified facade
print(Facade().sub_system_class_a())
print(Facade().sub_system_class_b("B"))
print(Facade().sub_system_class_c({"C": [1, 2, 3]}))

Output

python ./facade/facade_concept.py
A
B
{'C': [1, 2, 3]}
A
B
{'C': [1, 2, 3]}

SBCODE Editor

<>

Example Use Case

This is an example of a game engine API. The facade layer is creating one streamlined interface consisting of several methods from several larger API backend systems.

The client could connect directly to each subsystem's API and implement its authentication protocols, specific methods, etc. While it is possible, it would be quite a lot of consideration for each of the development teams, so the facade API unifies the common methods that becomes much less overwhelming for each new client developer to integrate into.

Example UML Diagram

Facade Example UML Diagram

Source Code

./facade/client.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
"The Facade Example Use Case"
import time
from decimal import Decimal
from game_api import GameAPI

USER = {"user_name": "sean"}
USER_ID = GameAPI.register_user(USER)

time.sleep(1)

GameAPI.submit_entry(USER_ID, Decimal('5'))

time.sleep(1)

print()
print("---- Gamestate Snapshot ----")
print(GameAPI.game_state())

time.sleep(1)

HISTORY = GameAPI.get_history()

print()
print("---- Reports History ----")
for row in HISTORY:
    print(f"{row} : {HISTORY[row][0]} : {HISTORY[row][1]}")

print()
print("---- Gamestate Snapshot ----")
print(GameAPI.game_state())

./facade/game_api.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
"The Game API facade"
from decimal import Decimal
from users import Users
from wallets import Wallets
from game_engine import GameEngine
from reports import Reports

class GameAPI():
    "The Game API facade"
    @staticmethod
    def get_balance(user_id: str) -> Decimal:
        "Get a players balance"
        return Wallets.get_balance(user_id)

    @staticmethod
    def game_state() -> dict:
        "Get the current game state"
        return GameEngine().get_game_state()

    @staticmethod
    def get_history() -> dict:
        "get the game history"
        return Reports.get_history()

    @staticmethod
    def change_pwd(user_id: str, password: str) -> bool:
        "change users password"
        return Users.change_pwd(user_id, password)

    @staticmethod
    def submit_entry(user_id: str, entry: Decimal) -> bool:
        "submit a bet"
        return GameEngine().submit_entry(user_id, entry)

    @staticmethod
    def register_user(value: dict[str, str]) -> str:  # Python 3.9
        # def register_user(value) -> str:  # Python 3.8 and earlier
        "register a new user and returns the new id"
        return Users.register_user(value)

./facade/users.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
"A Singleton Dictionary of Users"
from decimal import Decimal
from wallets import Wallets
from reports import Reports

class Users():
    "A Singleton Dictionary of Users"
    _users: dict[str, dict[str, str]] = {}  # Python 3.9
    # _users = {}  # Python 3.8 or earlier

    def __new__(cls):
        return cls

    @classmethod
    def register_user(cls, new_user: dict[str, str]) -> str:  # Python 3.9
        # def register_user(cls, new_user) -> str:  # Python 3.8 or earlier
        "register a user"
        if not new_user["user_name"] in cls._users:
            # generate really complicated unique user_id.
            # Using the existing user_name as the id for simplicity
            user_id = new_user["user_name"]
            cls._users[user_id] = new_user
            Reports.log_event(f"new user `{user_id}` created")
            # create a wallet for the new user
            Wallets().create_wallet(user_id)
            # give the user a sign up bonus
            Reports.log_event(
                f"Give new user `{user_id}` sign up bonus of 10")
            Wallets().adjust_balance(user_id, Decimal(10))
            return user_id
        return ""

    @classmethod
    def edit_user(cls, user_id: str, user: dict):
        "do nothing"
        print(user_id)
        print(user)
        return False

    @classmethod
    def change_pwd(cls, user_id: str, password: str):
        "do nothing"
        print(user_id)
        print(password)
        return False

./facade/wallets.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
"A Singleton Dictionary of User Wallets"
from decimal import Decimal
from reports import Reports

class Wallets():
    "A Singleton Dictionary of User Wallets"
    _wallets: dict[str, Decimal] = {}  # Python 3.9
    # _wallets = {}  # Python 3.8 or earlier

    def __new__(cls):
        return cls

    @classmethod
    def create_wallet(cls, user_id: str) -> bool:
        "A method to initialize a users wallet"
        if not user_id in cls._wallets:
            cls._wallets[user_id] = Decimal('0')
            Reports.log_event(
                f"wallet for `{user_id}` created and set to 0")
            return True
        return False

    @classmethod
    def get_balance(cls, user_id: str) -> Decimal:
        "A method to check a users balance"
        Reports.log_event(
            f"Balance check for `{user_id}` = {cls._wallets[user_id]}")
        return cls._wallets[user_id]

    @classmethod
    def adjust_balance(cls, user_id: str, amount: Decimal) -> Decimal:
        "A method to adjust a user balance up or down"
        cls._wallets[user_id] = cls._wallets[user_id] + Decimal(amount)
        Reports.log_event(
            f"Balance adjustment for `{user_id}`. "
            f"New balance = {cls._wallets[user_id]}"
        )
        return cls._wallets[user_id]

./facade/reports.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
"A Singleton Dictionary of Reported Events"
import time

class Reports():
    "A Singleton Dictionary of Reported Events"
    _reports: dict[int, tuple[float, str]] = {}  # Python 3.9
    # _reports = {}  # Python 3.8 or earlier
    _row_id = 0

    def __new__(cls):
        return cls

    @classmethod
    def get_history(cls) -> dict:
        "A method to retrieve all historic events"
        return cls._reports

    @classmethod
    def log_event(cls, event: str) -> bool:
        "A method to add a new event to the record"
        cls._reports[cls._row_id] = (time.time(), event)
        cls._row_id = cls._row_id + 1
        return True

./facade/game_engine.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
"The Game Engine"
import time
from decimal import Decimal
from wallets import Wallets
from reports import Reports

class GameEngine():
    "The Game Engine"
    _instance = None
    _start_time: int = 0
    _clock: int = 0
    _entries: list[tuple[str, Decimal]] = []  # Python 3.9
    # _entries = []  # Python 3.8 or earlier
    _game_open = True

    def __new__(cls):
        if cls._instance is None:
            cls._instance = GameEngine
            cls._start_time = int(time.time())
            cls._clock = 60
        return cls._instance

    @classmethod
    def get_game_state(cls) -> dict:
        "Get a snapshot of the current game state"
        now = int(time.time())
        time_remaining = cls._start_time - now + cls._clock
        if time_remaining < 0:
            time_remaining = 0
            cls._game_open = False
        return {
            "clock": time_remaining,
            "game_open": cls._game_open,
            "entries": cls._entries
        }

    @classmethod
    def submit_entry(cls, user_id: str, entry: Decimal) -> bool:
        "Submit a new entry for the user in this game"
        now = int(time.time())
        time_remaining = cls._start_time - now + cls._clock
        if time_remaining > 0:
            if Wallets.get_balance(user_id) > Decimal('1'):
                if Wallets.adjust_balance(user_id, Decimal('-1')):
                    cls._entries.append((user_id, entry))
                    Reports.log_event(
                        f"New entry `{entry}` submitted by `{user_id}`")
                    return True
                Reports.log_event(
                    f"Problem adjusting balance for `{user_id}`")
                return False
            Reports.log_event(f"User Balance for `{user_id}` to low")
            return False
        Reports.log_event("Game Closed")
        return False

Output

python ./facade/client.py

---- Gamestate Snapshot ----
{'clock': 59, 'game_open': True, 'entries': [('sean', Decimal('5'))]}

---- Reports History ----
0 : 1614087127.327007 : new user `sean` created
1 : 1614087127.327007 : wallet for `sean` created and set to 0
2 : 1614087127.327007 : Give new user `sean` sign up bonus of 10
3 : 1614087127.327007 : Balance adjustment for `sean`. New balance = 10
4 : 1614087128.3278701 : Balance check for `sean` = 10
5 : 1614087128.3278701 : Balance adjustment for `sean`. New balance = 9
6 : 1614087128.3278701 : New entry `5` submitted by `sean`

---- Gamestate Snapshot ----
{'clock': 58, 'game_open': True, 'entries': [('sean', Decimal('5'))]}

Error

If when trying to run the examples, you get an error

TypeError: 'type' object is not subscriptable

then it is likely that you are using Python 3.8 or earlier.

Inbuilt type hint support for list and dict is new since Python 3.9.

You can remove the type hints from the code. I.e.,

_reports: dict[int, tuple[float, str]] = {} # Python 3.9

becomes

_reports = {} # Python 3.8 or earlier

See Type Hints below.

SBCODE Editor

<>

New Coding Concepts

Python decimal Module

The decimal module provides support for correctly rounded decimal floating-point arithmetic.

If representing money values in python, it is better to use the decimal type rather than float.

Floats will have rounding errors versus decimal.

1
2
3
4
from decimal import Decimal

print(1.1 + 2.2)  # adding floats
print(Decimal('1.1') + Decimal('2.2')) # adding decimals

Outputs

3.3000000000000003
3.3

Note how the float addition results in 3.3000000000000003 whereas the decimal addition result equals 3.3.

Be aware though that when creating decimals, be sure to pass in a string representation, otherwise it will create a decimal from a float.

1
2
3
4
from decimal import *

print(Decimal(1.1))  # decimal from float
print(Decimal('1.1'))  # decimal from string

Outputs

1.100000000000000088817841970012523233890533447265625
1.1

Python Decimal: https://docs.python.org/3/library/decimal.html

Type Hints

In the Facade use case example, I have added type hints to the method signatures and class attributes.

    _clock: int = 0
    _entries: list[tuple[str, Decimal]] = []

    ...

    def get_balance(user_id: str) -> Decimal:
        "Get a players balance"
        ...

    ...

    def register_user(cls, new_user: dict[str, str]) -> str:
        "register a user"
        ...

See the extra : str after the user_id attribute, and the -> Decimal before the final colon in the get_balance() snippet.

This is indicating that if you use the get_balance() method, that the user_id should be a type of string, and that the method will return a Decimal.

Note that the Python runtime does not enforce the type hints and that they are optional. However, where they are beneficial is in the IDE of your choice or other third party tools such type checkers.

In VSCode, when typing code, it will show the types that the method needs.

VSCode Type Hints

For type checking, you can install an extra module called mypy

pip install mypy

and then run it against your code,

mypy ./facade/client.py
Success: no issues found in 1 source file

Mypy will also check any imported modules at the same time.

If working with money, then it is advisable to add extra checks to your code. Checking that type usage is consistent throughout your code, especially when using Decimals, is a good idea that will make your code more robust.

For example, if I wasn't consistent in using the Decimal throughout my code, then I would see a warning highlighted.

mypy ./facade/client.py
facade/game_engine.py:45: error: Argument 1 to "append" of "list" has incompatible type "Tuple[str, int]"; expected "Tuple[str, Decimal]"
facade/game_api.py:34: error: Argument 2 to "submit_entry" of "GameEngine" has incompatible type "Decimal"; expected "int"
Found 2 errors in 2 files (checked 1 source file)

Summary

  • Use when you want to provide a simple interface to a complex subsystem.
  • You want to layer your subsystems into an abstraction that is easier to understand.
  • Abstract Factory and Facade can be considered very similar. An Abstract Factory is about creating in interface over several creational classes of similar objects, whereas the Facade is more like an API layer over many creational, structural and/or behavioral patterns.
  • The Mediator is similar to the Facade in the way that it abstracts existing classes. The Facade is not intended to modify, load balance or apply any extra logic. A subsystem does not need to consider that existence of the facade, it would still work without it.
  • A Facade is a minimal interface that could also be implemented as a Singleton.
  • A Facade is an optional layer that does not alter the subsystem. The subsystem does not need to know about the Facade, and could even be used by many other facades created for different audiences.