One of the GoF design patterns is the facade. It lets us create a simple interface that hides the underlying complexity. For example, we can have a facade which lets the client book meetings on various calendars like Google, Outlook, Calendly, etc. The client specifies details about the meeting such as the title, description, etc. along with which calendar to use. The facade then executes appropriate logic to book the meeting, without the client having to deal with the low-level details.
This post talks about how we can create a facade in Python. We’ll first take a look at singledispatch to see how we can call different functions depending on the type of the argument. We’ll then build upon this to create a function which dispatches based on the value instead of the type. We’ll use the example given above to create a function which dispatches to the right function based on what calendar the client would like to use.
Single Dispatch
The official documentation defines single dispatch to be a function where the implementation is chosen based on the type of a single argument. This means we can have one function which handles integers, another which handles strings, and so on. Such functions are created using the singledispatch
decorator from the functools
package. Here’s a trivial example which prints the type of the argument handled by the function.
1 | import functools |
We start by decorating the echo
function with singledispatch
. This is the function we will pass our arguments to. We then create echo_int
, and echo_str
which are different implementation that will handle the various types of arguments. These are registered using the echo.register
decorator.
When we run the example, we get the following output. As expected, the function to execute is chosen based on the type of the argument. Calling the function with a type which is not handled results in a noop as we’ve set the body of echo
to ellipses.
1 | 5 is an int |
When looking at the source code of singledispatch
, we find that it maintains a dictionary which maps the type of the argument to its corresponding function. In the following sections, we’ll look at how we can dispatch based on the value of the argument.
Example
Let’s say we’re writing a library that lets the users book meetings on a calendar of their choosing. We expose a book_meeting
function. The argument to this function is an instance of the Meeting
data class which contains information about the meeting, and the calendar on which it should be booked.
Code
Model
We’ll start by adding an enum which represents the calendars that we support.
1 | import enum |
Next we’ll add the data class which represents the meeting as a dataclass
.
1 | import dataclasses as dc |
Finally, we’ll start creating the facade by adding functions which will dispatch based on the value of calendar
contained within the instance of Meeting
.
Dispatch
We’ll create a registry which maps the enum to its corresponding function. The function takes as input a Meeting
object and returns a boolean indicating whether the meeting was successfully booked or not.
1 | from typing import Callable, TypeVar |
Next we’ll add the book_meeting
function. This is where we dispatch to the appropriate function depending on the meeting object that is received as the argument.
1 | def book_meeting(meeting: Meeting) -> bool: |
To be able to register functions which contains the logic for a particular calendar, we’ll create a decorator called register
.
1 | def register(calendar: Calendar): |
register
accepts as argument the calendar for which we’re registering a function. It returns another higher-order function which puts the actual function in the registry. Since the actual logic of the execution is in the decorated function, we simply return the original function func
.
Finally, we register functions for different calendars.
1 |
|
We’ll put all of this code in action by trying to book a meeting on Google calendar.
1 | if __name__ == "__main__": |
This prints “Booked Google meeting”, like we’d expect. We can now continue to add more functions which contain logic for specific calendars. Our library can evolve without any change to the exposed interface. It’s also possible to organise functions into their own modules, import the register
decorator, and decorate them to add them to the registry. This has two main benefits. One, we keep the code well structured. Two, the code for different versions of the same calendar can stay separated; we avoid having to write if
checks to see the calendar version since that can be made part of the enum itself, like GOOGLE_V1
.
That’s it. That’s how you can create a facade in Python.