SOLID Principles

Single Responsibility Principle

A module should have one, and only one, reason to change. A module should be responsible to one, and only one, user or stakeholder. A module should be responsible to one, and only one, actor(a group of people) like members vs admin members. Members are consumers or customers and admins are admin users. A class should have one and only one responsibility. In our application level code, we define entity classes to represent real time entities such as User, Employee, Account etc. Most of these classes are examples of SRP principle because when we need to change the state of a person, only then we will modify the User class, and so on. When I am in doubt of whether a function abides by the SRP. I try to explain what that function does? If I have to use the conjunctions “and” or “or” then it is likely that the function doesn’t abide by the SRP.

SRP is applied to the database(DAO), service(business logic), and the controller layer(endpoint) even to the UI. An entity class should focus on just one database table, UserService should only focus on business methods that work with data in User class. Saving and Reading from the database should be delicated to the UserDAO interface. UserController focuses on endpoints that work with UserService business logic. AdminUserController has logic that is different from UserController’s logic.

@Data
@AllArgsConstructor
@NoArgsConstructor
@JsonInclude(value = Include.NON_NULL)
public class User implements Serializable {

    private static final long serialVersionUID = 1L;

    private Long id;
    private String firstName;
    private String lastName;
    private LocalDate dateOfBirth;
    private String email;
    private String password;

}
public interface UserService {

    User updateEmail(String newEmail);
}
@Service
@Slf4j
public class UserServiceImp implements UserService {
    
    /*
     * This UserServiceImp focuses on logic that are related to User
     */
    
    
    @Override
    public User updateEmail(String newEmail) {
        /**
         * 1. look up if newEmail already exists in db<br>
         * 2. change email to newEmail in db
         */
        return null;
    }

}
public interface UserAuthService {

    User signUp(User user);
    
    User login(String email, String password);
    
}

@Service
@Slf4j
public class UserAuthServiceImp implements UserAuthService {
    /*
     * User authentication only
     */
    @Override
    public User signUp(User user) {
        log.info("signUp(..)");
        return null;
    }

    @Override
    public User login(String email, String password) {
        log.info("login(..)");
        return null;
    }

}

Here we have a User entity class and two service classes that demo SRP. The UserAuthService interface focuses on authenticating the user where as the UserService interface focuses on User business logic. There is only one reason why we should make a change to these services.

Note that the service class should not contain utility global functions related to the module. It is better to separate global utility methods in another globally accessible class like UserUtils. This will help in maintaining the class for that particular purpose, and we can decide the visibility of that class to a specific module only. This means that UserUtils may or may not be accessible outside of its package.

Source code on Github

Benefits of SRP

The class is easier to read, understand, maintain, and is more reusable.

It is easier to test functions that are performing one basic operation.

It makes version control easier. For example, say we have a DAO class that handles database operations, and we see a change in that file in the git commits. By following the SRP, we will know that it is related to storage or database-related stuff.

Merge conflicts will appear less as files will have a single reason to change and conflicts that do exist will be easier to resolve.

Note that it is inevitable that new business requirements  will come up or updates to your code base will happen. Therefore, code that is decoupled will make your life easier when implementing the change. The last thing you want to do is to make one change and create a cascading effect to the rest of the system. 

Open-Closed Principle

A software artifact, entity, or class should be open for extension but closed for modification.

Open for extension means that the behavior of the module can be extended. As the requirements of the application change, we are able to extend the module with new behaviors that satisfy those changes. In other words, we are able to change what the module does. It’s called software for a reason that it should be easy to change.

Closed for modification means the behavior of a module does not result in changes to the source or binary code of the module. The binary executable version of the module, whether in a linkable library, a DLL, or a Java .jar, remains untouched.

Behavior and functionality can be easily changed by just extending what is already present – instead of modifying the present code.

If a behavior coded in a class needs to be changed, a way of doing that is to create a subclass and override methods as necessary. No change to the superclass is necessary, just new code in the subclass.

We still need some kind of toggle mechanism to switch between the original and extended behaviour, which could require modification of the present code, and the design needs to support the particular extension that we want to make – we cannot design our code in a way that ANY modification is possible without touching it.

The one exception to the rule is when fixing bugs in existing code. So, we should modify our class only at the time of bug fixing.

Design and code should be done in a way that new functionality should be added with minimum or no changes in the existing code. When needs to extend functionality – avoid tight coupling, don’t use if-else/switch-case logic, do code refactoring as required.

Benefits of OCP

Code is easier to maintain. Interfaces in code offers an additional level of abstraction which in turn enables loose coupling. The implementations of an interface are independent of each other and don’t need to share any code. Hence, you can easily maintain your code with client’s keep changing requirements.

Flexibility to extend functionalty. Further, if any change request arises in future, your code will be more flexible to extend.

public interface UserService {

    User updateEmail(String newEmail);
}
public class UserServiceImp implements UserService {
    
    /*
     * This UserServiceImp focuses on logic that are related to User
     */
    
    
    @Override
    public User updateEmail(String newEmail) {
        /**
         * 1. look up if newEmail already exists in db<br>
         * 2. change email to newEmail in db<br>
         * 3. send email to new email for verification
         */
        return null;
    }

}
public class SpouseService implements UserService {

    @Override
    public User updateEmail(String newEmail) {
        /**
         * 1. look up if newEmail already exists in db<br>
         * 2. change email to newEmail in db<br>
         * 3. send email to new email for verification<br>
         * 4. send email to Primary user that spouse email was change
         */
        return null;
    }

}

Source code on Github

