What    is SOLID
 SOLID is a structured design approach that ensures your
  software is modular and easy to maintain, understand,
  debug, and refactor.
 Following SOLID also helps save time and effort in both
  development and maintenance. SOLID prevents your code
  from becoming rigid and fragile, which helps you build
  long-lasting software.
 When it comes to designing flexible, scalable,
  maintainable, and reusable code in software development,
  Object-Oriented Design is critical.
               The SOLID Principles
 S — Single Responsibility
 A class should have a single responsibility
                         Goal
 Thisprinciple aims to separate behaviours so that if
 bugs arise as a result of your change, it won’t affect
 other unrelated behaviours.
                IMPLIMENTATION :
 Example:   Think of a mobile phone. A mobile phone
 has multiple components like a camera, a touchscreen, a
 speaker, etc. Each of these components has its own
 distinct function. Applying SRP in code, you would
 create separate classes for each component rather than
 having a single class that handles all functionalities.
   // Before SRP
    class MobilePhone {
    void makeCall() { /* ... */ }
    void takePhoto() { /* ... */ }
    void playMusic() { /* ... */ }
    }
    // After SRP
    class Call {
    void makeCall() { /* ... */ }
    }
    class Camera {
    void takePhoto() { /* ... */ }
    }
    class MusicPlayer {
    void playMusic() { /* ... */ }
    }
 O — Open-Closed
 Classes should be open for extension, but closed for
  modification
                            Goal
 This principle aims to extend a Class’s behaviour without
  changing the existing behaviour of that Class. This is to
  avoid causing bugs wherever the Class is being used.
                        Example
    Consider a geometric shapes library. Instead of modifying
    the existing shape classes when adding new shapes, you
    could create a common interface for all shapes and extend
    that interface to create new shapes. This way, existing
    code remains unchanged while new shapes can be easily
    added.
 //   Before OCP
class Circle {
double radius;
double area() {
return Math.PI * radius * radius;
}
}
   // After OCP
interface Shape {
double area();
}
class Circle implements Shape {
double radius;
@Override
public double area() {
return Math.PI * radius * radius;
}
}
class Rectangle implements Shape {
double width;
double height;
@Override
public double area() {
return width * height;
}
}
 L — Liskov Substitution
 If S is a subtype of T, then objects of type T in a program
  may be replaced with objects of type S without altering
  any of the desirable properties of that program.
   When a child Class cannot perform the same actions as
    its parent Class, this can cause bugs.
                               Goal
 This principle aims to enforce consistency so that the parent
  Class or its child Class can be used in the same way without
  any errors.
                           Example
 Imagine a scenario where you have a base class “Bird” and
derived classes “Sparrow” and “Ostrich”. If you follow LSP,
you should be able to use any instance of a derived class (e.g.,
“Sparrow”) wherever an instance of the base class (“Bird”) is
expected, without causing unexpected behavior.
   class Bird {
    void fly() { /* ... */ }
    }
    class Sparrow extends Bird {
    @Override
    void fly() { /* ... */ }
    }
    class Ostrich extends Bird {
    // This class doesn't override fly()
    }
 I — Interface Segregation
 Clients should not be forced to depend on methods that they
  do not use.
   When a Class is required to perform actions that are not
    useful, it is wasteful and may produce unexpected bugs if
    the Class does not have the ability to perform those actions.
                               Goal
   This principle aims at splitting a set of actions into smaller
    sets so that a Class executes ONLY the set of actions it
    requires.
                             Example
    In a messaging application, you might have interfaces for
    sending, receiving, and displaying messages. Instead of
    having a single large interface for all messaging operations,
    you would split it into smaller interfaces so that classes only
    need to implement the methods relevant to them.
   // Before ISP
    interface Messaging {
    void send();
    void receive();
    void display();
    }
    // After ISP
    interface Sender {
    void send();
    }
    interface Receiver {
    void receive();
    }
    interface Displayable {
    void display();
 D — Dependency Inversion
 - High-level modules should not depend on low-level
  modules. Both should depend on the abstraction.
 - Abstractions should not depend on details. Details should
  depend on abstractions.
   Goal
   This principle aims at reducing the dependency of a high-level
    Class on the low-level Class by introducing an interface.
    // Before DIP
    class MusicPlayer {
    void playMP3() { /* ... */ }
    }
    class AudioApp {
    private MusicPlayer musicPlayer = new MusicPlayer();
    void playAudio() {
    musicPlayer.playMP3();
    }
    }
   // After DIP
interface AudioPlayer {
void play();
}
class MP3Player implements AudioPlayer {
@Override
public void play() { /* ... */ }
}
class AudioApp {
private AudioPlayer audioPlayer;
AudioApp(AudioPlayer audioPlayer) {
this.audioPlayer = audioPlayer;
}
void playAudio() {
audioPlayer.play();
}
}
            SOLID     principles | Advantages
 Increased maintainability: By adhering to the SOLID
  principles, code is written in a way that is more easily
  understood and less prone to errors. This makes it easier to
  change and update the code over time without introducing
  bugs.
 Improved readability: The SOLID principles help to create
  code that is more organized and easier to read. This makes it
  easier for developers to understand the codebase and make
  changes to it.
 Increased scalability: The SOLID principles help to create
  code that is more flexible and extensible. This makes it
  easier to add new features and functionality to the codebase
  without introducing bugs or causing issues with existing
  code.
 Better testability: The SOLID principles help to create
  code that is more testable, which allows for more thorough
  testing and fewer bugs.
 Reduced coupling: The SOLID principles help to create
  code that is less tightly coupled, which makes it less likely
  that changes to one part of the system will affect other
  parts.
 Enhanced reusability: SOLID principles make the code
  more reusable by making the code more modular, which
  makes it easier to reuse parts of the codebase in other
  projects.
 Better performance: By following SOLID principles, the
  codebase is optimized for performance and scalability
  which results in faster and more efficient software.
               SOLID principles | Disadvantages
 Increased complexity: Adhering to the SOLID principles
  can lead to a more complex codebase, as it requires splitting
  code into smaller, more focused classes and interfaces.
 More classes and interfaces: Following SOLID principles
  can lead to creating more classes and interfaces, which can
  make the codebase harder to navigate.
 Over-engineering: It's possible to apply SOLID principles
  too strictly, which can lead to over-engineering and
  unnecessary complexity.
 Difficulty in understanding: SOLID principles are not easy
  to understand and implement, especially for junior
  developers or developers who are new to software
  development.
 Additional time and effort required: Adhering to SOLID
  principles requires extra time and effort, as it requires a lot
  of planning and design before you start writing code.
 Limited use-cases: SOLID principles are a good fit for
  large-scale projects but may not be suitable for small
  projects where the overhead of creating multiple classes and
  interfaces is not worth the benefits.
 Limited flexibility: SOLID principles can be limiting in
  terms of flexibility as it may not be easy to change the
  design once it's implemented.