Search code examples
javaabstract-class

How to extend java functionality of an abstract class without duplicating code?


I have an issue where I'm using a common class that many other apps are using to standardize some logic and reduce duplicate code. Most classes using this Operations class only ever need CRUD methods. However, in this specific case I need to call a 5th method, execute. Although, I don't want to repeat the logic that is defined inside the on method.

How would you recommend I can call the onExecute method via the same flow? If I can.

Ideally I would like to be able to do something like:

  1. Operations.on calls x number of methods (if they exist)
  2. extending classes like UserOperations can define any number of custom methods that will be invoked the same way the MyOperation methods are invoked.

I've shared the class structure below. I had to obscure the code to protect privacy agreements. If you need more clarification on the issue then please let me know and I can try to elaborate further.

// Common Code Package
public abstract class Operations {

    public enum MyOperation {
        CREATE, RETRIEVE, UPDATE, DELETE
    }


    protected MyCustomObject custom;
    protected MyOperation operation;


    public void on(MyOperation operation, Object instance, MyCustomObject custom){

        this.custom = custom;
        this.operation = operation;

        if(operation == null){
            throw new IllegalArgumentException("'operation' is invalid");
        }
        if(instance == null){
            throw new IllegalArgumentException("'instance' is invalid");
        }
        if(custom == null){
            throw new IllegalArgumentException("'custom' is invalid");
        }

        /* Other Logic */

        switch(operation){
        case CREATE: 
            onCreate(instance);
            break;
        case RETRIEVE: 
            onRetrieve(instance);
            break;
        case UPDATE: 
            onUpdate(instance);
            break;
        case DELETE: 
            onDelete(instance);
            break;
        default: 
            throw new UnsupportedOperationException(String.format("'%s' Operation Not Supported!", operation));
        }

        /* More Logic */


        if(!custom.isOkay()){
            throw new IllegalStateException("Not Okay");
        }
    }    


    protected void onCreate(Object instance) {
        throw new UnsupportedOperationException("Create Operation Not Supported!");
    }

    protected void onRetrieve(Object instance) {
        throw new UnsupportedOperationException("Retrieve Operation Not Supported!");
    }

    protected void onUpdate(Object instance) {
        throw new UnsupportedOperationException("Update Operation Not Supported!");
    }

    protected void onDelete(Object instance) {
        throw new UnsupportedOperationException("Delete Operation Not Supported!");
    }
}

--

// Local Code Package
@Service
public class UserOperations extends Operations {


    public void onExecute(Object instance){

        /* Different Logic */

        SomeType type = (SomeType) instance;

        if(type.someOtherField == null){
            custom.setOkay(false);
        }
    }


    @Override
    protected void onCreate(Object instance){

        /* Create Logic */

        SomeType type = (SomeType) instance;

        if(type.someField == null){
            custom.setOkay(false);
        }
    }
}

--

// Local Code Package
@RestController
@RequestMapping("/user")
public class UserController {


    @Autowired
    private UserService userService;
    @Autowired
    private UserOperations userOperations;


    @RequestMapping(value = "/create", method = RequestMethod.POST)
    public Object create(SomeType instance){

        MyCustomObject custom = new MyCustomObject();
        userOperations.on(MyOperation.CREATE, instance, custom);

        return userService.create(instance);
    }

    @RequestMapping(value = "/execute", method = RequestMethod.POST)
    public Object execute(){

        MyCustomObject custom = new MyCustomObject();

        // How??? MyOperation doesn't include EXECUTE
        // userOperations.onExecute(instance); doesn't include logic inside `on` method
        // userOperations.on(MyOperation.EXECUTE, instance, custom);

        return userService.execute(instance);
    }
}

Solution

  • I was thinking all night about this and decided I can accomplish what I want using reflection if I lookup the method by name. I got that working with some minor tweaks on both sides. I added backwards support for the enumerator to not disturb the other apps using the same underlying common code. The enumerator methods will definitely exist while any other methods may or may not exist depending on the extending class.

    I don't love that the additional types aren't "type-safe", but I think it's still good. Let me know if you think of something better.

    * Updated Code *

    // Common Code Package
    public abstract class Operations {
    
        private static final Logger LOGGER = LoggerFactory.getLogger(Operations.class);
    
        public enum MyOperation {
            CREATE, RETRIEVE, UPDATE, DELETE
        }
    
    
        protected MyCustomObject custom;
        protected String operation; 
    
        public void on(MyOperation operation, Object instance, MyCustomObject custom){
    
            // Change 'operation' to Standard Case e.g. CREATE => Create
            String lower = operation.name().toLowerCase();
            String value = lower.substring(0, 1).toUpperCase() + lower.substring(1);
    
            // Delegate Invocation
            on(value, obj, custom);
        }
        public void on(String operation, Object instance, MyCustomObject custom){
    
            this.custom = custom;
            this.operation = operation;
    
            if(operation == null){
                throw new IllegalArgumentException("'operation' is invalid");
            }
            if(instance == null){
                throw new IllegalArgumentException("'instance' is invalid");
            }
            if(custom == null){
                throw new IllegalArgumentException("'custom' is invalid");
            }
    
            /* Other Logic */
    
            try {
                String methodName = String.format("on%s", operation);
                this.getClass().getDeclaredMethod(methodName, Object.class).invoke(this, obj);
    
            } catch(IllegalAccessException | InvocationTargetException | NoSuchMethodException | SecurityException e){
                LOGGER.error("ERROR: {}", e);
            }
    
    
            /* More Logic */
    
    
            if(!custom.isOkay()){
                throw new IllegalStateException("Not Okay");
            }
        }    
    
    
        public void onCreate(Object instance) {
            throw new UnsupportedOperationException("Create Operation Not Supported!");
        }
    
        public void onRetrieve(Object instance) {
            throw new UnsupportedOperationException("Retrieve Operation Not Supported!");
        }
    
        public void onUpdate(Object instance) {
            throw new UnsupportedOperationException("Update Operation Not Supported!");
        }
    
        public void onDelete(Object instance) {
            throw new UnsupportedOperationException("Delete Operation Not Supported!");
        }
    }
    

    --

    // Local Code Package
    @Service
    public class UserOperations extends Operations {
    
    
        public void onExecute(Object instance){
    
            /* Different Logic */
    
            SomeType type = (SomeType) instance;
    
            if(type.someOtherField == null){
                custom.setOkay(false);
            }
        }
    
    
        @Override
        public void onCreate(Object instance){
    
            /* Create Logic */
    
            SomeType type = (SomeType) instance;
    
            if(type.someField == null){
                custom.setOkay(false);
            }
        }
    }
    

    --

    // Local Code Package
    @RestController
    @RequestMapping("/user")
    public class UserController {
    
    
        @Autowired
        private UserService userService;
        @Autowired
        private UserOperations userOperations;
    
    
        @RequestMapping(value = "/create", method = RequestMethod.POST)
        public Object create(SomeType instance){
    
            MyCustomObject custom = new MyCustomObject();
            userOperations.on(MyOperation.CREATE, instance, custom);
    
            return userService.create(instance);
        }
    
        @RequestMapping(value = "/execute", method = RequestMethod.POST)
        public Object execute(SomeType instance){
    
            MyCustomObject custom = new MyCustomObject();
            userOperations.on("Execute", instance, custom);
    
            return userService.execute(instance);
        }
    }