An Overview of SOLID Principles

An Overview of SOLID Principles

The SOLID Principles of Object-Oriented Programming

Let's look at each principle one by one. Following the SOLID acronym, they are:

  1. Single Responsibility Principle

  2. Open-Closed Principle

  3. Liskov Substitution Principle

  4. Interface Segregation Principle

  5. Dependency Inversion Principle

Single Responsibility

  • It says each entity, class, or microservice should have one, well-defined purpose. While this purpose may involve multiple tasks or behaviors, it's crucial to set clear boundary for that purpose to define responsibilities within the entity or class.

  • For instance, consider the following contract (an interface in Java) from Spring Security Framework, Even though it has one responsibility, it includes two distinct behaviours: encoding and matching passwords.

      public interface PasswordEncoder {
          String encode(CharSequence rawPassword);
    
          boolean matches(CharSequence rawPassword, String encodedPassword);
    
          default boolean upgradeEncoding(String encodedPassword) {
              return false;
          }
      }
    
  • Both behaviors fall within the same responsibility—the purpose of the password encoder.

In essence, the Single Responsibility Principle applies across various entities, ranging from individual classes, methods of classes to entire microservices. While these entities may perform multiple tasks, it's essential that these tasks align with a singular purpose. This purpose must be clearly defined with well-established boundaries such that we don't have to change it afterward. And this is where the second principle Open Closed comes into picture.

Open Closed

  • The Open-Closed Principle suggests that you should try not to change a responsibility rather you should be able to use the responsibility that you have to implement new functionality. Because when you change something that already exists, it means that you actually change the purpose that you initially defined.

  • That's why, when designing in object-oriented programming, your classes, and at the architecture level, your microservices take into consideration the responsibilities.

  • Consider the boundaries of your responsibilities and the purpose of the specific object you design. This aspect should not be changed afterward or should be changed as rarely as possible.

  • For instance, consider the following contract (an interface in Java) from Spring Security Framework again,

      public interface PasswordEncoder {
          String encode(CharSequence rawPassword);
    
          boolean matches(CharSequence rawPassword, String encodedPassword);
    
          default boolean upgradeEncoding(String encodedPassword) {
              return false;
          }
      }
    

    Do we typically change the PasswordEncoder interface? No, it's something we obtain from the framework and utilize as is. It has its boundaries defined by the encode and match behaviors. When we use it, we leverage it to develop new functionalities for our app. Thus, this object is open for new development but closed for modification. We refrain from altering the object to align with our new developments.

One implementation (class, entity) should not be changed; it's open to being used for developing new functionalities but closed to modification. This is because altering it likely means changing its responsibility, thereby deviating from its original purpose. This could occur if you didn't initially choose the correct boundary for its single responsibility or if you're attempting to add additional responsibilities, which isn't ideal.

Dependency Inversion

Let's discuss Dependency Inversion first to better understand Liskov Substitution and Interface Segregation because they are related to abstraction.

  • This principle is related to abstraction, stating that one implementation (object) should depend on the abstraction of another. By this way we are able to decouple the objects.

  • Consider this as when you have two objects, you shouldn't directly link them but instead link them through a contract(Interface).

  • This approach provides the possibility to change the implementation in the future. For example, we may find a better or more performant solution down the line, allowing us to replace the implementation without affecting those who are consuming it.

Liskov Substitution

  • This principle tells us that when you use a contract, any implementation provided should not change the logic of your algorithm. It should not change the way your application is working.

  • Let's dive into a problem which it addresses.

    • If you see the below example getSomeSet() follows the Dependency Inversion principle, but it violates LISKOV principle.

        /*
        Liskov principle:
        It says not to use something more than the contract offers you
      
        Never do like this, it violates LISKOV principle
        */
        public class Example1 {
      
          public static void main(String[] args) {
              Set<Integer> set = getSomeSet();
              // here you can not consider that they are always sorted
              // someone may change the code instead of TreeSet, for ex: they may provide HashSet
          }
      
          /*
          assume this is in the other class
           */
          private static Set<Integer> getSomeSet() {
              return new TreeSet<>(); // sorted
      
              // return new HashSet<>(); // not sorted anymore but still the program will work 
                                       // we will not get any compilation error, but it may change how our app behaves earlier
          }
      
        }
      
    • let's consider below example, it follows LISKOV principle.

          /*
          Liskov principle:
          It says not to use something more than the contract offers you.
          */
          public class Example2 {
      
            public static void main(String[] args) {
                Set<Integer> set = getSomeSet();
                // here you can not consider that they are always sorted
            }
      
            /*
            assume this is in the other class
             */
            private static SortedSet<Integer> getSomeSet() {
                return new TreeSet<>(); // sorted
            }
      
          }
      

Interface Segregation

  • This principle, like Liskov Substitution, is about abstraction. It suggests that when you create a contract, you should define its boundaries so that each entity consumes only what it needs.

  • Consider the example of TreeSet class in Java, which implements NavigableSet, SortedSet, and Set. SortedSet, positioned above Set, adds the requirement for sorted behavior to the collection.

        /*
        Liskov principle:
        It says not to use something more than the contract offers you.
        */
        public class Example {
    
          public static void main(String[] args) {
              Set<Integer> set = getSomeSet();
              // here you can not consider that they are always sorted
          }
    
          /*
          assume this is in the other class
           */
          private static SortedSet<Integer> getSomeSet() {
              return new TreeSet<>(); // sorted
          }
    
        }
    
  • Set, SortedSet, and NavigableSet exemplify Interface Segregation. We shouldn't force someone to always use a sorted Set; sometimes, we simply need a collection that doesn't allow duplicates. Segregating the responsibility of the contract allows for flexibility in implementation choices.

Interface Segregation ensures that contracts define boundaries, allowing entities to consume only what they need, as seen in Java's TreeSet class implementing various set interfaces with distinct responsibilities.

Conclusion

In conclusion, adhering to the SOLID principles—Single Responsibility, Open-Closed, Liskov Substitution, Interface Segregation, and Dependency Inversion—can greatly enhance the quality, maintainability, and flexibility of your software projects.

Did you find this article valuable?

Support Jyotirmaya Das by becoming a sponsor. Any amount is appreciated!