{"id":1993,"date":"2023-12-22T08:44:45","date_gmt":"2023-12-22T08:44:45","guid":{"rendered":"http:\/\/write.muthu.co\/?p=1993"},"modified":"2023-12-22T09:16:12","modified_gmt":"2023-12-22T09:16:12","slug":"a-crash-course-in-solid-principles","status":"publish","type":"post","link":"http:\/\/write.muthu.co\/a-crash-course-in-solid-principles\/","title":{"rendered":"A Crash Course in SOLID Principles"},"content":{"rendered":"\n

The SOLID principles are a set of five design principles in object-oriented programming intended to make software designs more understandable, flexible, and maintainable. These principles were introduced by Robert C. Martin (also known as Uncle Bob) and are considered fundamental guidelines for creating high-quality, robust, and scalable software systems.<\/p>\n\n\n\n

SOLID <\/strong>stands for:<\/p>\n\n\n\n

\n

S<\/strong> – Single-Responsiblity Principle \u00a0<\/p>\n\n\n\n

O<\/strong> – Open-closed Principle \u00a0<\/p>\n\n\n\n

L<\/strong> – Liskov Substitution Principle \u00a0<\/p>\n\n\n\n

I<\/strong> – Interface Segregation Principle \u00a0<\/p>\n\n\n\n

D<\/strong> – Dependency Inversion Principle<\/p>\n<\/div><\/div>\n\n\n\n

Single Responsibility Principle (SRP)<\/h2>\n\n\n\n
A class should have only one reason to change<\/strong><\/td><\/tr>
What it means<\/td>It should encapsulate one and only one aspect of functionality.<\/td><\/tr>
Application<\/td>Design classes to perform a single, well-defined task, making them focused, easier to understand, test, and maintain.<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n

To understand this concept in detail, let’s build an Area Calculator application that takes in a list of shapes and calculates their total area.<\/p>\n\n\n\n

Let’s start by creating a class to represent our squares, which takes the length of its side as a constructor parameter.<\/p>\n\n\n\n

\"\"<\/figure>\n\n\n\n
class Square {\r\n    \/\/ Use double for precision with area calculations\r\n    private double sideLength; \r\n\r\n    public Square(double sideLength) {\r\n        this.sideLength = sideLength;\r\n    }\r\n\r\n    public double getArea() {\r\n        \/\/ Calculate area as sideLength squared\r\n        return Math.pow(sideLength, 2); \r\n    }\r\n}<\/code><\/pre>\n\n\n\n

For circles, you will need to know the radius:<\/p>\n\n\n\n

\"\"<\/figure>\n\n\n\n
class Circle {\r\n    \/\/ Use double for precision with area calculations\r\n    private double radius;\r\n\r\n    public Circle(double radius) {\r\n        this.radius = radius;\r\n    }\r\n\r\n    public double getArea() {\r\n        \/\/ Calculate area using Pi and radius squared\r\n        return Math.PI * Math.pow(radius, 2); \r\n    }\r\n}\n<\/code><\/pre>\n\n\n\n

To find the total area covered by all our shapes, we’ll build an AreaCalculator<\/code> class, which will know how to calculate the area of each shape, whether it’s a Square <\/code>or a Circle<\/code>.<\/p>\n\n\n\n

\"\"<\/figure>\n\n\n\n
class AreaCalculator {\r\n\r\n    private List<Object> shapes;\r\n\r\n\tpublic AreaCalculator(List<Object> shapes) {\r\n\t\tthis.shapes = shapes;\r\n\t}\r\n\r\n\tpublic double calculateTotalArea() {\r\n\t\tdouble totalArea = 0;\r\n\t\tfor (Object shape : shapes) {\r\n\t\t\tif (shape instanceof Square) {\r\n\t\t\t\tSquare square = (Square) shape;\r\n\t\t\t\ttotalArea += square.getArea();\r\n\t\t\t} else if (shape instanceof Circle) {\r\n\t\t\t\tCircle circle = (Circle) shape;\r\n\t\t\t\ttotalArea += circle.getArea();\r\n\t\t\t} else {\r\n\t\t\t\tthrow new IllegalArgumentException(\"Unsupported shape type\");\r\n\t\t\t}\r\n\t\t}\r\n\t\treturn totalArea;\r\n\t}\r\n\r\n\tpublic String printAreaSummary() {\r\n\t\treturn String.format(\"Sum of the areas of provided shapes: %.2f\\n\", calculateTotalArea());\r\n\t}\r\n}<\/code><\/pre>\n\n\n\n

If you look carefully, you’ll see that the AreaCalculator <\/code>class does two things:<\/p>\n\n\n\n

    \n
  • it calculates the area of shapes and<\/li>\n\n\n\n
  • handles printing the total area.<\/li>\n<\/ul>\n\n\n\n

    If you want to display the area information differently, you might need to modify the printAreaSummary <\/code>method. This could involve formatting the output string differently, or even returning the areas for each shape individually instead of a single sum.<\/p>\n\n\n\n

    The purpose of the AreaCalculator <\/code>should be solely to calculate the area (adhering to the Single Responsibility Principle<\/strong>). The only reason it should ever change is when we want to support more types of shapes.<\/p>\n\n\n\n

    Therefore, let’s move the printing responsibility to a separate class called AreaPrinter<\/code>.<\/p>\n\n\n\n

    class AreaPrinter {\r\n\r\n\t\/\/ Use final for immutability\r\n    private final AreaCalculator calculator; \r\n\r\n    public AreaPrinter(AreaCalculator calculator) {\r\n        this.calculator = calculator;\r\n    }\r\n\r\n    public String getJSON() {\r\n    \t\/\/ Use Map for clearer data structure\r\n        Map<String, Double> data = new HashMap<>(); \r\n        data.put(\"sum\", calculator.calculateTotalArea());\r\n        \/\/ Use appropriate JSON encoder library\r\n        return JsonUtils.encodeToString(data);\r\n    }\r\n\r\n    public String getHTML() {\r\n        return String.format(\r\n                \"Sum of the areas of provided shapes: %.2f\\n\",\r\n                calculator.calculateTotalArea());\r\n    }\r\n}<\/code><\/pre>\n\n\n\n

    Now, our AreaCalculator <\/code>class does one thing and one thing only.<\/p>\n\n\n\n

    class AreaCalculator {\r\n\r\n    private List<Object> shapes;\r\n\r\n\tpublic AreaCalculator(List<Object> shapes) {\r\n\t\tthis.shapes = shapes;\r\n\t}\r\n\r\n\tpublic double calculateTotalArea() {\r\n\t\tdouble totalArea = 0;\r\n\t\tfor (Object shape : shapes) {\r\n\t\t\tif (shape instanceof Square) {\r\n\t\t\t\tSquare square = (Square) shape;\r\n\t\t\t\ttotalArea += square.getArea();\r\n\t\t\t} else if (shape instanceof Circle) {\r\n\t\t\t\tCircle circle = (Circle) shape;\r\n\t\t\t\ttotalArea += circle.getArea();\r\n\t\t\t} else {\r\n\t\t\t\tthrow new IllegalArgumentException(\"Unsupported shape type\");\r\n\t\t\t}\r\n\t\t}\r\n\t\treturn totalArea;\r\n\t}\r\n}<\/code><\/pre>\n\n\n\n

    Open\/Closed Principle (OCP)<\/h2>\n\n\n\n
    Software entities (classes, modules, functions) should be open for extension but closed for modification<\/strong><\/td><\/tr>
    What it means<\/td>Once a class is written and working, it should be open for extension to add new features, but its existing code should not be modified.<\/td><\/tr>
    Application<\/td>Use abstractions (like interfaces or abstract classes) to build a foundation that can be extended without changing existing code.<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n

    The class AreaCalculator <\/code>we wrote follows the Single responsibilty Principle<\/strong> well but it fails to follow the open\/closed principle because:<\/p>\n\n\n\n

      \n
    • Each new shape requires modifying calculateArea<\/code>. (modification is open)<\/li>\n\n\n\n
    • Conditional logic becomes complex as more shapes are added. (modification is open)<\/li>\n<\/ul>\n\n\n\n

      To make our class OCP-compliant<\/strong>, we should first close the possibility of modification of calculateArea <\/code>method which we can do using the below steps:<\/p>\n\n\n\n

      1. Define a Shape interface<\/h4>\n\n\n\n
      \"\"<\/figure>\n\n\n\n
      interface Shape {\r\n\tdouble getArea();\r\n}<\/code><\/pre>\n\n\n\n

      2. Create Concrete Shape classes<\/h4>\n\n\n\n
      \"\"<\/figure>\n\n\n\n
      class Square implements Shape {\r\n    \/\/ Use double for precision with area calculations\r\n    private double sideLength; \r\n\r\n    public Square(double sideLength) {\r\n        this.sideLength = sideLength;\r\n    }\r\n\r\n    public double getArea() {\r\n        \/\/ Calculate area as sideLength squared\r\n        return Math.pow(sideLength, 2); \r\n    }\r\n}\r\n\r\n\r\nclass Circle implements Shape {\r\n\r\n    private double radius; \/\/ Use double for precision with area calculations\r\n\r\n    public Circle(double radius) {\r\n        this.radius = radius;\r\n    }\r\n\r\n    public double getArea() {\r\n        return Math.PI * Math.pow(radius, 2); \/\/ Calculate area using Pi and radius squared\r\n    }\r\n}\r\n<\/code><\/pre>\n\n\n\n

      3. Modify the AreaCalculator class<\/h4>\n\n\n\n
      class AreaCalculator {\r\n\r\n    private List<Shape> shapes;\r\n\r\n    public AreaCalculator(List<Shape> shapes) {\r\n        this.shapes = shapes;\r\n    }\r\n\r\n    public double calculateTotalArea() {\r\n        double totalArea = 0;\r\n        for (Shape shape : shapes) {\r\n        \ttotalArea += shape.getArea();\r\n        }\r\n        return totalArea;\r\n    }\r\n}<\/code><\/pre>\n\n\n\n
        \n
      • AreaCalculator <\/code>is now closed for modification, open for extension. We can extend this class further and add new methods or override the existing method.<\/li>\n\n\n\n
      • Adding new shapes doesn’t require changing AreaCalculator <\/code>class, it will be automatically supported.<\/li>\n\n\n\n
      • Code is more maintainable, testable, and extensible.<\/li>\n<\/ul>\n\n\n\n

        Liskov Substitution Principle (LSP)<\/h2>\n\n\n\n
        Objects of a superclass should be replaceable with objects of its subclasses without affecting the correctness of the program<\/strong><\/td><\/tr>
        What it means<\/td>Subtypes must be substitutable for their base types without altering the correctness of the program<\/td><\/tr>
        Application<\/td>When inheriting from a class or implementing an interface, ensure that the derived types can be used interchangeably with their base types<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n

        The Liskov Substitution Principle (LSP) plays a crucial role in ensuring the flexibility of software systems. Named after Barbara Liskov, who introduced the principle in 1987, LSP is one of the five SOLID principles that aim to improve the design and maintainability of software.<\/p>\n\n\n\n

        At its core, the Liskov Substitution Principle articulates the idea of substitutability between objects of different types within an inheritance hierarchy. The principle asserts that if a class (let’s call it S) is a subtype of another class (let’s call it T), instances of class T should be replaceable with instances of class S without altering the correctness of the program. In simpler terms, a child class should seamlessly replace its parent class without introducing errors or modifying the expected behavior of the system.<\/p>\n\n\n\n

        Imagine a scenario where you have a base class representing a general shape and a derived class representing a specific type of shape, say a square. According to LSP, if square (S) is a subtype of shape (T), you should be able to substitute an instance of the square class wherever an instance of the shape class is expected, without causing any issues.<\/p>\n\n\n\n

        Let’s consider a scenario involving a Rectangle <\/code>and a Square<\/code>, where Square <\/code>is a subclass of Rectangle<\/code>. According to classical geometry, a square is a special case of a rectangle where all sides are of equal length. However, implementing this relationship directly might lead to a violation of LSP.<\/p>\n\n\n\n

        class Rectangle {\r\n    protected int width;\r\n    protected int height;\r\n\r\n    public void setWidth(int width) {\r\n        this.width = width;\r\n    }\r\n\r\n    public void setHeight(int height) {\r\n        this.height = height;\r\n    }\r\n\r\n    public int getArea() {\r\n        return this.width * this.height;\r\n    }\r\n}\r\n\r\nclass Square extends Rectangle {\r\n    @Override\r\n    public void setWidth(int width) {\r\n        this.width = width;\r\n        this.height = width;\r\n    }\r\n\r\n    @Override\r\n    public void setHeight(int height) {\r\n        this.height = height;\r\n        this.width = height;\r\n    }\r\n}<\/code><\/pre>\n\n\n\n

        In this scenario, a Square <\/code>is modeled as a subclass of Rectangle<\/code>. However, by overriding the setWidth() <\/code>and setHeight()<\/code> methods in the Square <\/code>class to set both dimensions to the same value, we violate the LSP. This is because a Square <\/code>behaves differently than a Rectangle <\/code>by constraining both width and height to the same value, which is not consistent with the behavior expected from a Rectangle<\/code>.<\/p>\n\n\n\n

        Fixing the Code using LSP<\/h4>\n\n\n\n

        To adhere to the Liskov Substitution Principle, it’s essential to reconsider the relationship between Square <\/code>and Rectangle<\/code>. Instead of making Square <\/code>a subclass of Rectangle<\/code>, let’s refactor the code to remove this inheritance relationship and create separate classes for Square <\/code>and Rectangle <\/code>that do not violate the behavior of each other:<\/p>\n\n\n\n

        class Rectangle {\r\n    protected int width;\r\n    protected int height;\r\n\r\n    public Rectangle(int width, int height) {\r\n        this.width = width;\r\n        this.height = height;\r\n    }\r\n\r\n    public int getArea() {\r\n        return this.width * this.height;\r\n    }\r\n}\r\n\r\nclass Square {\r\n    private int side;\r\n\r\n    public Square(int side) {\r\n        this.side = side;\r\n    }\r\n\r\n    public int getArea() {\r\n        return this.side * this.side;\r\n    }\r\n}<\/code><\/pre>\n\n\n\n

        In this updated code, both Rectangle <\/code>and Square <\/code>are separate classes with distinct behaviors. Each class calculates its area based on its specific properties without inheriting from each other. This design adheres to LSP by ensuring that instances of Rectangle <\/code>and Square<\/code> do not alter each other’s behavior when used interchangeably.<\/p>\n\n\n\n

        Why is LSP important? The principle contributes to code reusability, extensibility, and maintainability. When adhering to LSP, developers can confidently extend existing classes without fear of introducing unexpected behaviors or errors. This promotes a modular and scalable codebase, allowing for easier updates and modifications.<\/p>\n\n\n\n

        Interface Segregation Principle (ISP)<\/h2>\n\n\n\n
        A client should not be forced to depend on interfaces they do not use<\/strong><\/td><\/tr>
        What it means<\/td>Split large interfaces into smaller, more specific ones so that clients only need to know about the methods that are of interest to them.<\/td><\/tr>
        Application<\/td>Design cohesive and specific interfaces, avoiding “fat” interfaces that force implementing classes to provide unnecessary functionalities<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n

        Imagine you’re building a house. Would you rather have a single, oversized toolbox containing every possible tool you might need, or several smaller, specialized toolkits for specific tasks like carpentry, plumbing, and electrical work? The answer is obvious: smaller focused toolkits make your work more efficient and organized.<\/p>\n\n\n\n

        The same principle applies to software design also. The Interface Segregation Principle (ISP)<\/strong> tells you to break down large interfaces into smaller, more focused ones based on functionality<\/em><\/strong>. This leads to cleaner and more maintainable code. Take for example, the below Fat<\/em><\/strong> interface.<\/p>\n\n\n\n

        interface Machine {\r\n    void print();\r\n    void scan();\r\n    void fax();\r\n}\r\n\r\nclass MultiFunctionPrinter implements Machine {\r\n    @Override\r\n    public void print() {\r\n        \/\/ Printing implementation\r\n    }\r\n\r\n    @Override\r\n    public void scan() {\r\n        \/\/ Scanning implementation\r\n    }\r\n\r\n    \/\/ Forced to implement fax(), even though it's not used\r\n    @Override\r\n    public void fax() {\r\n        \/\/ Do nothing or throw an exception\r\n    }\r\n}<\/code><\/pre>\n\n\n\n

        The fat<\/em><\/strong> Machine <\/code>interface forces clients (classes implementing the interface) to carry around methods they don’t need, adding unnecessary complexity and weight. This can lead to:<\/p>\n\n\n\n

          \n
        • Increased coupling<\/strong>: Clients become dependent on the entire interface, even for unused methods. This makes them less flexible and harder to reuse in different contexts.<\/li>\n\n\n\n
        • Code bloat<\/strong>: Unnecessary methods clutter up the codebase, making it harder to understand and maintain.<\/li>\n\n\n\n
        • Confusion and errors<\/strong>: Developers implementing the interface might mistakenly use the wrong method, leading to unexpected behavior and bugs.<\/li>\n<\/ul>\n\n\n\n

          Lets split up the Fat <\/em><\/strong>interface into a thinner cohesive <\/em><\/strong>interface.<\/p>\n\n\n\n

          interface Printable {\r\n    void print();\r\n}\r\n\r\ninterface Scannable {\r\n    void scan();\r\n}\r\n\r\ninterface Faxable {\r\n    void fax();\r\n}\r\n\r\nclass MultiFunctionPrinter implements Printable, Scannable {\r\n    @Override\r\n    public void print() {\r\n        \/\/ Printing implementation\r\n    }\r\n\r\n    @Override\r\n    public void scan() {\r\n        \/\/ Scanning implementation\r\n    }\r\n}\r\n\r\nclass SimplePrinter implements Printable {\r\n    @Override\r\n    public void print() {\r\n        \/\/ Printing implementation\r\n    }\r\n}<\/code><\/pre>\n\n\n\n

          By splitting up fat interfaces into smaller, cohesive interfaces<\/em> based on functionality, we achieve several benefits:<\/p>\n\n\n\n

            \n
          • Improved clarity:<\/strong> Each interface clearly defines a specific set of related functionalities, making it easier to understand and use.<\/li>\n\n\n\n
          • Reduced coupling:<\/strong> Clients only need to implement the interfaces that provide the methods they actually use, leading to looser coupling and increased reusability.<\/li>\n\n\n\n
          • Enhanced maintainability<\/strong>: Smaller interfaces are easier to modify and evolve without impacting other parts of the codebase.<\/li>\n\n\n\n
          • Developer satisfaction:<\/strong> Working with focused interfaces is more efficient and enjoyable for developers, leading to better code quality and productivity.<\/li>\n<\/ul>\n\n\n\n

            The Interface Segregation Principle<\/em><\/strong> helps you break down fat interfaces into smaller, focused ones, improving code clarity, and ultimately create software that is easier to understand, use, and maintain.<\/p>\n\n\n\n

            Dependency Inversion Principle (DIP)<\/h2>\n\n\n\n
            High-level modules should not depend on low-level modules. Both should depend on abstractions<\/td><\/tr>
            What it means<\/td>Abstractions (interfaces or abstract classes) should define the interaction between high-level and low-level modules. Details should depend on abstractions, not the other way around.<\/td><\/tr>
            Application<\/td>Use dependency injection, inversion of control containers, and programming to interfaces to achieve loose coupling between modules.<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n

            Dependency Inversion Principle states that “High-level modules should not depend on low-level modules. Both should depend on abstractions.<\/em><\/strong>” In simpler terms, your code shouldn’t be directly tied to specific implementations. Instead, it should rely on abstract interfaces or base classes that define the desired behavior, allowing for flexibility and adaptability.<\/p>\n\n\n\n

            class HighLevelModule {\r\n    private LowLevelModule lowLevelModule = new LowLevelModule();\r\n\r\n    public void doSomething() {\r\n        lowLevelModule.specificOperation();\r\n    }\r\n}<\/code><\/pre>\n\n\n\n

            You can see that:<\/p>\n\n\n\n

              \n
            • HighLevelModule <\/code>is tightly coupled to LowLevelModule<\/code>, making it difficult to change or test independently.<\/li>\n\n\n\n
            • Introducing a new LowLevelModule <\/code>implementation would require code changes in HighLevelModule<\/code>.<\/li>\n<\/ul>\n\n\n\n

              To make the above functionality Dependency Inversion Principle<\/em><\/strong> compliant, we redesign our class as shown below:<\/p>\n\n\n\n

              interface Abstraction {\r\n    void operation();\r\n}\r\n\r\nclass LowLevelModule implements Abstraction {\r\n    @Override\r\n    public void operation() {\r\n        \/\/ Specific implementation\r\n    }\r\n}\r\n\r\nclass HighLevelModule {\r\n    private Abstraction abstraction;\r\n\r\n    public HighLevelModule(Abstraction abstraction) {\r\n        this.abstraction = abstraction;\r\n    }\r\n\r\n    public void doSomething() {\r\n        abstraction.operation();\r\n    }\r\n}<\/code><\/pre>\n\n\n\n

              Now, you can see that:<\/p>\n\n\n\n

                \n
              • HighLevelModule <\/code>depends on the Abstraction <\/code>interface, not the concrete LowLevelModule<\/code>.<\/li>\n\n\n\n
              • Concrete implementations can be injected through the constructor, allowing flexibility and testability.<\/li>\n\n\n\n
              • New implementations can be introduced without modifying HighLevelModule<\/code>.<\/li>\n<\/ul>\n\n\n\n

                A key consideration in the development of real-time applications is maintaining a loose coupling between High-level and Low-level modules. When a class possesses knowledge about the design and implementation of another class, it introduces the risk that any changes made to one class might disrupt the functionality of the other. Therefore, it is crucial to ensure loose coupling between high-level and low-level modules\/classes. Achieving this involves making both modules dependent on abstractions rather than having direct knowledge or instance of each other.<\/p>\n\n\n\n

                Bibliography<\/h2>\n\n\n\n

                Martin,\u00a0Robert,\u00a0and\u00a0Sommerville,\u00a0Ian.\u00a0Value Pack: Software Engineering with Agile Software Development, Principles, Patterns and Practices.\u00a0United Kingdom,Pearson Education, Limited,\u00a02004.<\/p>\n","protected":false},"excerpt":{"rendered":"

                The SOLID principles are a set of five design principles in object-oriented programming intended to make software designs more understandable, flexible, and maintainable. These principles were introduced by Robert C. Martin (also known as Uncle Bob) and are considered fundamental guidelines for creating high-quality, robust, and scalable software systems. SOLID stands for: S – Single-Responsiblity […]<\/p>\n","protected":false},"author":1,"featured_media":2005,"comment_status":"closed","ping_status":"","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[69],"tags":[],"_links":{"self":[{"href":"http:\/\/write.muthu.co\/wp-json\/wp\/v2\/posts\/1993"}],"collection":[{"href":"http:\/\/write.muthu.co\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"http:\/\/write.muthu.co\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"http:\/\/write.muthu.co\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"http:\/\/write.muthu.co\/wp-json\/wp\/v2\/comments?post=1993"}],"version-history":[{"count":3,"href":"http:\/\/write.muthu.co\/wp-json\/wp\/v2\/posts\/1993\/revisions"}],"predecessor-version":[{"id":2004,"href":"http:\/\/write.muthu.co\/wp-json\/wp\/v2\/posts\/1993\/revisions\/2004"}],"wp:featuredmedia":[{"embeddable":true,"href":"http:\/\/write.muthu.co\/wp-json\/wp\/v2\/media\/2005"}],"wp:attachment":[{"href":"http:\/\/write.muthu.co\/wp-json\/wp\/v2\/media?parent=1993"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"http:\/\/write.muthu.co\/wp-json\/wp\/v2\/categories?post=1993"},{"taxonomy":"post_tag","embeddable":true,"href":"http:\/\/write.muthu.co\/wp-json\/wp\/v2\/tags?post=1993"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}