CHAPTER Facade Pattern
e Phantom of the Opera is there, inside my mind.
Christine
In this chapter, we will look at another way to hide the complexity of a system from the users of that system. All the moving parts should look like a single entity to the outside world.
All systems are more complex than they seem at first glance. Take, for instance,
a point of sale (POS) system. Most people only ever interact with such a system as a customer on the other side of the register. In a simple case, every transaction that takes place requires a number of things to happen. The sale needs to be recorded, the stock levels for each item in the transaction need to be adjusted, and you might even have to apply some sort of loyalty points for regular customers. Most of these systems also allow for specials that take into account the items in the transaction, like buy two and get the cheapest one for free.
There are a number of things involved in this transaction process, things like the loyalty system, the stock system, specials or promotions system, any payment systems, and whatever else is needed to make the process flow the way it should. For each of these interactions, we will have different classes providing the functionality in question.
123
' Wessel Badenhorst 2017
W. Badenhorst, Practical Python Design Patterns, https://doi.org/10.1007/978-1-4842-2680-3_8
CHAPTER FACADE PATTERN
It would not be hard to imagine an implementation of the process transaction that would work like this:
import datetime import random
class Invoice(object):
def __init__(self, customer):
self.timestamp = datetime.datetime.now() self.number = self.generate_number() self.lines = []
self.total = 0
self.tax = 0
self.customer = customer
def save(self):
- Save the invoice to some sort of persistent storage pass
def send_to_printer(self):
- Send invoice representation to external printer pass
def add_line(self, invoice_line):
self.lines.append(invoice_line) self.calculate()
def remove_line(self, line_item):
try:
self.lines.remove(line_item)
except ValueError as e:
print("could not remove {} because there is no such item in the invoice".format(line_item))
def calculate(self):
self.total = sum(x.total * x.amount for x in self.lines) self.tax = sum(x.total * x.tax_rate for x in self.lines)
124
CHAPTER FACADE PATTERN
def generate_number(self):
rand = random.randint(1, 1000)
return "{}{}".fomat(self.timestamp, rand)
class InvoiceLine(object):
def __init__(self, line_item):
- turn line item into an invoice line containing the current price etc pass
def save(self):
- save invoice line to persistent storage pass
class Receipt(object):
def __init__(self, invoice, payment_type):
self.invoice = invoice
self.customer = invoice.customer self.payment_type = payment_type pass
def save(self):
- Save receipt to persistent storage pass
class Item(object): def __init__(self):
pass
@classmethod
def fetch(cls, item_barcode):
- fetch item from persistent storage pass
def save(self):
- Save item to persistent storage pass
125
CHAPTER 9 FACADE PATTERN
class Customer(object): def __init__(self):
pass
@classmethod
def fetch(cls, customer_code):
- fetch customer from persistent storage pass
def save(self):
- save customer to persistent storage pass
class LoyaltyAccount(object): def __init__(self):
pass
@classmethod
def fetch(cls, customer):
- fetch loyaltyAccount from persistent storage pass
def calculate(self, invoice):
- Calculate the loyalty points received for this purchase pass
def save(self):
- save loyalty account back to persistent storage pass
If you want to process a sale, you have to interact with all of these classes.
def complex_sales_processor(customer_code, item_dict_list, payment_type): customer = Customer.fetch_customer(customer_code)
invoice = Invoice()
for item_dict in item_dict_list:
item = Item.fetch(item_dict["barcode"]) item.amount_in_stock - item_dict["amount_purchased"] item.save()
126
CHAPTER FACADE PATTERN
invoice_line = InvoiceLine(item) invoice.add_line(invoice_line)
invoice.calculate() invoice.save()
loyalt_account = LoyaltyAccount.fetch(customer) loyalty_account.calculate(invoice)
loyalty_account.save()
receipt = Receipt(invoice, payment_type) receipt.save()
As you can see, whatever system is tasked with processing a sale needs to interact with a very large number of classes specific to the point of sale sub-system.
Systems evolve that is a fact of life and as they grow they can become very complex. To keep the whole system simple, we want to hide all the functionality from the client.
I know you smelled it the moment you saw the preceding snippet. Everything is tightly coupled, and we have a large number of classes to interact with. When we want to extend our POS with some new technology or functionality, we will need to dig into the internals of the system. We might just forget to update some small hidden piece of code, and our whole system will thus come crashing down. We want to simplify this system without losing any of the system s power.
Ideally, we want our transaction processor to look something like this:
def nice_sales_processor(customer_code, item_dict_list, payment_type): invoice = SomeInvoiceClass(customer_code)
for item_dict in item_dict_list:
invoice.add_item(item_dict["barcode"], item_dict_list["amount_
purchased"])
invoice.finalize()
invoice.generate_receipt(payment_type)
127
CHAPTER FACADE PATTERN
This makes the code much cleaner and easier to read. A simple rule of thumb in object-oriented programming is that whenever you encounter an ugly or messy system, hide it in an object.
You can imagine that this problem of complex or ugly systems is one that regularly comes up in the life of a programmer, so you can be certain that there are several opinions on how to best solve this issue. One such the solution is to use a design pattern called the facade pattern. This pattern is specifically used to create a complexity-limiting interface to a sub-system or collection of sub-systems. Look at the following piece of code to get a better feel for how this abstraction via the facade pattern should happen.
simple_ facade.py
class Invoice:
def __init__(self, customer): pass
class Customer:
- Altered customer class to try to fetch a customer on init or creates a new one
def __init__(self, customer_id): pass
class Item:
- Altered item class to try to fetch an item on init or creates a new one
def __init__(self, item_barcode): pass
class Facade:
@staticmethod
def make_invoice(customer): return Invoice(customer)
@staticmethod
def make_customer(customer_id): return Customer(customer_id)
@staticmethod
def make_item(item_barcode): return Item(item_barcode)
- ...
Before you read on, try to implement a facade pattern for the POS system on your own.
128
CHAPTER FACADE PATTERN
What Sets the Facade Pattern Apart
One key distinction with regards to the facade pattern is that the facade is used not just
to encapsulate a single object but also to provide a wrapper that presents a simplified interface to a group of complex sub-systems that is easy to use without unneeded functions or complexity. Whenever possible, you want to limit the amount of knowledge and or complexity you expose to the world outside the system. The more you allow access to and interaction with a system, the more tightly coupled that system becomes. The ultimate result of allowing greater depth of access to your system is a system that can be used like a black box, with a clear set of expected inputs and well-defined outputs. The facade should decide which internal classes and representations to use. It is also within the facade that you will make changes in terms of functions to be assigned to external or updated internal systems when the time comes to do so.
Facades are often used in software libraries since the convenient methods provided by the facade make the library easier to understand and use. Since interaction with
the library happens via the facade, using facades also reduces the dependencies from outside the library on its inner workings. As for any other system, this allows more flexibility when developing the library.
Not all APIs are created equal, and you will have to deal with poorly designed collections of APIs. However, using the facade pattern, you can wrap these APIs in a single well-designed API that is a joy to work with.
One of the most useful habits you can develop is that of creating solutions to your own problems. If you have a system you need to work with that is less than ideal, build a facade for it to make it work in your context. If your IDE misses a tool you love, code up your own extension to provide this function. Stop waiting for other people to come along and provide a solution. You will feel more empowered every day that you take an action to shape your world to fit your needs.
Let s implement our POS backend as just such a facade. Since we do not want to build the whole system, several the functions defined will be stubs, which you could expand if you chose to.
class Sale(object): def __init__(self):
pass
129
CHAPTER FACADE PATTERN
@staticmethod
def make_invoice(customer_id):
return Invoice(Customer.fetch(customer_id))
@staticmethod
def make_customer(): return Customer()
@staticmethod
def make_item(item_barcode):
return Item(item_barcode)
@staticmethod
def make_invoice_line(item):
return InvoiceLine(item)
@staticmethod
def make_receipt(invoice, payment_type): return Receipt(invoice, payment_type)
@staticmethod
def make_loyalty_account(customer):
return LoyaltyAccount(customer)
@staticmethod
def fetch_invoice(invoice_id):
return Invoice(customer)
@staticmethod
def fetch_customer(customer_id): return Customer(customer_id)
@staticmethod
def fetch_item(item_barcode): return Item(item_barcode)
@staticmethod
def fetch_invoice_line(line_item_id):
return InvoiceLine(item)
130
CHAPTER FACADE PATTERN
@staticmethod
def fetch_receipts(invoice_id):
return Receipt(invoice, payment_type)
@staticmethod
def fetch_loyalty_account(customer_id):
return LoyaltyAccount(customer)
@staticmethod
def add_item(invoice, item_barcode, amount_purchased):
item = Item.fetch(item_barcode) item.amount_in_stock - amount_purchased item.save()
invoice_line = InvoiceLine.make(item) invoice.add_line(invoice_line)
@staticmethod
def finalize(invoice):
invoice.calculate() invoice.save()
loyalt_account = LoyaltyAccount.fetch(invoice.customer) loyalty_account.calculate(invoice) loyalty_account.save()
@staticmethod
def generate_receipt(invoice, payment_type):
receipt = Receipt(invoice, payment_type) receipt.save()
Using this new Sales class, which serves as a facade for the rest of the system, the ideal function we looked at earlier now looks like this:
def nice_sales_processor(customer_id, item_dict_list, payment_type): invoice = Sale.make_invoice(customer_id)
for item_dict in item_dict_list:
Sale.add_item(invoice, item_dict["barcode"], item_dict_list["amount_
purchased"])
131
CHAPTER 9 FACADE PATTERN
Sale.finalize(invoice)
Sale.generate_receipt(invoice, payment_type)
As you can see, not a lot has changed.
The code for Sales results in a single entry point to the more complex business system. We now have a limited set of functions to call via the facade that allows us to interact with the parts of the system we need access to, without overburdening us with all the internals of the system. We also have a single class to interact with, so we no longer have to go digging through the whole class diagram to find the correct place to hook into the system.
In the sub-system, a third-party stock-management system or loyalty program could replace internal systems without having the clients using the facade class change a single line of code. Our code is thus more loosely coupled and easier to extend. The POS clients do not care if we decide to hand off the stock and accounting to a third-party provider or build and run an internal system. The simplified interface remains blissfully unaware of the complexity under the hood without reducing the power inherent in the sub-system.
There is not much to the facade pattern. You start out by feeling the frustration of a sub- system or group of sub-systems that does not work the way you want it to, or you find yourself having to look up interactions in many classes. By now, you know something is not right, so you decide to clean up this mess, or at least hide it behind a single, elegant interface. You design your wrapper class to turn the ugly sub-system into a black box that deals with the interactions and complexities for you. The rest of your code, and other potential client code, only deals with this facade. The world is now just a tiny bit better for your efforts well done.
Facades are often implemented as singletons when they do not have
to keep track of state. Alter the facade implementation in the last section of code so it uses the singleton pattern from Chapter 2.
Expand the point of sale system outlined in this chapter until you
have a system that you could use in the real world.
Pick your favorite social media platform and create your own facade
that allows you to interact with their API.
'Practical Python Design Patterns' 카테고리의 다른 글
Chain of Responsibility Pattern (0) | 2023.03.28 |
---|---|
Proxy Pattern (0) | 2023.03.28 |
Decorator Pattern (0) | 2023.03.28 |
Adapter Pattern (0) | 2023.03.28 |
Builder Pattern (0) | 2023.03.28 |
댓글