Want to Contribute to us or want to have 15k+ Audience read your Article ? Or Just want to make a strong Backlink?

SOLIDify Your Foundation: Mastering Software Design with a Deep Dive into SOLID Principles

On the earth of software program growth, writing maintainable and extensible code is essential. One solution to obtain that is by following the SOLID rules, a set of 5 design rules that make it easier to create code that’s extra strong, simpler to grasp, and fewer vulnerable to bugs. On this weblog publish, we’ll discover every of the SOLID rules and supply code examples in Java to show tips on how to apply them successfully.



– Single Accountability Precept (SRP)

The Single Accountability Precept states {that a} class ought to have just one cause to alter, that means it ought to have just one accountability. If a category has a number of obligations, adjustments in a single space could inadvertently have an effect on different areas of the code. Let’s take a look at an instance:

public class Merchandise {
    personal String description;
    personal double value;
}

public class Order {
    personal int orderId;
    personal int amount;
    personal Merchandise merchandise;

    public Order(int orderId, Merchandise merchandise, int amount) {
        this.orderId = orderId;
        this.merchandise = merchandise;
        this.amount = amount;
    }

    public double calculateTotal() {
        // Calculate the order complete
        double orderTotal = merchandise.value * this.amount;
        return orderTotal;
    }

    public void saveToDatabase() {
        // Save the order to the database
    }
}
Enter fullscreen mode

Exit fullscreen mode

On this instance, the Order class is chargeable for two distinct duties: calculating the order complete and saving it to the database. This violates the SRP as a result of the category has a number of causes to alter. This may result in a number of issues akin to

  • Code Upkeep: If there are adjustments within the order calculation logic or the database operations, it may possibly inadvertently have an effect on the opposite a part of the category. This design can result in code that’s tough to keep up, take a look at, and perceive.

  • Reusability: The order calculation and database operation logic are tightly coupled throughout the class, making it difficult to reuse both of those functionalities independently.

To stick to SRP, we will separate these obligations into two completely different courses:

public class Merchandise {
    personal String description;
    personal double value;
}

public class Order {
    personal int orderId;
    personal int amount;
    personal Merchandise merchandise;

    public Order(int orderId, Merchandise merchandise, int amount) {
        this.orderId = orderId;
        this.merchandise = merchandise;
        this.amount = amount;
    }

    public double calculateTotal() {
        // Calculate the order complete
        double orderTotal = merchandise.value * this.amount;
        return orderTotal;
    }
}

public class OrderRepository {
    public void saveToDatabase(Order order) {
        // Save the order to the database
    }
}
Enter fullscreen mode

Exit fullscreen mode

Now, the Order class is accountable just for order-related calculations, adhering to the SRP. The OrderRepository class is chargeable for database operations, akin to saving and retrieving orders. This separation of issues not solely makes the code simpler to keep up and perceive but in addition permits for unbiased testing and modification of every class with out affecting the opposite.



– Open/Closed Precept (OCP)

The Open/Closed Precept means that software program entities (courses, modules, capabilities) must be open for extension however closed for modification. In different phrases, you must be capable of add new performance to your code with out altering current code. This precept encourages the usage of inheritance and polymorphism to realize extensibility.

Let’s illustrate the Open/Closed Precept with an instance involving geometric shapes in Java:

Think about a situation the place you are constructing a system to calculate and show the areas of varied geometric shapes. Initially, you’ve got a ShapeCalculator class that calculates the realm of various shapes. Nevertheless, as new shapes are launched, the prevailing code should stay untouched.

public class ShapeCalculator {
    public double calculateArea(Form form) {
        if (form instanceof Circle) {
            Circle circle = (Circle) form;
            return Math.PI * Math.pow(circle.getRadius(), 2);
        } else if (form instanceof Rectangle) {
            Rectangle rectangle = (Rectangle) form;
            return rectangle.getWidth() * rectangle.getHeight();
        }
        // Add extra shapes and calculations...
        return 0.0;
    }
}
Enter fullscreen mode

Exit fullscreen mode

On this non-OCP instance, so as to add a brand new form, you would wish to switch the ShapeCalculator class, violating the Open/Closed Precept.

Now, let’s apply the Open/Closed Precept to create an extensible design utilizing inheritance and polymorphism:

interface Form {
    public double calculateArea();
}

class Circle implements Form {
    personal double radius;

    public Circle(double radius) {
        this.radius = radius;
    }

    @Override
    public double calculateArea() {
        return Math.PI * Math.pow(radius, 2);
    }
}

class Rectangle implements Form {
    personal double width;
    personal double top;

    public Rectangle(double width, double top) {
        this.width = width;
        this.top = top;
    }

    @Override
    public double calculateArea() {
        return width * top;
    }
}

public class ShapeCalculator {
    public double calculateArea(Form form) {
        return form.calculateArea();
    }
}
Enter fullscreen mode

Exit fullscreen mode

Now, you possibly can simply add new form sorts(e.g., Triangle, Sq.) with out modifying the ShapeCalculator class.



