Design Patterns
╰( ͡° ͜ʖ ͡° )つ──☆*:・゚
Semeru Research Group
by David N. Palacio
- Make the code more readable, flexible, and manageable with independent strategies that describe how to solve a problem
Why using Design Patterns?
Agenda
- A Motivating Example
- Classification of Design Patterns
- Creational: Singleton
- Structural: Facade
- Behavioral: Strategy
- Anti-Patterns
A Motivating Example
def multiply(a, b):
return a * b
import functools
def multiply(a, b):
return a * b
def summation(array):
return functools.reduce(lambda a, b: a+b, array)
import functools
def multiply(a, b):
return a * b
def divide(a, b):
return a / b
def summation(array):
return functools.reduce(lambda a, b: a+b, array)
import functools
def multiply(a, b):
return a * b
def divide(a, b):
return a / b
def summation(array):
return functools.reduce(lambda a, b: a+b, array)
def max(array):
return functools.reduce(lambda a, b: a if a > b else b, array)
import functools
def multiply(a, b):
return a * b
def divide(a, b):
return a / b
def power(a, b):
return a ** b
def summation(array):
return functools.reduce(lambda a, b: a+b, array)
def max(array):
return functools.reduce(lambda a, b: a if a > b else b, array)
def min(array):
return functools.reduce(lambda a, b: a if a < b else b, array)
What's wrong with this approach? (5min)
Some Observations
- Repetitive Code
- Variable Redundancy
- Not maintainable
- Follows a PATTERN
Why using Design Patters?
Reusability
import functools
def multiply(a, b):
return a * b
def divide(a, b):
return a / b
def power(a, b):
return a ** b
def summation(array):
return functools.reduce(lambda a, b: a+b, array)
def max(array):
return functools.reduce(lambda a, b: a if a > b else b, array)
def min(array):
return functools.reduce(lambda a, b: a if a < b else b, array)
How can we enhance it? (7min)
import functools
OperationEnum = {
'+': lambda x,y: x+y,
'-': lambda x,y: x-y,
'*': lambda x,y: x*y,
'/': lambda x,y: x/y,
'**': lambda x,y: x**y
}
def mathOperation(a, b, operation='+'):
return OperationEnum[operation](a,b)
def summation(array):
return functools.reduce(lambda a, b: a+b, array)
def max(array):
return functools.reduce(lambda a, b: a if a > b else b, array)
def min(array):
return functools.reduce(lambda a, b: a if a < b else b, array)
A Design Pattern is a description or template that can be repeatedly applied to a commonly recurring problem in Software Design
Classification of Design Patterns
Types
- Creational Patterns
- Structural Patterns
- Behavioral Patterns
- Architectural Pattern (e.g., SOA, Publish/Subscribe, Reactive Programming)
- Concurrency Pattern (i.e., multi-threaded programming)
How program's elements are created?
The Creational Design Patterns deal with the class or object instantiation
Abstract Factory
Builder
Factory Method
Prototype
Singleton
How program's elements relate to each other?
The Structural Design Patterns are about organizing different classes members and objects to form larger structures and provide new functionality
Adapter
Bridge
Composite
Decorator
Facade
Proxy
How program's elements communicate to each other?
The Behavioral Design Patterns are about identifying common communication patterns between objects and realizing these patterns
Chain of Resposability
Command
Iterator
Mediator
Stategy
Memento
State
Template
Visitor
Creational Pattern: Singleton
Singleton lets you ensure that a class has only one instance while providing a global access point to the instance.
Logging System
The Singleton can be accessible globally, but it is not a global variable. It is a class that can be instanced at any time, but after it is first instanced, any new instances will point to the same instance as the first.
Logging System
Loggin Obj A
Loggin Obj B
Loggin Obj C
Client
Singleton Elements
-
For a class to behave as a Singleton, it should not contain any references to
self
but use static variables, static methods and/or class methods
"""
Singleton Logger
"""
import copy
class LoggerSingleton():
"The Singleton Class"
shared_value = [DEBUG, INFO, WARNING, ERROR]
def __new__(cls):
return cls
@staticmethod
def debug():
"Use @staticmethod if no inner variables required"
@staticmethod
def info():
"Use @staticmethod if no inner variables required"
@staticmethod
def warning():
"Use @staticmethod if no inner variables required"
@staticmethod
def error():
"Use @staticmethod if no inner variables required"
@classmethod
def logger_status_cls(cls):
"Use @classmethod to access class level variables"
print(cls.shared_value)
# The Client
# All uses of singleton point to the same memory address (id)
print(f"id(Singleton)\t= {id(LoggerSingleton)}")
OBJECTA = LoggerSingleton()
print(f"id(OBJECTA)\t= {id(OBJECTA)}")
OBJECTB = copy.deepcopy(OBJECTA)
print(f"id(OBJECTB)\t= {id(OBJECTB)}")
OBJECTC = LoggerSingleton()
print(f"id(OBJECTC)\t= {id(OBJECTC)}")
Logger Class
General Solution
"""
Singleton Concept Sample Code
https://sbcode.net/python/singleton/#singletonsingleton_conceptpy
"""
import copy
class Singleton():
"The Singleton Class"
value = []
def __new__(cls):
return cls
@staticmethod
def static_method():
"Use @staticmethod if no inner variables required"
@classmethod
def class_method(cls):
"Use @classmethod to access class level variables"
print(cls.value)
# The Client
# All uses of singleton point to the same memory address (id)
print(f"id(Singleton)\t= {id(Singleton)}")
OBJECT1 = Singleton()
print(f"id(OBJECT1)\t= {id(OBJECT1)}")
OBJECT2 = copy.deepcopy(OBJECT1)
print(f"id(OBJECT2)\t= {id(OBJECT2)}")
OBJECT3 = Singleton()
print(f"id(OBJECT1)\t= {id(OBJECT3)}")
General Solution
Characteristics
- Global access to the instance of an object
- Method classess cannot have more than one instance
- Initialized only when it is requested for the first time
In the projects where we specifically need strong control over the global variables (e.g., logging, caching, thread pools, and configuration settings), it is highly recommended to use Singleton Pattern
Structural Pattern: FaÇades
Facade provides a simplified interface to a library, a framework, or any other complex set of classes.
Natural Language Processing Pipeline
The word Facade means the face of a building or particularly an outer lying interface of a complex system, consists of several sub-systems
Detecting Similar Text
Preprocessing
Vectorizing
Distance Computation
Facade Elements
"""Facade pattern with an example of NLP"""
class Preprocessing:
'''Subsystem # 1'''
def preprocess(self):
print("Washing...")
class Vectorizing:
'''Subsystem # 2'''
def vectorize(self):
print("Rinsing...")
class ComputingDistance:
'''Subsystem # 3'''
def distance(self):
print("Spinning...")
class TextSimilarity:
'''Facade'''
def __init__(self):
self.preprocessing = Preprocessing()
self.vectorizing = Vectorizing()
self.computingDistance = ComputingDistance()
def startSimilarityPipeline(self):
self.preprocessing.preprocess()
self.vectorizing.vectorize()
self.computingDistance.distance()
""" main method """
if __name__ == "__main__":
NLPTextSimilarity = TextSimilarity()
NLPTextSimilarity.startSimilarityPipeline()
Natural Language Processing Similarity
General Solution
"""
The Facade Pattern Concept
"""
class SubSystemClassA:
@staticmethod
def method():
return "A"
class SubSystemClassB:
@staticmethod
def method():
return "B"
class SubSystemClassC:
@staticmethod
def method():
return "C"
# facade
class Facade:
def __init__(self):
self.sub_system_class_a = SubSystemClassA()
self.sub_system_class_b = SubSystemClassB()
self.sub_system_class_c = SubSystemClassC()
def create(self):
result = self.sub_system_class_a.method()
result += self.sub_system_class_b.method()
result += self.sub_system_class_c.method()
return result
# client
FACADE = Facade()
RESULT = FACADE.create()
print("The Result = %s" % RESULT)
General Solution
Characteristics
- [isolation] Code can be isolated from the complexity of a subsystem
- [testing] Code can be tested easily because of modularity
- [coupling] Loose coupling between the clients and the subsystems
Facade is used when we want to provide a unique structure to a sub-system by dividing them into layers.
Behavioral Pattern: Strategy
Strategy lets you define a familiy of algorithms, put each of them into a separate class and make their objects interchangeable.
Sorting Algorithms
In the Strategy, an object/context runs a chosen algorithm, but the state of the object/context doesn't change in case we try a different algorithm.
Sorting Algorithms
Quicksort
Mergesort
Heapsort
Bubblesort
Strategy Elements
- Strategy Interface: an interface that all Strategy subclasses/algorithms must implement
- Concrete Strategy: the subclass that implements an alternative algorithms
- Context: this is the object that receives the concrete strategy to execute it
"""
The Strategy Pattern Concept
https://sbcode.net/python/strategy/#strategystrategy_conceptpy
"""
from abc import ABCMeta, abstractmethod
#Context
class Array():
"This is the object whose behavior will change"
@staticmethod
def request(strategy):
"""The request is handled by the class passed in"""
return strategy()
#Strategy Interface
class IStrategySorting(metaclass=ABCMeta):
"A strategy Interface"
@staticmethod
@abstractmethod
def __str__():
"Implement the __str__ dunder"
#Concrete Strategy
class QuickSort(IStrategySorting):
"A Concrete Strategy Subclass"
def __str__(self):
return "I am QuickSort"
class MergeSort(IStrategySorting):
"A Concrete Strategy Subclass"
def __str__(self):
return "I am MergeSort"
class BubbleSort(IStrategySorting):
"A Concrete Strategy Subclass"
def __str__(self):
return "I am BubbleSort"
# The Client
ListofNumbers = Array([5,6,5,8,9,11,1])
print(ListofNumbers.request(QuickSort))
print(ListofNumbers.request(MergeSort))
print(ListofNumbers.request(BubbleSort))
Sorting Algorithms
"""
The Strategy Pattern Concept
https://sbcode.net/python/strategy/#strategystrategy_conceptpy
"""
from abc import ABCMeta, abstractmethod
#Context
class Array():
"This is the object whose behavior will change"
@staticmethod
def request(strategy):
"""The request is handled by the class passed in"""
return strategy()
#Strategy Interface
class IStrategySorting(metaclass=ABCMeta):
"A strategy Interface"
@staticmethod
@abstractmethod
def __str__():
"Implement the __str__ dunder"
#Concrete Strategy
class QuickSort(IStrategySorting):
"A Concrete Strategy Subclass"
def __str__(self):
return "I am QuickSort"
class MergeSort(IStrategySorting):
"A Concrete Strategy Subclass"
def __str__(self):
return "I am MergeSort"
class BubbleSort(IStrategySorting):
"A Concrete Strategy Subclass"
def __str__(self):
return "I am BubbleSort"
# The Client
ListofNumbers = Array([5,6,5,8,9,11,1])
print(ListofNumbers.request(QuickSort))
print(ListofNumbers.request(MergeSort))
print(ListofNumbers.request(BubbleSort))
How should we add HeapSort?
General Solution
"""
The Strategy Pattern Concept
https://sbcode.net/python/strategy/#strategystrategy_conceptpy
"""
from abc import ABCMeta, abstractmethod
class Context():
"This is the object whose behavior will change"
@staticmethod
def request(strategy):
"""The request is handled by the class passed in"""
return strategy()
class IStrategy(metaclass=ABCMeta):
"A strategy Interface"
@staticmethod
@abstractmethod
def __str__():
"Implement the __str__ dunder"
class ConcreteStrategyA(IStrategy):
"A Concrete Strategy Subclass"
def __str__(self):
return "I am ConcreteStrategyA"
class ConcreteStrategyB(IStrategy):
"A Concrete Strategy Subclass"
def __str__(self):
return "I am ConcreteStrategyB"
class ConcreteStrategyC(IStrategy):
"A Concrete Strategy Subclass"
def __str__(self):
return "I am ConcreteStrategyC"
# The Client
CONTEXT = Context()
print(CONTEXT.request(ConcreteStrategyA))
print(CONTEXT.request(ConcreteStrategyB))
print(CONTEXT.request(ConcreteStrategyC))
General Solution
Characteristics
- Without changing the client's code, it is always easy to introduce a new strategy
- We can isolate specific implementation details of the strategies from client's code
- Variables and data structures are encapsulated in strategy classes. Context class won't be affected by changes in data structures
- It is possible to switch the strategies at the run-time
Strategy is generally used to isolate the business logic of the class from the algorithmic implementation.
Anti-Patterns
An Anti-Pattern is a common response to a recurrent problem that is ineffective and generates software decay.
# Running outer loop from 2 to 3
for i in range(2, 4):
# Printing inside the outer loop
# Running inner loop from 1 to 10
for j in range(1, 11):
if i==j:
#Third Nested
for k in range(1, 10^5):
if k == i:
print("Same Index")
break
# Printing inside the inner loop
print(i, "*", j, "=", i*j)
# Printing inside the outer loop
print()
Is anything odd here? (2 min)
# Running outer loop from 2 to 3
for i in range(2, 4):
# Printing inside the outer loop
# Running inner loop from 1 to 10
for j in range(1, 11):
if i==j:
#Third Nested
for k in range(1, 10^5):
if k == i:
print("Same Index")
break
# Printing inside the inner loop
print(i, "*", j, "=", i*j)
# Printing inside the outer loop
print()
High Cyclomatic Complexity (↑): Nested Loops
def multiply(a, b):
return a * b
def divide(a, b):
return a / b
def power(a, b):
return a ** b
def summation(array):
return functools.reduce(lambda a, b: a+b, array)
def max(array):
return functools.reduce(lambda a, b: a if a > b else b, array)
Is anything odd here?
def multiply(a, b):
return a * b
def divide(a, b):
return a / b
def power(a, b):
return a ** b
def summation(array):
return functools.reduce(lambda a, b: a+b, array)
def max(array):
return functools.reduce(lambda a, b: a if a > b else b, array)
High # of Clones (↑): Duplicate Code
def storeInDataBase(
elementA,
elementB,
elementC,
elementD,
elementE,
elementF,
elementG,
elementH):
##
##
##
return Databaseconnection(..) #all elements
Is anything odd here? (1 min)
def storeInDataBase(
elementA,
elementB,
elementC,
elementD,
elementE,
elementF,
elementG,
elementH):
##
##
##
return Databaseconnection(..) #all elements
High # of Parameters (↑): Too many parameters
class Employee:
retirement_age = 50
def __init__(self, name, id):
self.name = name
self.id = id
self.salary = None
self.remaining_years = None
self.length = length #<---?
def years_to_work(self, present_age):
self.remaining_years = self.retirement_age-present_age
return self.remaining_years
def setSalary(self, salary):
self.salary = salary
def getSalary(self):
return self.salary
def getPerimeter(self):
return round(4 * self.length)
def getArea(self):
return round(self.length * self.length)
Is anything odd here? (2 min)
class Employee:
retirement_age = 50
def __init__(self, name, id):
self.name = name
self.id = id
self.salary = None
self.remaining_years = None
self.length = length #<---?
def years_to_work(self, present_age):
self.remaining_years = self.retirement_age-present_age
return self.remaining_years
def setSalary(self, salary):
self.salary = salary
def getSalary(self):
return self.salary
def getPerimeter(self):
return round(4 * self.length)
def getArea(self):
return round(self.length * self.length)
Unrelated methods: God Class
class Employee:
retirement_age = 50
def __init__(self, name, id):
self.person = Person(name,id)
self.name = self.person.name
self.id = self.person.id
self.salary = None
self.remaining_years = None
def years_to_work(self, present_age):
self.remaining_years = self.person.retirement_age-present_age
return self.remaining_years
def setSalary(self, salary):
self.person.salary = salary
def getSalary(self):
return self.person.salary
Is anything odd here? (3 min)
class Employee:
retirement_age = 50
def __init__(self, name, id):
self.person = Person(name,id)
self.name = self.person.name
self.id = self.person.id
self.salary = None
self.remaining_years = None
def years_to_work(self, present_age):
self.remaining_years = self.person.retirement_age-present_age
return self.remaining_years
def setSalary(self, salary):
self.person.salary = salary
def getSalary(self):
return self.person.salary
Access to Data from Another Object: Feature Envy
Summary
Reusability
Reusability
Creational Patterns
Reusability
Creational Patterns
Structural Patterns
Reusability
Creational Patterns
Structural Patterns
Behavioural Patterns
Appendix
Structural Pattern: Adapter
Adapter allows objects with incompatible interfaces to collaborate.
Multimodality Data
The adapter is similar to the Facade, but you are modifying the method signature, combining other methods and/or transforming data that is exchanged between the existing interface and the client.
Same Vector Representation
Images
Natural Language
Audio
Adapter Elements
- Target: The domain-specific class to be adapted
- Adapter: The concrete adapter class with the logic/algorithms needed for the adoption process
Vectorizing Multimodal Data
class VideoData:
"""Class for VideoData"""
def __init__(self):
self.name = "VideoData"
def Video2Tensor(self):
return "Video2Tensor"
class TextData:
"""Class for TextData"""
def __init__(self):
self.name = "TextData"
def Text2Tensor(self):
return "Text2Tensor"
class AudioData:
"""Class for AudioData"""
def __init__(self):
self.name = "AudioData"
def Audio2Tensor(self):
return "Text2Tensor"
class TensorAdapter:
"""
Adapts an object by replacing methods.
Usage:
audioData = AudioData()
audioData = Adapter(audioData, vectorization = audioData.Audio2Tensor)
"""
def __init__(self, obj, **adapted_methods):
"""We set the adapted methods in the object's dict"""
self.obj = obj
self.__dict__.update(adapted_methods)
def __getattr__(self, attr):
"""All non-adapted calls are passed to the object"""
return getattr(self.obj, attr)
def original_dict(self):
"""Print original object dict"""
return self.obj.__dict__
""" main method """
if __name__ == "__main__":
"""list to store objects"""
objects = []
audioData = MotorCycle()
objects.append(Adapter(audioData, vectorization = audioData.Audio2Tensor))
textData = TextData()
objects.append(Adapter(textData, vectorization = textData.Text2Tensor))
videoData = VideoData()
objects.append(Adapter(videoData, vectorization = videoData.Video2Tensor))
for obj in objects:
print("A {0} is a {1} Tensor".format(obj.name, obj.vectorization()))
Adapter Pattern is always used when we are in need to make certain classes compatible to communicate.
Design Patterns
By David Nader Palacio
Design Patterns
- 87