Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save psenger/840a50ceecfa7725ef468b21ccc1660b to your computer and use it in GitHub Desktop.

Select an option

Save psenger/840a50ceecfa7725ef468b21ccc1660b to your computer and use it in GitHub Desktop.
[Java principles - SOLID and Composition ] #Java

SOLID

The SOLID principle is a set of five object-oriented design principles that aim to make software systems more maintainable, scalable, and extensible. The SOLID acronym stands for:

  1. Single Responsibility Principle (SRP): Each class should have a single responsibility, meaning it should only have one reason to change. This principle helps ensure that each class has a clear and specific purpose, making it easier to maintain, test, and reuse.
  2. Open/Closed Principle (OCP): Software entities (classes, modules, functions, etc.) should be open for extension but closed for modification. This means that you should be able to add new functionality to a system without modifying existing code, which helps prevent unintended side effects and makes the system more flexible and reusable.
  3. Liskov Substitution Principle (LSP): Subtypes should be substitutable for their base types. This means that any object of a base class should be able to be replaced by any object of a derived class without affecting the correctness of the program. This principle helps ensure that classes are properly designed and avoid unexpected behaviors or bugs.
  4. Interface Segregation Principle (ISP): Clients should not be forced to depend on interfaces they don't use. This means that you should design interfaces that are specific to the needs of the clients that use them, which helps prevent unnecessary dependencies and makes the system more modular and scalable.
  5. Dependency Inversion Principle (DIP): High-level modules should not depend on low-level modules. Instead, both should depend on abstractions. This means that you should design your system so that each module or component depends on an abstraction (an interface or abstract class) rather than a concrete implementation. This principle helps decouple the different parts of a system and makes it easier to change or replace components without affecting the rest of the system.

By following the SOLID principles, you can create software that is more maintainable, scalable, and extensible. These principles help you create code that is easier to read, test, and modify, and that can adapt to changing requirements or business needs.

The Dependency Inversion Principle (DIP) is one of the SOLID principles that states that high-level modules should not depend on low-level modules, but both should depend on abstractions. Abstractions should not depend on details, but details should depend on abstractions.

Here's an example of how the DIP can be applied in Java:

// Abstraction
interface SwitchableDevice {
    void turnOn();
    void turnOff();
}

// Low-level module
class LightBulb implements SwitchableDevice {
    @Override
    public void turnOn() {
        System.out.println("Light bulb turned on");
    }

    @Override
    public void turnOff() {
        System.out.println("Light bulb turned off");
    }
}

// High-level module
class Switch {
    private SwitchableDevice device;
    
    public void setDevice(SwitchableDevice device) {
        this.device = device;
    }
    
    public void turnOn() {
        device.turnOn();
    }
    
    public void turnOff() {
        device.turnOff();
    }
}

public class Main {
    public static void main(String[] args) {
        Switch lightSwitch = new Switch();
        lightSwitch.setDevice(new LightBulb());
        lightSwitch.turnOn();
        lightSwitch.turnOff();
    }
}

In this example, we have an abstraction called SwitchableDevice that defines two methods: turnOn and turnOff. We also have a low-level module called LightBulb that implements this interface.

The high-level module is represented by the Switch class, which depends on the SwitchableDevice abstraction rather than the LightBulb class directly. This means that the Switch class can work with any device that implements the SwitchableDevice interface, not just LightBulb.

To use the Switch class, we simply create an instance of it and pass in a SwitchableDevice object. In this case, we're passing in a LightBulb object, but we could also pass in a different device that implements the SwitchableDevice interface.

By using the DIP, we've made our code more flexible and easier to maintain, because the high-level module is not tightly coupled to the low-level module, and both depend on abstractions rather than concrete implementations. This makes it easier to change the implementation of the low-level module without affecting the high-level module, and also allows us to reuse the high-level module with different low-level modules that implement the same abstraction.

The Interface Segregation Principle (ISP) is one of the SOLID principles that states that clients should not be forced to depend on methods or interfaces they do not use. In other words, it's better to have smaller, more specific interfaces than larger, generic ones.

Here's an example of how the ISP can be applied in Java:

// Interface for a machine that can print, scan and fax
interface MultiFunctionMachine {
    void print();
    void scan();
    void fax();
}

// A basic printer that can only print
class Printer implements MultiFunctionMachine {
    @Override
    public void print() {
        System.out.println("Printing...");
    }
    
    // These methods are not applicable to Printer, but we must implement them anyway
    @Override
    public void scan() {
        throw new UnsupportedOperationException();
    }
    
    @Override
    public void fax() {
        throw new UnsupportedOperationException();
    }
}

// A multifunction machine that can print, scan and fax
class MultiFunctionDevice implements MultiFunctionMachine {
    @Override
    public void print() {
        System.out.println("Printing...");
    }
    
    @Override
    public void scan() {
        System.out.println("Scanning...");
    }
    
    @Override
    public void fax() {
        System.out.println("Faxing...");
    }
}

public class Main {
    public static void main(String[] args) {
        MultiFunctionMachine machine = new MultiFunctionDevice();
        machine.print();
        machine.scan();
        machine.fax();
        
        // We can also use a Printer object as a MultiFunctionMachine, even though it doesn't support scan and fax
        machine = new Printer();
        machine.print();
    }
}

In this example, we have an interface called MultiFunctionMachine that defines three methods: print, scan, and fax. We also have two classes that implement this interface: Printer and MultiFunctionDevice.