– Liskov Substitution Precept (LSP)

The Liskov Substitution Precept (LSP) is among the SOLID rules of object-oriented design, and it emphasizes that objects of derived courses ought to be capable of change objects of the bottom class with out affecting the correctness of this system. In easier phrases, when you have a base class and derived courses, the derived courses ought to be capable of lengthen the bottom class(and never slender it down) with out altering the anticipated behaviour of this system.

Let’s illustrate LSP utilizing a “Car” instance:

public class Car {
    public String getEngineName() {
        return "Car Engine";
    }

    public int getNumberOfWheels() {
        return 2;
    }
}
Enter fullscreen mode

Exit fullscreen mode

Now, let’s create the “Automobile” class as a subclass of “Car” to signify vehicles with engines and 4 wheels and let’s additionally create a “Bicycle” class that extends the “Car” class:

public class Automobile extends Car {
    @Override
    public String getEngineName() {
        return "Automobile Engine";
    }

    @Override
    public int getNumberOfWheels() {
        return 4;
    }
}

public class Bicycle extends Car {
    // Bicycle-specific properties and strategies

    @Override
    public int getNumberOfWheels() {
        return 2;
    }
}
Enter fullscreen mode

Exit fullscreen mode

The issue right here is {that a} bicycle would not have an engine, so the “getEngineName” technique would not make sense for a bicycle (We are able to both select to implement its personal getEngineName perform and return null or don’t implement it in any respect and simply inherit the generic getEngineName technique). If you happen to create an occasion of the “Bicycle” class and attempt to name “getEngineName”, it may result in confusion or surprising behaviour in both case as a result of bicycles haven’t got engines:

Violation of Liskov Substitution Precept:

public class Bicycle extends Car {
    // Bicycle-specific properties and strategies

    @Override
    public String getEngineName() {
        return null;
    }

    @Override
    public int getNumberOfWheels() {
        return 2;
    }
}

public class Most important{
    public static void predominant(String[] args) {
        Listing<Car> vehicleList = new ArrayList<>();
        vehicleList.add(new Car());
        vehicleList.add(new Automobile());
        vehicleList.add(new Bicycle());

        for(Car automobile : vehicleList){
            System.out.println(automobile.getEngineName());// This isn't applicable for a bicycle
        }
    }
}
Enter fullscreen mode

Exit fullscreen mode

Decision – Making use of Liskov Substitution Precept:

To stick to the Liskov Substitution Precept, you must modify the category hierarchy. A technique to do that is by introducing an intermediate class, let’s name it “EngineVehicle,” which incorporates the “engineName” technique:

public class Car {
    public int getNumberOfWheels() {
        return 2;
    }
}

public class EngineVehicle extends Car {
    public String getEngineName() {
        return "Car Engine";
    }
}

public class Automobile extends EngineVehicle {
    @Override
    public String getEngineName() {
        return "Automobile Engine";
    }

    @Override
    public int getNumberOfWheels() {
        return 4;
    }
}

public class Bicycle extends Car {
    @Override
    public int getNumberOfWheels() {
        return 2;
    }
}
Enter fullscreen mode

Exit fullscreen mode

Now, the “Car” class stays a base class with out realizing something in regards to the engine, and the “EngineVehicle” class handles the engine-related strategies.

By restructuring the category hierarchy on this approach, you make sure that the Liskov Substitution Precept is just not violated. Now, you need to use each “Car” and “Bicycle” objects interchangeably with out surprising behaviour:

