Skip to content

Latest commit

 

History

History

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 

README.md

Topic of contents


Following are the 5 SOLID design principles (definitions are taken from Wikipedia):

The single-responsibility principle

"There should never be more than one reason for a class to change." In other words, every class should have only one responsibility. This is also known as separation of concerns. Following sample displays this effect:

from typing import Union, Any

class Journal:
    """
    currently this class does only 1 thing i.e. managing entries - logging, deleting and displaying
    """
    def __init__(self):
        self.entries = []
        self.count = 0
        
    def add_entry(self, note: str):
        self.count += 1
        self.entries.append(f"{self.count}. {note}")
    
    def remove_entry(self, pos: int):
        del self.entries[pos]
        
    def __str__(self) -> str:
        return "\n".join(self.entries)
    
    
    # this is an anti-pattern, when we try to add save/load features i.e. persistance to the Journal class
    # if we have other classes, they would need similar functioanily and then we would be implementing similar methods for those classes as well
    # thus it's better to handle this is a separate class - separation of concerns :)
    def save(self, file_path: str):
        pass
    
    def load(self, file_path: str):
        pass
    
    
class PersistanceManager:
    @staticmethod
    # can take any other type
    def save(journal: Union[Journal, Any], filename: str):
        with open(filename, "w") as f:
            f.write(str(journal))
    
    @staticmethod
    def load(filepath: str):
        with open(filepath, "r") as f:
            return f.read()


if __name__ == "__main__":
    j = Journal()
    j.add_entry("this is good.")          
    j.add_entry("hello world")
    print(j)

    # saving and loading journal
    journal_path = "journal.log"
    PersistanceManager.save(j, journal_path)
    print("\n loaded journal")
    print(PersistanceManager.load(journal_path))

The open–closed principle

"Software entities ... should be open for extension, but closed for modification." Following sample displays that how adding modifying the class to deliver new feature would result into state-space explosion. And, how we can use Specification pattern to better solve the problem.

from enum import Enum

class Color(Enum):
    RED = 1
    GREEN = 2
    BLUE = 3

class Size(Enum):
    SMALL = 1
    MEDIUM = 2
    LARGE = 3
    
class Product():
    def __init__(self, name, color, size) -> None:
        self.name = name
        self.color = color
        self.size = size
        
    
# let's say we need to filter product by color
class FilterProduct:
    def filter_by_color(self, products, color):
        for p in products:
            if p.color == color:
                yield p
        
    # now let's say, later, we want to filter products by size too and hence add the following method
    # this violated the open-closed principle as we have modified the class rather than extending it
    # since these filter addition can keep on going, say filter by both color and size, filter by color or size 
    def filter_by_size(self, products, size):
        for p in products:
            if p.size == size:
                yield p
                
                
    def filter_by_size_and_color(self, products, size, color):
        for p in products:
            if p.size == size and p.color == color:
                yield p
        
        
# by adding filters for each case will cause state-space explosion
# 2 features -> 3 possible filter method with and
# 3 features -> 7 possible filter method with and


# Apart from Design-patterns there are Enterprise patterns and one of those is Specification which we can use here  
# Specification will be a base class that determines if a particular item satisfies as particular criteria 
class BaseSpecification:
    def is_satisfied(self, item):
        pass
    
    # support & for creating specs, this overloads "&" not "and" 
    # we need this __and__ in BaseSpecification coz we will be applying it on BaseSpecification classes like ColorSpecification, SizeSpecification
    def __and__(self, other):
        return AndSpecification(self, other)
    
    
# base class that takes items and the spec to filter upon
class BaseFilter:
    def filter(self, items, spec):
        pass

# now if we want to filter by some critera(s), we simply extend these classes
class ColorSpecification(BaseSpecification):
    def __init__(self, color) -> None:
        self.color = color
    
    def is_satisfied(self, item):
        return item.color == self.color
    

class SizeSpecification(BaseSpecification):
    def __init__(self, size) -> None:
        self.size = size
            
    def is_satisfied(self, item):
        return item.size == self.size


# And Specification
class AndSpecification(BaseSpecification):
    def __init__(self, *args) -> None:
        self.specs = args

    # return true only if all specs are satisfied for the item
    def is_satisfied(self, item):
        return all(map(
            lambda spec: spec.is_satisfied(item), self.specs 
        ))


class Filter(BaseFilter):
    # why we are defining filter here and not in BaseFilter?
    # that's coz it will allow us to implement different types of filtering 
    # for e.g. we are assuming items to be iterable which might not always hold and in that case we will extend BaseFilter to handle it
    def filter(self, items, spec):
        for item in items:
            if spec.is_satisfied(item):
                yield item
                
             
if __name__ == "__main__":
    apple = Product("Apple", Color.GREEN, Size.SMALL)
    tree = Product("Tree", Color.GREEN, Size.LARGE)
    house = Product("House", Color.BLUE, Size.LARGE)
    
    products = [apple, tree, house]

    
    # old approach
    fp = FilterProduct()
    print('Green products (old approach): ')
    for p in fp.filter_by_color(products, Color.GREEN):
        print(f'\t{p.name} is green')
        
    
    # new approach
    fp_new = Filter()
    green_spec = ColorSpecification(Color.GREEN)
    
    print('\nGreen products (new approach): ')
    for p in fp_new.filter(products, green_spec):
        print(f'\t{p.name} is green')
        
    
    # combining filters
    green_and_large_spec = AndSpecification(green_spec, SizeSpecification(Size.LARGE))
    
    print('\nGreen and Large products (new approach): ')
    for p in fp_new.filter(products, green_and_large_spec):
        print(f'\t{p.name} is green and large')
        
        
    # combining filters using & operator
    green_and_large_spec2 = green_spec & SizeSpecification(Size.LARGE)
    
    print('\nGreen and Large products (new approach): ')
    for p in fp_new.filter(products, green_and_large_spec2):
        print(f'\t{p.name} is green and large')

The Liskov substitution principle

"Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it."


The interface segregation principle

"Many client-specific interfaces are better than one general-purpose interface."


The dependency inversion principle

"Depend upon abstractions, [not] concretions."