The problem is that the Printer class doesn't support scanning and faxing, but it still has to implement these methods because it's required by the MultiFunctionMachine interface. This means that we're violating the ISP, because clients of the Printer class are forced to depend on methods that are not applicable to it.

To fix this, we can create separate interfaces for printing, scanning and faxing, and have the Printer class only implement the printing interface:

interface PrinterMachine {
    void print();
}

interface ScannerMachine {
    void scan();
}

interface FaxMachine {
    void fax();
}

class Printer implements PrinterMachine {
    @Override
    public void print() {
        System.out.println("Printing...");
    }
}

class MultiFunctionDevice implements PrinterMachine, ScannerMachine, FaxMachine {
    @Override
    public void print() {
        System.out.println("Printing...");
    }
    
    @Override
    public void scan() {
        System.out.println("Scanning...");
    }
    
    @Override
    public void fax() {
        System.out.println("Faxing...");
    }
}

public class Main {
    public static void main(String[] args) {
        PrinterMachine printer = new Printer();
        printer.print();
        
        ScannerMachine scanner = new MultiFunctionDevice();
        scanner.scan();
        
        FaxMachine fax = new MultiFunctionDevice();
        fax.fax();
    }
}

Now, we have separate interfaces for printing, scanning, and faxing, and each class only implements the methods that are applicable to it. This makes the code more modular and easier to maintain, and clients of each class are only dependent on the methods they actually use.

The Liskov Substitution Principle (LSP) is a fundamental principle of object-oriented programming, which states that subtypes should be substitutable for their base types. In simpler terms, if class B is a subclass of class A, then objects of class B should be able to replace objects of class A without affecting the correctness of the program.

Here's an example of LSP violation in Java:

class Rectangle {
    protected int width, height;
    
    public Rectangle(int width, int height) {
        this.width = width;
        this.height = height;
    }
    
    public int getWidth() {
        return width;
    }
    
    public void setWidth(int width) {
        this.width = width;
    }
    
    public int getHeight() {
        return height;
    }
    
    public void setHeight(int height) {
        this.height = height;
    }
    
    public int getArea() {
        return width * height;
    }
}

class Square extends Rectangle {
    public Square(int size) {
        super(size, size);
    }
    
    @Override
    public void setWidth(int width) {
        super.setWidth(width);
        super.setHeight(width);
    }
    
    @Override
    public void setHeight(int height) {
        super.setWidth(height);
        super.setHeight(height);
    }
}

public class Main {
    public static void main(String[] args) {
        Rectangle rectangle = new Rectangle(3, 4);
        System.out.println(rectangle.getArea()); // Output: 12
        
        Rectangle square = new Square(3);
        square.setWidth(4);
        System.out.println(square.getArea()); // Output: 16
    }
}

In this example, we have a Rectangle class and a Square class that extends Rectangle. The problem is that we violate the LSP, because the setWidth and setHeight methods in the Square class behave differently than in the Rectangle class. In the Rectangle class, changing the width or height only affects that specific dimension, but in the Square class, changing the width or height affects both dimensions, because a square must have equal width and height.

This means that if we create a Square object and assign it to a Rectangle variable, we can call the setWidth method with a value that violates the Square class's invariant (i.e., that width and height must always be equal). This results in a Square object that is no longer a square, violating the LSP.

To fix this, we can either remove the Square class and use only the Rectangle class, or we can change the design of the Square class so that it does not violate the LSP. For example, we can make Square a separate class that does not inherit from Rectangle, or we can make both classes implement a common interface and define the setWidth and setHeight methods differently for each class.

The Open/Closed Principle (OCP) is one of the SOLID principles that states that software entities (classes, modules, functions, etc.) should be open for extension but closed for modification. This means that we should be able to add new functionality to a system without changing its existing code.

Here's an example of how the OCP can be applied in Java:

// Abstract class representing a shape
abstract class Shape {
    abstract double area();
}

// Concrete class representing a rectangle
class Rectangle extends Shape {
    private double width;
    private double height;
    
    public Rectangle(double width, double height) {
        this.width = width;
        this.height = height;
    }
    
    @Override
    double area() {
        return width * height;
    }
}

// Concrete class representing a circle
class Circle extends Shape {
    private double radius;
    
    public Circle(double radius) {
        this.radius = radius;
    }
    
    @Override
    double area() {
        return Math.PI * radius * radius;
    }
}

// Class that calculates the sum of areas of multiple shapes
class AreaCalculator {
    public double calculateArea(Shape[] shapes) {
        double sum = 0;
        for (Shape shape : shapes) {
            sum += shape.area();
        }
        return sum;
    }
}

public class Main {
    public static void main(String[] args) {
        Shape[] shapes = { new Rectangle(5, 10), new Circle(3) };
        AreaCalculator calculator = new AreaCalculator();
        double totalArea = calculator.calculateArea(shapes);
        System.out.println("Total area: " + totalArea);
    }
}

In this example, we have an abstract class called Shape that represents a shape with an area() method. We also have two concrete classes, Rectangle and Circle, that extend Shape and implement their own area() methods.

The AreaCalculator class takes an array of Shape objects and calculates the sum of their areas using a for loop. Notice that we don't need to modify the AreaCalculator class when we add new shapes that extend the Shape class.

To use the AreaCalculator class, we simply create an array of Shape objects and pass it to the calculateArea method. In this case, we're passing in an array with a Rectangle and a Circle object.

By using the OCP, we've made our code more flexible and easier to maintain, because we can add new shapes to the system without modifying the AreaCalculator class. This allows us to add new functionality to the system without breaking existing code, which is important for large and complex software systems.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment