Introduction
The facade pattern is used as a way to hide more complex logic. A facade can do the following:
- Improve usability of a library or API by masking interaction with the more complex components of the underlying code.
- Provide interfaces which are context-specific. That means that each client of an API could get its own facade, depening on use-cases.
A facade pattern is typically used in the following cases:
- A subsystems, or more subsystems have tightly coupled elements.
- In layered software, you might need an entry point for each layer.
- In the case of very complex systems, a facade can help
This might all sound quite complex, here is what it looks like:
The Facade class has a simple interface hiding the fact that it might have more complex interaction with for example classA and classB. This example is simple, but you can imagine that in complex systems, there are tens or hundreds of classes with which the Façade has to interact.
Implementation in Python
In this, rather contrived, example, we will build an imaginry ingester library, which ingest data in three different ways: through RSS, Mail or manually.
All of the ingesters get the same interface. Because of the flexibility of this pattern this is not a necessity, but I do it here to keep things simple:
class Ingester:
def ingest(self, data: str):
pass
Note that we hint that the data ingested must be of string type.
Now we define our first ingester:
class RSSIngester(Ingester):
def ingest(self, data: str):
print(f"RSS Ingester: Ingesting RSS data: {data}")
We can be very short about this class:
- It derives from the Ingester class, which means it needs to define the ingest() method
- The ingest() method does nothing more than just print out the data.
In the same way we can define the other ingestors:
class MailIngester(Ingester):
def ingest(self, data: str):
print(f"RSS Ingester: Ingesting Mail data: {data}")
class ManualIngester(Ingester):
def ingest(self, data: str):
print(f"RSS Ingester: Ingesting Manual data: {data}")
As you can see, they basically follow the same pattern.
Now we come to the interesting bit: the Facade class itself:
class Facade:
_rss_ingester: Ingester = None
_mail_ingester: Ingester = None
_manual_ingester: Ingester = None
def __init__(self):
self._rss_ingester = RSSIngester()
self._mail_ingester = MailIngester()
self._manual_ingester = ManualIngester()
def operate_ingesters(self, data: str):
self._rss_ingester.ingest(data)
self._mail_ingester.ingest(data)
self._manual_ingester.ingest(data)
Here some more explanation is needed:
- In the constructor, we initialize three fields with the three types of ingester
- These fields are defined at the top of the class, and they are of type Ingester, the common baseclass. That means the Facade can only access certain functionalities of these objects. This can also help to hide complexities.
- Note that the client classes of the facade cannot change these classes, at least in this example.
- The operate_ingesters() method calls the ingest() method on all three ingesters, thereby hiding any possible complexity that might be needed to operate those.
Now we can test the facade:
if __name__ == "__main__":
facade = Facade()
facade.operate_ingesters("data for ingest")
Line by line:
- A Facade object is created
- Next we call operate_ingesters() to operate on the ingesters.
Conclusion
This is not the most difficult pattern to implement. As with most Design Patterns, it is just common sense. In a later article I hope to provide you with a more complex example for this pattern.