Search code examples
design-patternsdependency-injectionsolid-principles

How to practice SOLID principle of OOP design?


I'm new to SOLID principle but I understand it. My main problem is having a hard time designing my classes to follow the SOLID specially the Dependency Inversion. Sometimes it's easy to write the whole logic into procedural pattern rather than to use SOLID.

For example:

Let's say that we are creating an Attendance Monitoring System, and we have logic(or procedure) that scan the employee fingerprint, get it's ID, determine whether or not it's valid or not, determine what time he was in, write the login info to the database, and show if it's successful or not.

It is easy to write this in a procedural manner with a bunch of 'if else', loop and switch. But in future I'm gonna suffer the 'code debt'.

If we applying SOLID principle here. I know that we need to have a some kind of object like 'AttendanceServiceClass' that has a method like 'scanEmployeeID()', 'processthislogin()' or 'isItsucessful()'. And I know that this class has a dependency to a repository, userinfo, and other objects.

Basically my problem is analyzing about the design of the class and its dependencies

What is the step by step way of analyzing the design of your class?

sorry for my English.


Solution

  • Sometimes it's easy to write the whole logic into procedural pattern rather than to use SOLID

    I cannot agree more, it is easier for us programmer to handle code in procedural pattern. This makes OOP hard for programmer who accustomed to procedural programming.

    However I found it easier to write the general interface and consumer first rather than breaking the interface designed to smaller modules. This is, a kind of Test First Development -> Red, green, refactor practice. (please note that, if you want to achieve the neat design, consider following TDD instead this guide. This guide is just a small section of doing TDD)

    Say that we want to create the ServiceAttendance to do scanEmployeeID. We will have an interface like (please notice the example is in C# naming):

    public interface IServiceAttendance{
        bool ScanEmployeeId();
    }
    

    Please note that I decide the method to return bool instead of void to determine success/failure operation. Please notice the consumer example below does not implement any DI because I just want to show how to consume it. Then in the consumer, we can have:

    public void ConsumeServiceAttendance(){
        IServiceAttendance attendance = Resolve<IServiceAttendance>();
        if(attendance.ScanEmployeeId()){
            // do something
        }
    }
    

    This concludes the consumer. Now we move to implementation. Say that you can develop it using procedural programming and got the monolithic code block. You can state the implementation with pseu-like statement.

    public class ServiceAttendance : IServiceAttendance{
        public bool ScanEmployeeId(){
            bool isEmpValid = false;
            // 1 scan the employee id
            // 2 validate the login
            // 3 if valid, create the login session
            // 4 notify the user
            return isEmpValid;
        }
    }
    

    Now we have 4 steps to be done in this one operation. My principal is, not to do over 3 facade process in one method so I can simply refactor the 3 and 4 to one process. Now we have

    public class ServiceAttendance : IServiceAttendance{
        public bool ScanEmployeeId(){
            bool isEmpValid = false;
            // 1 scan the employee id
            // 2 validate the login
            // 3 if valid, create the login session and notify the user
            return isEmpValid;
        }
    }
    

    This, we have 3 main operation. We can analyze whether we need to create a smaller module or not by breaking down the operation. Say we want to break the second operation. We can get:

    // 2 validate the login
    // 2.1 check if employee id matches the format policy
    // 2.2 check if employee id exists in repository
    // 2.3 check if employee id valid to access the module
    

    The breakdown operation itself is obvious enough to break the second module into another smaller module. For 2.2 and 2.3, we need a smaller module to be injected. Simply because it will need dependency to repository, thus need to be injected. The same case apply for operation step 1 scan the employee id, because it will need dependency to fingerprint scanner, so the scanner handler must be implemented in separated module.

    We can always breakdown the operation, as we can do it in 2.1:

    // 2.1 check if employee id matches the format policy
    // 2.1.1 employee id must match the length
    // 2.1.2 employee id must has format emp#####
    

    Now I am unsure if 2.1.1 and 2.1.2 need to be broken down into 2 separated modules, it is up to you to decide. And now we got the interfaces, then we can start the implementation. Expect to throw exceptions during validation or you will need to pass custom class to handle error messages.