본문 바로가기
Practical Python Design Patterns

Facade Pattern

by 자동매매 2023. 3. 28.

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.

Point of Sale Example

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 Evolution

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.

Parting Shots

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.

Exercises

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.

132

'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

댓글