Skip to content

Instantly share code, notes, and snippets.

@LabN36
Created February 13, 2025 06:31
Show Gist options
  • Save LabN36/3b923807ba609a8c80e96c57be1d5f8e to your computer and use it in GitHub Desktop.
Save LabN36/3b923807ba609a8c80e96c57be1d5f8e to your computer and use it in GitHub Desktop.
SOLID Principle with example

Okay, here are short Java examples illustrating each of the SOLID principles:

1. Single Responsibility Principle (SRP)

  • Bad (violates SRP):

    class User {
        private String username;
        private String password;
    
        public void createUser(String username, String password) {
            // Create user in database
        }
    
        public void sendEmail(String email, String message) {
            // Send email
        }
    }
  • Good (follows SRP):

    class User {
        private String username;
        private String password;
    
        public void createUser(String username, String password) {
            // Store data for the user
        }
    }
    
    class UserDatabase {
       public void save(User user){
         // Save the user in db
       }
    }
    
    class EmailService {
        public void sendEmail(String email, String message) {
            // Send email
        }
    }

    Explanation: The bad example has the User class responsible for both user creation and sending emails. The good example separates these concerns into User, UserDatabase and EmailService classes, each with a single, well-defined responsibility.

2. Open/Closed Principle (OCP)

  • Bad (violates OCP):

    class Rectangle {
        public double width;
        public double height;
    }
    
    class Circle {
        public double radius;
    }
    
    class AreaCalculator {
        public double calculateArea(Object shape) {
            if (shape instanceof Rectangle) {
                Rectangle rectangle = (Rectangle) shape;
                return rectangle.width * rectangle.height;
            } else if (shape instanceof Circle) {
                Circle circle = (Circle) shape;
                return Math.PI * circle.radius * circle.radius;
            }
            return 0;
        }
    }
  • Good (follows OCP):

    interface Shape {
        double calculateArea();
    }
    
    class Rectangle implements Shape {
        public double width;
        public double height;
    
        @Override
        public double calculateArea() {
            return width * height;
        }
    }
    
    class Circle implements Shape {
        public double radius;
    
        @Override
        public double calculateArea() {
            return Math.PI * radius * radius;
        }
    }
    
    class AreaCalculator {
        public double calculateArea(Shape shape) {
            return shape.calculateArea();
        }
    }

    Explanation: The bad example requires modification of AreaCalculator every time a new shape is added. The good example uses an Shape interface. New shapes can be added without modifying AreaCalculator. The AreaCalculator is now open for extension (adding new shapes) but closed for modification (no need to change its code).

3. Liskov Substitution Principle (LSP)

  • Bad (violates LSP):

    class Bird {
        public void fly() {
            System.out.println("Bird is flying");
        }
    }
    
    class Ostrich extends Bird {
        @Override
        public void fly() {
            throw new UnsupportedOperationException("Ostriches can't fly");
        }
    }
    
    //Client code
    public class Main {
      public static void main(String[] args) {
        Bird bird = new Ostrich();
        bird.fly(); // This throws an exception, breaking LSP
      }
    }
  • Good (follows LSP):

    class Bird {
       public void makeSound(){
          System.out.println("Generic bird sound");
       }
    }
    
    interface FlyingBird {
        void fly();
    }
    
    class Sparrow extends Bird implements FlyingBird {
        @Override
        public void fly() {
            System.out.println("Sparrow is flying");
        }
    
        @Override
        public void makeSound() {
            System.out.println("Chirp!");
        }
    }
    
    class Ostrich extends Bird {
        @Override
        public void makeSound() {
            System.out.println("Ostrich sound");
        }
        // Ostriches don't fly, so they don't implement FlyingBird
    }
    
    //Client code
    public class Main {
      public static void main(String[] args) {
        Bird bird = new Ostrich();
        bird.makeSound(); // This works fine, LSP is not violated
      }
    }

    Explanation: In the bad example, Ostrich cannot meaningfully implement the fly() method without throwing an exception. This violates LSP because Ostrich cannot be substituted for Bird in all contexts. The good example fixes this by separating the ability to fly into a different interface. Now, Ostrich extends Bird but does not implement FlyingBird. Client code that expects a Bird won't be surprised if it gets an Ostrich.

4. Interface Segregation Principle (ISP)

  • Bad (violates ISP):

    interface Worker {
        void work();
        void eat();
    }
    
    class Human implements Worker {
        @Override
        public void work() {
            System.out.println("Human working");
        }
    
        @Override
        public void eat() {
            System.out.println("Human eating");
        }
    }
    
    class Robot implements Worker {
        @Override
        public void work() {
            System.out.println("Robot working");
        }
    
        @Override
        public void eat() {
            // Robots don't eat, but we have to implement this method
            throw new UnsupportedOperationException("Robots don't eat");
        }
    }
  • Good (follows ISP):

    interface Workable {
        void work();
    }
    
    interface Eatable {
        void eat();
    }
    
    class Human implements Workable, Eatable {
        @Override
        public void work() {
            System.out.println("Human working");
        }
    
        @Override
        public void eat() {
            System.out.println("Human eating");
        }
    }
    
    class Robot implements Workable {
        @Override
        public void work() {
            System.out.println("Robot working");
        }
        // Robots don't need to implement Eatable
    }

    Explanation: The bad example forces Robot to implement eat(), even though it doesn't need it. The good example splits the Worker interface into Workable and Eatable interfaces. Now, Robot only implements Workable. Clients should not be forced to depend on methods they don't use.

5. Dependency Inversion Principle (DIP)

  • Bad (violates DIP):

    class LightBulb {
        public void turnOn() {
            System.out.println("LightBulb: Bulb turned on");
        }
    
        public void turnOff() {
            System.out.println("LightBulb: Bulb turned off");
        }
    }
    
    class Switch {
        private LightBulb bulb;
    
        public Switch() {
            this.bulb = new LightBulb(); // High-level module depends on a low-level module
        }
    
        public void operate() {
            bulb.turnOn();
        }
    }
  • Good (follows DIP):

    interface Switchable {
        void turnOn();
        void turnOff();
    }
    
    class LightBulb implements Switchable {
        @Override
        public void turnOn() {
            System.out.println("LightBulb: Bulb turned on");
        }
    
        @Override
        public void turnOff() {
            System.out.println("LightBulb: Bulb turned off");
        }
    }
    
     class Fan implements Switchable {
        @Override
        public void turnOn() {
            System.out.println("Fan: Fan turned on");
        }
    
        @Override
        public void turnOff() {
            System.out.println("Fan: Fan turned off");
        }
    }
    
    class Switch {
        private Switchable device;
    
        public Switch(Switchable device) {
            this.device = device; // High-level module depends on an abstraction
        }
    
        public void operate() {
            device.turnOn();
        }
    }
    
    //Usage
    public class Main {
      public static void main(String[] args) {
        LightBulb bulb = new LightBulb();
        Switch mySwitch = new Switch(bulb);
        mySwitch.operate();
    
        Fan fan = new Fan();
        Switch fanSwitch = new Switch(fan);
        fanSwitch.operate();
      }
    }

    Explanation: In the bad example, Switch directly depends on the LightBulb class. This is tight coupling. The good example uses an Switchable interface. Now, Switch depends on the Switchable abstraction rather than a concrete LightBulb class. This makes the system more flexible and testable. High-level modules (like Switch) should not depend on low-level modules (like LightBulb); both should depend on abstractions. These examples are simplified for demonstration purposes. Real-world applications often involve more complex scenarios. Remember that SOLID principles are guidelines, not strict rules. The goal is to create maintainable, flexible, and robust software.

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