{"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 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 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 For circles, you will need to know the radius:<\/p>\n\n\n\n To find the total area covered by all our shapes, we’ll build an If you look carefully, you’ll see that the If you want to display the area information differently, you might need to modify the The purpose of the Therefore, let’s move the printing responsibility to a separate class called Now, our The class To make our class OCP-compliant<\/strong>, we should first close the possibility of modification of 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 In this scenario, a To adhere to the Liskov Substitution Principle, it’s essential to reconsider the relationship between In this updated code, both 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 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 The fat<\/em><\/strong> Lets split up the Fat <\/em><\/strong>interface into a thinner cohesive <\/em><\/strong>interface.<\/p>\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 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 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 You can see that:<\/p>\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 Now, you can see that:<\/p>\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 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}]}}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 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
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
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
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
AreaCalculator <\/code>class does two things:<\/p>\n\n\n\n
\n
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
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
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
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 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
calculateArea<\/code>. (modification is open)<\/li>\n\n\n\n
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
interface Shape {\r\n\tdouble getArea();\r\n}<\/code><\/pre>\n\n\n\n
2. Create Concrete Shape classes<\/h4>\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
AreaCalculator <\/code>class, it will be automatically supported.<\/li>\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 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
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
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
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
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 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
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
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
\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 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
\n
HighLevelModule <\/code>is tightly coupled to
LowLevelModule<\/code>, making it difficult to change or test independently.<\/li>\n\n\n\n
LowLevelModule <\/code>implementation would require code changes in
HighLevelModule<\/code>.<\/li>\n<\/ul>\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
\n
HighLevelModule <\/code>depends on the
Abstraction <\/code>interface, not the concrete
LowLevelModule<\/code>.<\/li>\n\n\n\n
HighLevelModule<\/code>.<\/li>\n<\/ul>\n\n\n\n
Bibliography<\/h2>\n\n\n\n