Here we are extending the fuctionality of UserService as for SpouseService. Notice that there an extra logic when a spouse changes their email.

The LISKOV Substitution Principle

if we substitute a superclass object reference with an object of any of its subclasses, the program should not break. 

The LSP is applicable when there’s a supertype-subtype inheritance relationship by either extending a class or implementing an interface. We can think of the methods defined in the supertype as defining a contract. Every subtype is expected to stick to this contract. If a subclass does not adhere to the superclass’s contract, it’s violating the LSP.

How can a method in a subclass break a superclass method’s contract? There are several possible ways:

  1. Returning an object that’s incompatible with the object returned by the superclass method.
  2. Throwing a new exception that’s not thrown by the superclass method.
  3. Changing the semantics or introducing side effects that are not part of the superclass’s contract

LSP violations are a design smell. We may have generalized a concept prematurely and created a superclass where none is needed. Future requirements for the concept might not fit the class hierarchy we have created.

If client code cannot substitute a superclass reference with a subclass object freely, it would be forced to do instanceof checks and specially handle some subclasses. If this kind of conditional code is spread across the codebase, it will be difficult to maintain.

Every time we add or modify a subclass, we would have to comb through the codebase and change multiple places. This is difficult and error-prone.

It also defeats the purpose of introducing the supertype abstraction in the first place which is to make it easy to enhance the program.

The LSP is a very useful idea to keep in mind both when developing a new application and when enhancing or modifying an existing one.

When designing the class hierarchy for a new application, the LSP helps make sure that we are not prematurely generalizing concepts in our problem domain.

When enhancing an existing application by adding or changing a subclass, being mindful of the LSP helps ensure that our changes are in line with the superclass’s contract and that the client code’s expectations continue to be met.

Benefits or LSP

) Code Reusability
2) Easier Maintenance
3) Reduced Coupling

public interface UserNotificationService {

    boolean sendEmail(User user);
}

@Service
@Slf4j
public class UserNotificationServiceImp implements UserNotificationService {

    @Override
    public boolean sendEmail(User user) {
        /**
         * loop through and send email
         */
        return false;
    }

}
public boolean notifyAllUsersOfDayOff() {
    List<User> users = new ArrayList<>();
    users.add(new User());
    users.add(new AdminUser());

    /**
     * call the UserNotificationService.sendEmail(...) to send the notification
     */

    for (User user : users) {
        // User and AdminUser
        userNotificationService.sendEmail(user);
    }
    
    return false;
}

Source code on Github

Interface Segregation Principle

Interface segregation simply means that we should break larger interfaces into smaller ones. Thus ensuring that implementing classes need not implement unwanted methods.

Clients should not be forced to depend upon interfaces that they do not use. The goal of this principle is to reduce the side effects of using larger interfaces by breaking application interfaces into smaller ones. It’s similar to the Single Responsibility Principle , where each class or interface serves a single purpose.

Precise application design and correct abstraction is the key behind the Interface Segregation Principle. Though it’ll take more time and effort in the design phase of an application and might increase the code complexity, in the end, we get a flexible code.

public interface UserService {

    User updateEmail(String newEmail);
}
public interface AdminUserService {

    boolean addClient(AdminUser adminUser, User user);
    
    boolean notifyAllUsersOfDayOff();
}

Even though we have combined these two interfaces into one, it is better long term to separate them into smaller interfaces.

Benefits of ISP

  1. Increased code readability
  2. Easier to Implement
  3. Easier to Maintain
  4. Better organization of code
  5. Don’t need to throw exceptions unnecessarily

Dependency Inversion Principle

  1. High-level modules should not depend on low-level modules. Both should depend on abstractions.
  2. Abstractions should not depend on details. Details should depend on abstractions.

Conventional application architecture follows a top-down design approach where a high-level problem is broken into smaller parts. In other words, the high-level design is described in terms of these smaller parts. As a result, high-level modules that gets written directly depends on the smaller (low-level) modules.

What Dependency Inversion Principle says is that instead of a high-level module depending on a low-level module, both should depend on an abstraction. 

@Data
@AllArgsConstructor
@NoArgsConstructor
@JsonInclude(value = Include.NON_NULL)
public class Client implements Serializable {

    private static final long serialVersionUID = 1L;

    private Long id;
    private String firstName;
    private String lastName;
    private LocalDate dateOfBirth;
    private String email;
    private String password;

    /**
     * private User user;<br>
     * 1. schedule appointment on calendar of user.<br>
     * 2. send email to user as a reminder<br>
     * 3. charge client appointment fee<br>
     * 
     * Note: instead of doing this logic here(bad idea). we moved it to the ClientService
     * interface
     */

}
@Service
public class ClientServiceImp implements ClientService {

    @Override
    public boolean bookAppointment(Client client, User user) {
        /**
         * 1. schedule appointment on calendar of user.<br>
         * 2. send email to user as a reminder<br>
         * 3. charge client appointment fee<br>
         */
        return false;
    }

}

Source code on Github

Here our entity classes or lower level classes don’t dependent on each other, they dependent on abstraction(ClientService interface)

Dependency Inversion is more focused on the structure of your code, its focus is keeping your code loosely coupled. On the other hand, Dependency Injection is how the code functionally works. When programming with the Spring Framework, Spring is using Dependency Injection to assemble your application. Dependency Inversion is what decouples your code so Spring can use Dependency Injection at run time.

Benefits of DIP

  1. Code is decoupled.

 

 

 




Subscribe To Our Newsletter
You will receive our latest post and tutorial.
Thank you for subscribing!

required
required


Leave a Reply

Your email address will not be published.