public class Most important{
    public static void predominant(String[] args) {
        Listing<Car> vehicleList = new ArrayList<>();
        vehicleList.add(new Car());
        vehicleList.add(new Automobile());
        vehicleList.add(new Bicycle());

        for(Car automobile : vehicleList){
            System.out.println(automobile.getNumberOfWheels());// This may work effective;
        }

        Listing<EngineVehicle> engineVehicleList = new ArrayList<>();
        engineVehicleList.add(new EngineVehicle());
        engineVehicleList.add(new Automobile());

        // and that is how we will print engine identify
        for(EngineVehicle engineVehicle : engineVehicleList){
            System.out.println(engineVehicle.getEngineName();
        }
    }
Enter fullscreen mode

Exit fullscreen mode



– Interface Segregation Precept (ISP)

The Interface Segregation Precept means that shoppers shouldn’t be compelled to rely on interfaces they do not use. In different phrases, it is higher to have a number of small, particular interfaces than one giant, normal interface. For example the ISP, let’s contemplate a restaurant worker situation involving waiters and cooks.

Think about you are designing a software program system for a restaurant administration software, and also you wish to create interfaces for restaurant staff. In a poorly designed system with out contemplating ISP, you may need a single monolithic interface that comprises strategies for all doable duties staff may carry out, akin to serving clients, cooking meals, cleansing tables, and managing reservations:

public interface RestaurantEmployee {
    void takeOrder();
    void serveFood();
    void prepareFood();
    void handleCustomerPayment();
    void manageKitchen();
}
Enter fullscreen mode

Exit fullscreen mode

On this situation, you are violating the ISP as a result of staff, akin to cooks, shouldn’t be compelled to implement strategies like takeOrder or handleCustomerPayment which might be irrelevant to their position. The identical applies to waiters, who shouldn’t need to implement strategies like prepareFood or manageKitchen.

To stick to the ISP, you must break down this monolithic interface into smaller, extra particular interfaces that every signify a specific position or accountability. Let’s create separate interfaces for waiters and cooks:

// Interface for Waiters
public interface Waiter {
    void takeOrder();
    void serveFood();
    void handleCustomerPayment();
}

// Interface for Cooks
public interface Prepare dinner {
    void prepareFood();
    void manageKitchen();
}
Enter fullscreen mode

Exit fullscreen mode

Now, the Waiter interface comprises strategies related to the obligations of waiters, whereas the Prepare dinner interface comprises strategies related to the obligations of cooks.

With this design, staff can implement solely the interfaces that match their particular roles. Waiters will implement the Waiter interface, and cooks will implement the Prepare dinner interface.

This segregation ensures that every class is chargeable for a targeted set of duties, making the codebase extra maintainable and fewer error-prone. It additionally permits you to add new roles or obligations to your restaurant administration system with out affecting current courses that need not implement them.



– Dependency Inversion Precept (DIP)

The Dependency Inversion Precept (DIP) is among the SOLID rules of object-oriented design. It states that high-level modules shouldn’t rely on low-level modules; each ought to rely on abstractions. In different phrases, it encourages the usage of interfaces or summary courses to outline abstractions, permitting for flexibility and decoupling in your code.

In keeping with DIP, our courses ought to rely on interfaces or summary courses as an alternative of concrete courses and capabilities.

Let’s clarify the Dependency Inversion Precept utilizing an instance of a keyboard:

Think about a typical laptop system with a keyboard. In a system that does not observe the DIP, the high-level module (software) would possibly straight rely on the low-level module (keyboard). This direct dependency can result in inflexibility and issues whenever you wish to change or improve the keyboard.

Non-DIP Instance:

public class Software {
    personal Keyboard keyboard;

    // Direct dependency on Keyboard class.
    public Software(Keyboard keyboard) {
        this.keyboard = keyboard; 
    }

    public void typeMessage() {
        keyboard.sort(); // The applying straight makes use of the concrete keyboard class.
    }
}

public class Most important{
    public static void predominant(String[] args) {
        Software software = new Software(new Keyboard()); //Immediately instantiates the Keyboard class.

    }
}
Enter fullscreen mode

Exit fullscreen mode

On this non-DIP instance, the Software class relies upon straight on the Keyboard class, which represents a particular sort of keyboard. If you happen to ever wish to change the keyboard or use a unique enter gadget, you’d want to switch the Software class, violating the Open/Closed Precept (OCP).

Now, let’s apply the Dependency Inversion Precept:

DIP Instance:

public interface InputDevice {
    void sort();
}

public class Keyboard implements InputDevice {
    public void sort() {
        // Typing logic particular to the keyboard
    }
}

public class Software {
    personal InputDevice inputDevice;

    public Software(InputDevice inputDevice) {
        this.inputDevice = inputDevice; // Accepts any enter gadget that implements the InputDevice interface.
    }

    public void typeMessage() {
        inputDevice.sort(); // The applying makes use of the InputDevice interface, permitting flexibility.
    }
}

public class Most important{
    public static void predominant(String[] args) {
        Software software = new Software(new Keyboard());

        Software software = new Software(new Mouse()); //In future we will cross any enter gadget that implements the InputDevice interface with out altering a single line within the Software class.
    }
}
Enter fullscreen mode

Exit fullscreen mode

On this DIP-compliant instance, we have launched the InputDevice interface, which serves as an abstraction for any enter gadget, akin to a keyboard or a unique enter gadget. The Keyboard class implements this interface.

The Software class now accepts an occasion of an InputDevice by its constructor. This implies you possibly can simply swap out the concrete enter gadget with out modifying the Software class, adhering to the Open/Closed Precept (OCP) and offering flexibility. This separation of issues and abstraction of dependencies makes the code extra maintainable and extensible, aligning with the Dependency Inversion Precept (DIP).



Conclusion

SOLID rules are a cornerstone of writing clear, maintainable, and adaptable code. They supply a roadmap for creating software program that may face up to the take a look at of time, accommodate new options and necessities, and decrease the introduction of bugs throughout growth and upkeep.

By internalizing these rules and making use of them persistently in your software program growth practices, you possibly can construct unshakable code that serves as a basis for strong, long-lasting software program techniques. Embracing SOLID rules isn’t just a finest observe however a dedication to high quality and professionalism within the area of software program growth.

Add a Comment

Your email address will not be published. Required fields are marked *

Want to Contribute to us or want to have 15k+ Audience read your Article ? Or Just want to make a strong Backlink?