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.