Bookmarks

You haven't yet saved any bookmarks. To bookmark a post, just click .

  • SOLID Principles made easy

  • SOLID is an acronym for first five object-oriented design (OOD) principles by Robert C. Martin (aka Uncle Bob).

    These principles make developers life easy by making foundations for maintenance, clean code and extending of code base as project grows. These design principles encourage us to create more maintainable, understandable, and flexible software.

    S — Single Responsibility Principle

    A class should have one and only one reason to change, meaning a class/method  should have only one job.
    • Classes and methods should have high cohesion
    class DrawShapes:
        def draw_circle(self):
            [...]
    	
        def draw_rectangle(self):
            [...]
    
        # This function violates the principle
        # Give it it's own class
        def clear_shape(self):
             [...]                 

    Solution:

    class DrawShapes:
        def draw_circle(self):
            [...]
    	
        def draw_rectangle(self):
            [...]
    
    
    class Display:
        def clear_shape(self):
             [...]                 

    O — Open/Closed Principle

    Objects or entities should be open for extension but closed for modification.

    The above code also violates the second principle, what if we wanted to draw circle using different algorithm? or in different color?

    i.e., the previous class DrawShapes need to be modified to make changed happen, but that's what second principle states; class should be closed for modification.

    The below code solves the issue:

    class Circle:
        # This class is closed for modification
        def draw_circle(self):
            """Draw default circle"""
            [...]
    
    # But open for extension
    class CircleNewAlgorithm(Circle):
        # The different function to draw circle
        def draw_circle_using_new_algo(self):
            [...]

    L — Liskov Substitution Principle

    Let q(x) be a property provable about objects of x of type T. Then q(y) should be provable for objects y of type S where S is a subtype of T
    Parent classes should be replaceable by their subclasses but without altering the behavior.

    In previous code, the class CircleNewAlgorithm class's function draw_circle_using_new_algo breaks the third principle.

    If a parent class can do something, a child class must also be able to do it.

    The newer class is supposed to draw circle using new algorithm but using inheritance gets the older function as well, making it ambiguous to which function should be called:

    circle = CircleNewAlgorithm()
    
    # Which of the two function should be called?
    circle.draw_circle()                # older algorithm
    circle.draw_circle_using_new_algo() # newer algorithm

    The solution is simply to override the previous implementation:

    class CircleNewAlgorithm(Circle):
        # Override older function to draw circle
        draw_circle(self):
            """Newer algorithm goes here"""
            [...]

    I — Interface Segregation Principle

    A client/classes should never be forced to implement an interface that it doesn't use, clients shouldn't be forced to depend on methods they do not use.

    Use several specific interfaces instead of one general one.

    Lets say our original Circle class implement WindowInterface like this:

    from abc import abstractmethod
    
    # Abstract base class used as interface for example
    # but other language support interfaces
    class WindowInterface:
        @abstractmethod
        def draw_circle(self):
            pass
    
        @abstractmethod
        def clear_screen(self):
            """function to clear screen"""
            pass
    

    The abstract method clear_screen violates fourth principle as class implementing this might require to implement clear_screen method as well, which obviously is not required for a child class of Circle to handle. This will also violate first principle.

    But what if the method is somehow reasonable to belong to interface and not to classes implementing them, then

    In that case you can provide default implementation for that function (like default methods in Java) so the class implementing the interface need not to provide the implementation if they don't use it.

    Remember a class should not be forced to implement an interface that it doesn't use.

    For our example, solution might look like this:

    from abc import abstractmethod
    
    # Our interface for class
    class CircleInterface:
        @abstractmehod
        def draw_circle(self):
            pass
    
    class Circle(CircleInterface):
        def draw_circle(self):
            """implement circle drawing function"""
            [...]
    
    # Our interface for display
    class DisplayInterface:
        @abstractmethod
        def clear_screen():
            pass
    
    class Display(DisplayInterface)
        def clear_screen(self):
            """implement screen clearning function"""
            [...]
    

    D — Dependency Inversion Principle

    Entities/classes must depend on abstractions, not on concretions. It states that the high-level module must not depend on the low-level module, but they should depend on abstractions.

    The principle allows for decoupling.

    Let's say out Circle class depends on some external library to render the shape it draws.

    class Circle:
        import GraphicsLibrary
        def __init__(self):
        # external concreate dependency is problematic
            self.driver = self.GraphicsLibrary.driver 
    
        def draw_circle(self)
            self.driver.draw_pixel()
    
    circle = Circle()
    

    The problem arises when we want to use some other library for rendering or create our own library. The concretion makes it harder to make that change to current implementation.

    What we can do is something like this:

    from abc import abstractmethod
    
    # create an interface to render
    class Renderer():
        @abstractmethod
        def driver(self):
            pass
    
    # Create our class for external render library 
    class GraphicsLibraryRender(Render):
        import GraphicsLibrary
        def driver(self):
            return self.GraphicsLibrary.driver
    
    # and our Circle class can now use the interface
    class Circle:
        def __init__(self, Renderer):
            self.driver = self.Renderer.driver() 
    
        def draw_circle(self)
            self.driver.draw_pixel()
    
    graphics_ren = GraphicsLibraryRender()
    circle = Circle(graphics_ren)
    

    What this will now allow us to use different classes that implements Renderer interface and that could be external library or in future your own library or dummy library for dry run without modifying the entire codebase.

    Keep Learning...