Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Replace switch-case with polymorphism

I know there are similar questions already, but looking at them I still have some doubts about how I should design my code. I have a service that allows for User registration / login /update / delete. The thing is that the User is an abstract type, which contains the data typeOfUser based on which the actual registration / update / delete methods should be called, and right now I do that in a switch-case block. I'd like to replace that with some better design.

UserController.java

public class UserController {

    public UserDto register(UserDto user) {
        switch(user.getTypeOfUser()) {
        case DRIVER: return driverService.register(user);
        case CUSTOMER: return customerService.register(user);
        // ...
        }
    } 

    public UserDto update(UserDto user) {
        switch(user.getTypeOfUser) {
        case DRIVER: return driverService.update((DriverDto) user);
        case CUSTOMER: return customerService.update((CustomerDto) user);
        // ...
        }
    }

    public UserDto login(long userId) {
        loginService.login(userId);

        UserBO user = userService.readById(userId);

        switch(user.getTypeOfUser) {
        case DRIVER: return DriverDto.fromBO((DriverBO) user);
        case CUSTOMER: return CustomerDto.fromBO((CustomerBO) user);
        // ...
        }
    }

    // ...
}

I understand that something like Visitor pattern could be used, but would I really need to add the methods of registration / login /update / delete in the Enum itself? I don't really have a clear idea on how to do that, any help is appreciated.

like image 840
wesleyy Avatar asked Feb 17 '18 09:02

wesleyy


2 Answers

I'd like to replace that with some better design.

The first step towards replacing the switch statement and take advantage of Polymorphism instead is to ensure that there is a single contract (read method signature) for each of the operations regardless of the user type. The following steps will explain how to achieve this :

Step 1 : Define a common interface for performing all operations

interface UserService {
    public UserDto register(UserDto user);
    public UserDto update(UserDto user);
    public UserDto login(UserDto user)
}

Step 2 : Make UserController take a UserService as a dependency

public class UserController {
     
    private UserService userService;
    
    public UserController(UserService userService) {
        this.userService = userService;
    }
    
    public UserDto register(UserDto user) {
       userService.register(user);
    } 

    public UserDto update(UserDto user) {
        userService.update(user);
        
    }

    public UserDto login(long userId) {
        userService.login(user);
    }
      
}

Step 3 : Create subclasses to handle different types of users that take CustomerDto and CustomerBO as a dependency

class CustomerService implements UserService {
    private CustomerDto userDto;
    private CustomerBO userBO;
    
    public CustomerService(UserDto userDto,UserBO userBo) {
          this.userDto = (CustomerDto)userDto;
          this.userBO= (CustomerBO)userBo;
    }

    //implement register,login and update methods to operate on userDto and userBo
}

Implement the DriverService class in a similar fashion with a dependency on DriverBo and DriverDto objects respectively.

Step 4 : Implement a runtime factory that decides which service to pass to UserController :

public UserControllerFactory {
    public static void createUserController(UserDto user) {
       if(user.getTypeOfUser().equals(CUSTOMER)) { 
           return new UserController(new CustomerService(user));
       } else if(user.getTypeOfUser().equals(DRIVER)) {
           return new UserController(new DriverService(user));
       }
    }
 
}

Step 5 Call the factory to create a user controller

UserDto user = someMethodThatCreatesUserDto(();
UserController controller = UserControllerFactory.createUserController(user);
controller.register();
controller.update();
controller.login();

The advantage of the above approach is that the switch/if-else statements are moved all they way back to a single class i.e the factory.

like image 189
Chetan Kinger Avatar answered Oct 02 '22 04:10

Chetan Kinger


You'd want something like that:

public abstract class User {
    abstract void register();
    abstract void update();
    abstract void login();

    // maybe some more common non-abstract methods
}

Any type of User will have a class that extends this abstract class and therefore must implement all its abstract methods, like this:

public class Driver extends User {
    public void register() {
        // do whatever a driver does when register...
    }
    public void update() {
        // do whatever a driver does when update...
    }
    public void login() {
        // do whatever a driver does when login...
    }
}

public class Customer extends User {
    public void register() {
        // do whatever a customer does when register...
    }
    public void update() {
        // do whatever a customer does when update...
    }
    public void login() {
        // do whatever a customer does when login...
    }
}

This way, you're avoiding any switch case code. For instance, you can have an array of Users, each one them will be instantiated using new Driver() or new Customer(). Then, for example, if you're iterating over this array and executing all the Users login() method, each user's login() will be called according to its specific type ==> no switch-case needed, no casting needed!

like image 35
SHG Avatar answered Oct 02 '22 06:10

SHG