In this chapter, we will continue looking at the concept of design patterns in Python 3 and its various categories and their implementation while developing software using Python.
In the previous chapter, we learned how to apply behavioral design patterns with examples. In this chapter, we will continue looking at the remaining two categories – structural and creational design patterns. We will see how they can be applied in Python using our core example of ABC Megamart.
In this chapter, we will be looking at the following main topics:
By the end of this chapter, you should be able to understand some of the examples of important structural and creational design patterns and learn how they can be implemented in various applications.
The code examples shared in this chapter are available on GitHub under the code for this chapter here: https://github.com/PacktPublishing/Metaprogramming-with-Python/tree/main/Chapter13.
As the name suggests, structural design patterns are used to design the structure of classes and their implementation in such a way that the classes and objects can be extended or reused effectively. In this section, we will be covering three such structural design patterns — bridge, façade, and proxy patterns. We are considering these three design patterns because they are unique and they represent three different aspects of how structural design patterns can be used.
The bridge design pattern is applied to bridge multiple elements or operations of implementation using the concept of abstraction or the abstract method. To explain this further and to implement this design pattern, our code should have the following elements:
Let’s look at the bridge pattern with an example. In this example, we will look at printing the business card for branch managers that belong to two different supermarkets – ABC Megamart and XYZ Megamart. Let’s see how:
from abc import abstractmethod, ABC
class PrintCard(ABC):
def add_name(self, name):
self.name = name
def add_manager(self, branch):
self.branch = branch.FORMATTING
@abstractmethod
def printcard(self):
pass
class CardABC(PrintCard):
def __init__(self, logo, name, branch):
self.logo = logo
super().add_name(name)
super().add_manager(branch)
def printcard(self, *args):
print(self.logo + self.name)
for arg in args:
print(self.branch + str(arg))
class CardXYZ(PrintCard):
def __init__(self, style, logo, name, branch):
self.style = style
self.logo = logo
super().add_name(name)
super().add_manager(branch)
def printcard(self, *args):
print(self.logo + self.style + self.name)
for arg in args:
print(self.branch + str(arg))
class Manager:
def formatting(self):
pass
class Manager_manhattan(Manager):
def __init__(self):
self.formatting()
def formatting(self):
self.FORMATTING = '33[7m'
class Manager_albany(Manager):
def __init__(self):
self.formatting()
def formatting(self):
self.FORMATTING = ' 33[94m'
manager_manhattan = CardABC(logo = '33[43m', name = 'ABC Megamart', branch = Manager_manhattan())
manager_manhattan.printcard('John M',
'40097 5th Main Street',
'Manhattan',
'New York City',
'New York',
11007)
The output is represented as follows, with the formatting as provided in the class instantiation:
ABC Megamart
John M
40097 5th Main Street
Manhattan
New York City
New York
11007
manager_albany = CardXYZ(style = '33[43m',logo = '33[5m', name = 'XYZ Megamart', branch = Manager_albany())
manager_albany.printcard('Ron D','[email protected]','123 Main Street','Albany','New York', 12084)
The output is represented as follows, with the style and formatting as provided in the class instantiation:
XYZ Megamart
Ron D
123 Main Street
Albany
New York
12084
Let’s connect the elements of this design pattern with their corresponding objects in the example with the following graphical representation:
Figure 13.1 – Bridge pattern classes
So, the bridge pattern has been implemented by creating a bridge between the abstraction and the implementation classes. With this understanding, let’s look at the facade pattern.
In this section, we will look at the facade pattern, where we will design a black box kind of implementation to hide the complexity of a system that handles multiple subsystems from the end user or client. To explain this further and to implement this design/core pattern, our code needs the following elements:
To understand the facade pattern further, let’s create a set of functionalities that starts from adding items to the shopping cart, moving to the counter, scanning bar codes, billing, and finally, printing the invoice:
class Cart:
def __init__(self, items):
self.items = items
def return_cart(self):
cart_items = []
for i in self.items:
cart_items.append(i)
print("Running return_cart...")
return cart_items
class Counter:
def __init__(self, name):
self.name = name
def goto_counter(self):
countername = self.name
print("Running goto_counter...")
return countername
class BarCode:
def __init__(self, scan):
self.scan = scan
def scan_bar_code(self):
codes = []
for i in self.scan:
codes.append(i)
print("Running scan_bar_code...")
return codes
class Billing:
def __init__(self, codes, units ):
self.codes = codes
self.units = units
def add_billing(self):
codes = self.codes.scan_bar_code()
pricetag = []
for i in self.units:
pricetag.append(i)
bill = dict(zip(codes, pricetag))
print("Running add_billing...")
return bill
class Tax:
def __init__(self, tax):
self.tax = tax
def add_tax(self):
taxed = []
for i in self.tax:
taxed.append(i)
print("Running add_tax...")
return taxed
class FinalBill:
def __init__(self, billing, cart, tax):
self.billing = billing
self.cart = cart
self.tax = tax
def calc_bill(self):
bill = self.billing.add_billing()
items = []
cart_items = self.cart.return_cart()
calc_bill = []
taxes = self.tax.add_tax()
for item,tax in zip(bill.items(),taxes):
items.append(item[1])
calc_bill.append(item[1] + item[1]*tax)
finalbill = dict(zip(cart_items, calc_bill))
print("Running calc_bill...")
return finalbill
class Invoice:
def __init__(self, finalbill, counter):
self.finalbill = finalbill
self.counter = counter
def print_invoice(self):
finalbill = self.finalbill.calc_bill()
final_total = sum(finalbill.values())
print("Running print_invoice...")
print('**************ABC
Megamart*****************')
print('***********------------------
**************')
print('Counter Name: ',
self.counter.goto_counter())
for item,price in finalbill.items():
print(item,": ", price)
print('Total:',final_total)
print('***********------------------
**************')
print('***************PAID********************
****')
class Queue:
def __init__(self, items, name, scan, units, tax):
self.cart = Cart(items)
self.counter = Counter(name)
self.barcode = BarCode(scan)
self.billing = Billing(self.barcode, units)
self.tax = Tax(tax)
self.finalbill = FinalBill(self.billing,
self.cart, self.tax)
self.invoice = Invoice(self.finalbill,
self.counter)
def pipeline(self):
self.cart.return_cart()
self.counter.goto_counter()
self.barcode.scan_bar_code()
self.tax.add_tax()
def pipeline_implicit(self):
self.invoice.print_invoice()
def run_facade():
queue = Queue(items = ['paperclips','blue
pens','stapler','pencils'],
name = ['Regular Counter'],
scan = [113323,3434332,2131243,2332783],
units = [10,15,12,14],
tax = [0.04,0.03,0.035,0.025],
)
queue.pipeline()
run_facade()
The output for the preceding test is as follows:
Running return_cart...
Running goto_counter...
Running scan_bar_code...
Running add_tax...
def run_facade_implicit():
queue = Queue(items = ['paperclips','blue
pens','stapler','pencils'],
name = ['Regular Counter'],
scan = [113323,3434332,2131243,2332783],
units = [10,15,12,14],
tax = [0.04,0.03,0.035,0.025],
)
queue.pipeline_implicit()
run_facade_implicit()
The output for the preceding test is as follows:
Running scan_bar_code...
Running add_billing...
Running return_cart...
Running add_tax...
Running calc_bill...
Running print_invoice...
**************ABC Megamart*****************
***********------------------**************
Running goto_counter...
Counter Name: ['Regular Counter']
paperclips : 10.4
blue pens : 15.45
stapler : 12.42
pencils : 14.35
Total: 52.620000000000005
***********------------------**************
***************PAID************************
Let’s connect the elements of this design pattern with their corresponding objects in the example in the following graphical representation:
Figure 13.2 – Facade pattern classes
So, the facade pattern has been implemented by creating a black box that provides the end users with an interface to access the functions of a complex system without worrying about the implementation details. Now, let’s look at the proxy pattern.
In this section, we will look at the proxy design pattern. As the name implies, the proxy pattern is applied to create a proxy around the actual functionality so that the actual functionality is executed only when the proxy allows it based on certain preconditions. To explain this further and to implement this design pattern, our code needs the following elements:
In this example, let’s consider the NYC branch of ABC Megamart and create a class named NYC:
class NYC:
def __init__(self):
self.manager = {}
self.branch = {}
self.product = {}
self.sales = {}
def set_parameters(self, manager, branch, product,
sales):
self.manager = manager
self.branch = branch
self.product = product
self.sales = sales
def get_parameters(self):
return self.manager, self.branch,
self.product, self.sales
def calc_tax_nyc(self):
branch = self.branch
manager = self.manager
product = self.product
sales = self.sales
pricebeforetax = sales['purchase_price'] +
sales['purchase_price'] *
sales['profit_margin']
finalselling_price = pricebeforetax +
(pricebeforetax * (sales['tax_rate'] +
sales['local_rate']))
sales['selling_price'] = finalselling_price
return branch, manager, product, sales
class ReturnBook(NYC):
def __init__(self, nyc):
self.nyc = nyc
def add_book_details(self, state, manager, branch,
product, sales):
if state in ['NY', 'NYC', 'New York']:
self.nyc.set_parameters(manager, branch,
product, sales)
else:
print("There is no branch in the state:",
state)
def show_book_details(self, state):
if state in ['NY', 'NYC', 'New York']:
return self.nyc.get_parameters()
else:
print(state, "has no data")
def calc_tax(self, state):
if state in ['NY', 'NYC', 'New York']:
return self.nyc.calc_tax_nyc()
else:
print("The state", state, "is not
supported")
branch_manhattan = ReturnBook(NYC())
branch_manhattan.add_book_details(state = 'NY', manager = {'regional_manager': 'John M',
'branch_manager': 'Tom H',
'sub_branch_id': '2021-01'},
branch = {'branchID': 2021,
'branch_street': '40097 5th Main Street',
'branch_borough': 'Manhattan',
'branch_city': 'New York City',
'branch_state': 'New York',
'branch_zip': 11007},
product = {'productId': 100002,
'product_name': 'WashingMachine',
'product_brand': 'Whirlpool'},
sales = {'purchase_price': 450,
'profit_margin': 0.19,
'tax_rate': 0.4,
'local_rate': 0.055})
branch_manhattan.show_book_details('NY')
The output of the preceding code is as follows:
({'regional_manager': 'John M',
'branch_manager': 'Tom H',
'sub_branch_id': '2021-01'},
{'branchID': 2021,
'branch_street': '40097 5th Main Street',
'branch_borough': 'Manhattan',
'branch_city': 'New York City',
'branch_state': 'New York',
'branch_zip': 11007},
{'productId': 100002,
'product_name': 'WashingMachine',
'product_brand': 'Whirlpool'},
{'purchase_price': 450,
'profit_margin': 0.19,
'tax_rate': 0.4,
'local_rate': 0.055})
branch_manhattan.calc_tax('NY')
branch_manhattan.add_book_details(state = 'LA', manager = {'regional_manager': 'John M',
'branch_manager': 'Tom H',
'sub_branch_id': '2021-01'},
branch = {'branchID': 2021,
'branch_street': '40097 5th Main Street',
'branch_borough': 'Manhattan',
'branch_city': 'New York City',
'branch_state': 'New York',
'branch_zip': 11007},
product = {'productId': 100002,
'product_name': 'WashingMachine',
'product_brand': 'Whirlpool'},
sales = {'purchase_price': 450,
'profit_margin': 0.19,
'tax_rate': 0.4,
'local_rate': 0.055})
The output of the preceding code is as follows:
There is no branch in the state: LA
The output of the preceding code is as follows:
LA has no data
branch_manhattan.calc_tax('LA')
The output is as follows:
The state LA is not supported
Let’s connect the elements of this design pattern with their corresponding objects in the example in the following graphical representation:
Figure 13.3 – Proxy design pattern classes
So, the proxy pattern has been implemented by creating a proxy class that adds the required conditions to execute the actual functionalities. Next, we’re moving on to exploring the creational design patterns.
Creational design patterns are various methods to add abstraction in the process of object creation. In this section, we will be looking at three such design patterns, namely the factory method, prototype pattern, and singleton pattern.
The factory design pattern is a method of abstraction where a factory class is created to create an object for the class from the factory class instead of directly instantiating the object. To explain this further and to implement this design pattern, our code needs the following elements:
For this example, let’s implement using another scenario from ABC Megamart:
from abc import abstractmethod
class Branch:
@abstractmethod
def buy_product(self):
pass
@abstractmethod
def maintenance_cost(self):
pass
class Brooklyn(Branch):
def __init__(self,product,unit_price,quantity,
product_type):
self.product = product
self.unit_price = unit_price
self.quantity = quantity
self.product_type = product_type
def buy_product(self):
if (self.product_type == 'FMCG'):
self.statetax_rate = 0.035
self.promotiontype = 'Discount'
self.discount = 0.10
self.initialprice =
self.unit_price*self.quantity
self.salesprice = self.initialprice +
self.initialprice*self.statetax_rate
self.finalprice = self.salesprice *
(1-self.discount)
return self.salesprice,
self.product,self.promotiontype
else:
return "We don't stock this product"
def maintenance_cost(self):
self.coldstorageCost = 100
if (self.product_type == 'FMCG'):
self.maintenance_cost = self.quantity *
0.25 + self.coldstorageCost
return self.maintenance_cost
else:
return "We don't stock this product"
class Manhattan(Branch):
def __init__(self,product,unit_price,quantity,
product_type):
self.product = product
self.unit_price = unit_price
self.quantity = quantity
self.product_type = product_type
def buy_product(self):
if (self.product_type == 'Electronics'):
self.statetax_rate = 0.05
self.promotiontype = 'Buy 1 Get 1'
self.discount = 0.50
self.initialprice =
self.unit_price*self.quantity
self.salesprice = self.initialprice +
self.initialprice*self.statetax_rate
self.finalprice = self.salesprice *
(1-self.discount)
return self.finalprice,
self.product,self.promotiontype
else:
return "We don't stock this product"
def maintenance_cost(self):
if (self.product_type == 'Electronics'):
self.maintenance_cost = self.quantity * 0.05
return self.maintenance_cost
else:
return "We don't stock this product"
Class BranchFactory:
def create_branch(self,branch,product,unit_price,
quantity,product_type):
if str.upper(branch) == 'BROOKLYN':
return Brooklyn(product,unit_price,
quantity,product_type)
elif str.upper(branch) == 'MANHATTAN':
return Manhattan(product,unit_price,
quantity,product_type)
def test_factory(branch,product,unit_price,quantity,product_type):
branchfactory = BranchFactory()
branchobject = branchfactory.create_branch(branch,
product,unit_price,quantity,product_type)
print(branchobject)
print(branchobject.buy_product())
print(branchobject.maintenance_cost())
test_factory('Brooklyn','Milk', 10,5,'FMCG')
The output for the preceding code is as follows:
<__main__.Brooklyn object at 0x000002101D4569A0>
(51.75, 'Milk', 'Discount')
101.25
test_factory('manhattan','iPhone', 1000,1,'Electronics')
The output for the preceding code is as follows:
<__main__.Manhattan object at 0x000002101D456310>
(525.0, 'iPhone', 'Buy 1 Get 1')
0.05
Let’s connect the elements of this design pattern with their corresponding objects in the example with the following graphical representation:
Figure 13.4 – Factory pattern classes
So, the factory pattern has been implemented by creating a factory class that instantiates the Abstraction subclasses. With this implementation, we have learned about the creational design pattern with an example.
The prototype design pattern is also used to implement abstraction during the creation of a Python object. A prototype can be used by the end user to create a copy of an object of a class without the overhead of understanding the detailed implementation behind it. To explain this further and to implement this design pattern, our code needs the following elements:
For this example, let’s implement using another scenario from ABC Megamart:
class Prototype:
def __init__(self):
self.cp = __import__('copy')
def clone(self, objname):
return self.cp.deepcopy(objname)
class FMCG:
def __init__(self,supplier_name,supplier_code,
supplier_address,supplier_contract_start_date,
supplier_contract_end_date,supplier_quality_code):
self.supplier_name = supplier_name
self.supplier_code = supplier_code
self.supplier_address = supplier_address
self.supplier_contract_start_date =
supplier_contract_start_date
self.supplier_contract_end_date =
supplier_contract_end_date
self.supplier_quality_code =
supplier_quality_code
def get_supplier_details(self):
supplierDetails = {
'Supplier_name': self.supplier_name,
'Supplier_code': self.supplier_code,
'Supplier_address': self.supplier_address,
'ContractStartDate':
self.supplier_contract_start_date,
'ContractEndDate':
self.supplier_contract_end_date,
'QualityCode': self.supplier_quality_code
}
return supplierDetails
fmcg_supplier = FMCG('Test Supplier','a0015','5093 9th Main Street, Pasadena,California, 91001', '05/04/2020', '05/04/2025',1)
proto = Prototype()
fmcg_supplier_reuse = proto.clone(fmcg_supplier)
id(fmcg_supplier)
The output is as follows:
2268233820528
id(fmcg_supplier_reuse)
The output is as follows:
2268233819616
fmcg_supplier_reuse.supplier_name = 'ABC Supplier'
fmcg_supplier_reuse.get_supplier_details()
The output is as follows:
{'Supplier_name': 'ABC Supplier',
'Supplier_code': 'a0015',
'Supplier_address': '5093 9th Main Street, Pasadena,California, 91001',
'ContractStartDate': '05/04/2020',
'ContractEndDate': '05/04/2025',
'QualityCode': 1}
fmcg_supplier.get_supplier_details()
The output is as follows:
{'Supplier_name': 'Test Supplier',
'Supplier_code': 'a0015',
'Supplier_address': '5093 9th Main Street, Pasadena,California, 91001',
'ContractStartDate': '05/04/2020',
'ContractEndDate': '05/04/2025',
'QualityCode': 1}
So, the prototype pattern has been implemented by creating a Prototype class that copies the object of the implementation class. Now that you’ve understood this, let’s look at the singleton design pattern.
As the name suggests, the singleton pattern is a design pattern where we can limit the number of class instances created for a class while initializing the class itself. To explain this further and implement this design pattern, we need to develop the elements of the singleton class in our code.
Unlike the other patterns, this pattern has only one element – the singleton class. A singleton class will have a constraint set within its init method to limit the number of instances to one.
For this example, let’s implement using another scenario from ABC Megamart:
class SingletonBilling:
billing_instance = None
product_name = 'Dark Chocolate'
unit_price = 6
quantity = 4
tax = 0.054
def __init__(self):
if SingletonBilling.billing_instance == None:
SingletonBilling.billing_instance = self
else:
print("Billing can have only one
instance")
def generate_bill(self):
total = self.unit_price * self.quantity
final_total = total + total*self.tax
print('***********------------------
**************')
print('Product:', self.product_name)
print('Total:',final_total)
print('***********------------------
**************')
invoice1 = SingletonBilling()
invoice1.generate_bill()
The output is displayed as follows:
***********------------------**************
Product: Dark Chocolate
Total: 25.296
***********------------------**************
invoice2 = SingletonBilling()
The second instance could not be created for the class due to its singleton property. The output is as expected:
Billing can have only one instance
So, the singleton pattern has been implemented by restricting the singleton class from creating more than one instance. With this example, we have covered three types of creational design patterns and their implementation.
In this chapter, we have learned about the concept of structural and creational design patterns by applying some of these design patterns in Python 3. We implemented the bridge design pattern and understood each of its elements. We understood the facade design pattern and its various elements. We also implemented the proxy design pattern with an example. We also covered creational design patterns such as the factory method, prototype, and singleton patterns with their corresponding examples.
Similar to other chapters covered in this book, this chapter, which explains the second part of design patterns, also focused on metaprogramming and its impact on Python code.
In the next chapter, we will continue the code generation with some examples.