Chain of Responsibility Pattern in APEX

I was interested in seeing if this was possible in APEX, and was looking around for an implementation of this pattern and wasn’t able to find a reference, so I decided to just have a go and see what I could come up with. This is a very well documented pattern in other programming languages…so can it be done?

Use Case

The use case of this pattern is when you find yourself in a scenario where you have a large amount of if…else if…else if blocks. It enables you to refactor these blocks into something that is more maintainable, readable and testable. It is, in effect, an object oriented approach to if…else if…else if.

Here is a common and easy to understand example. Let’s say we have a user registration screen in a community, and we have a custom controller on the backend of that which is used to validate various aspects of that input before allowing that user to be registered in the system. Lets say we want to validate 3 things about that user:

  1. the potential users location
  2. the potential users age
  3. the potential users email address (against a regex)

Consider how might this look in APEX if we do not use the Chain of Responsibility Pattern:

boolean isValid;

UserRegistrationRequest ur = new UserRegistrationRequest();
ur.email = 'someone@live.co.uk';
ur.age = 36;
ur.isoLocationCode = 'GB';
            
if(!(Pattern.matches('^[a-zA-Z0-9._|\\\\%#~`=?&/$^*!}{+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,4}$', ur.email){
    isValid = false;
} else if (ur.age < 18 ) {
    isValid = false;
} else if (ur.isoLocationCode != 'GB') {
    isValid = false;
} else {
    isValid = true;
}

Not too bad right? But if we start adding lots of conditions it can start getting hard to read. If we want to make a change to one of them, or reorder them, or introduce more complex conditions, that carries a higher risk than if each element were separated out into their own class.

Pattern Overview

There are 3 actors in this pattern.

  1. Receiver: Handles a given command.
  2. Handler: Runs through all the receivers.
  3. Sender: Invokes the handler.

There are actually 2 ways to implement this. One is where each handler is responsible for invoking the next handler in the chain, and contains the command logic (handler acts as both handler and receiver). And one where we introduce dedicated receivers & a single handler.

I will detail both approaches.

Handler acts as Handler and Receiver

For this to work you will need one interface and one abstract base class.

IHandler interface

A simple interface which has methods for setting the next handler in the chain (SetNext), and one for the concrete implementation of whatever we are trying to do (Handle).

public interface IHandler {
    IHandler SetNext(IHandler next);
    void Handle (object request);
}

Handler abstract base class

This is what we will actually extend in our concrete implementations. This simply gives us the boiler plate code required to set (SetNext) and invoke (Handle) the next item after the handle method is called.

public abstract class Handler implements IHandler{

    private IHandler Next {get; set;}
    
    // any boilerplate code goes here
    public virtual void Handle(object request) {
        if( Next!= null ) {
            Next.Handle(request);
        }
    }
    
    public IHandler SetNext(IHandler next){ 
        this.Next = next;
        return this.Next;
    }
}

UserRegistrationRequest class

Purely for the purposes of this example I have created a very simple class to store the registration details we are validating.

public class UserRegistrationRequest {
    public string email { get; set; }
    public integer age { get; set; }
    public string isoLocationCode { get; set; }
}

ApplicationException

There is also an exception class referenced in the code that you will need

public class ApplicationException extends Exception {

}

For this example we will also need 3 extensions of the Handler abstract base class. Note how on each one we have to remember to invoke the base handle method (if we do not the chaining wont work).

UserEmailValidationHandler

public class UserEmailValidationHandler Extends Handler {

     public override void Handle(object request) {
        
        UserRegistrationRequest userRegistrationRequest = (UserRegistrationRequest)request;
        
        if (!(Pattern.matches('^[a-zA-Z0-9._|\\\\%#~`=?&/$^*!}{+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,4}$', userRegistrationRequest.email))) {
            throw new ApplicationException('email validation failed');
        }
        
        //invoke the base class if we are still valid to go to the next check
        super.Handle(request);
    }
}

UserAgeValidationHandler

public class UserAgeValidationHandler Extends Handler{

     public override void Handle(object request) {
        
        UserRegistrationRequest userRegistrationRequest = (UserRegistrationRequest)request;
        
        if (userRegistrationRequest.age < 18 ) {
            throw new ApplicationException('age validation failed');
        }
        
        //invoke the base class if we are still valid to go to the next check
        super.Handle(request);
    }
}

UserLocationValidationHandler

public class UserLocationValidationHandler Extends Handler{

     public override void Handle(object request) {
       
        UserRegistrationRequest userRegistrationRequest = (UserRegistrationRequest)request;
        
        if (userRegistrationRequest.isoLocationCode != 'GB' ) {
            throw new ApplicationException('location validation failed');
        }
        
        //invoke the base class if we are still valid to go to the next check
        super.Handle(request);
    }
}

UserRegistrationValidationSender

Now we can tie it all together.

public class UserRegistrationValidationSender {

    public static boolean IsValid (UserRegistrationRequest userRegistrationRequest){
        
        try {
            //create the first handler in the chain;
            Handler handler = new UserEmailValidationHandler();
            //then use SetNext to add the other validation handlers
            handler
                .SetNext(new UserAgeValidationHandler())
                .SetNext(new UserLocationValidationHandler());
            //then call the handle method 
            handler.Handle(userRegistrationRequest);
            
        }
        catch (ApplicationException ex) {
            //added system debug to view the errors when trying this example
            system.debug(ex);
            return false;
        }
  
        return true;
    }
}

Now it is much easier to understand, change the order of execution, or add in new handlers to create more validations.

We can test the functionality by running the following anonymous apex

UserRegistrationRequest userRegistrationRequest = new UserRegistrationRequest();
userRegistrationRequest.email = 'notvalid';
userRegistrationRequest.age = 25;
userRegistrationRequest.isoLocationCode = 'GB';

//will return false
system.debug(UserRegistrationValidationSender.IsValid(userRegistrationRequest));

userRegistrationRequest.email = 'someone@live.co.uk';
userRegistrationRequest.age = 25;
userRegistrationRequest.isoLocationCode = 'GB';

//will return true
system.debug(UserRegistrationValidationSender.IsValid(userRegistrationRequest));

Dedicated Handler and Receiver

This is a similar approach but allows us to remove the responsibility of managing the next step from our validation code. For this we need a dedicated class to act as a shared handler & a receiver interface.

IReceiver interface

public interface IReceiver {
	void Handle (object request);
}

Handler class

public class ValidationHandler {
    
    private list <IReceiver> receivers;
    
    public ValidationHandler(IReceiver[] receivers){
        this.receivers = receivers;
    }
    
    public void Handle (object request) {
        for (IReceiver receiver : this.receivers) {
            receiver.Handle(request);
        }
    }
    
    public void SetNext (IReceiver next){
        this.receivers.Add(next);
    }
}

So here the idea is we pass this a list of receivers, and then the handler iterates them and executes the handle method. The benefit of this approach is that we do not need to remember to call the Super.Handle method.

So now we can create 3 classes that implement the receiver. They are very similar to the 3 handler classes from the previous example.

UserEmailValidationReceiver

public class UserEmailValidationReceiver implements IReceiver {
	
    public void Handle(object request) {
 		UserRegistrationRequest userRegistrationRequest = (UserRegistrationRequest)request;
         
        if (!(Pattern.matches('^[a-zA-Z0-9._|\\\\%#~`=?&/$^*!}{+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,4}$', userRegistrationRequest.email))) {
            throw new ApplicationException('email validation failed');
        }
	}
}

UserLocationValidationReceiver

public class UserLocationValidationReceiver implements IReceiver {
 	
    public void Handle(object request) {
       
        UserRegistrationRequest userRegistrationRequest = (UserRegistrationRequest)request;
        
        if (userRegistrationRequest.isoLocationCode != 'GB' ) {
            throw new ApplicationException('location validation failed');
        }
    }
}

UserAgeValidationReceiver

public class UserAgeValidationReceiver implements IReceiver {
  
    public void Handle(object request) {
        
        UserRegistrationRequest userRegistrationRequest = (UserRegistrationRequest)request;
        
        if (userRegistrationRequest.age < 18 ) {
            throw new ApplicationException('age validation failed');
        }
    } 
}

Update UserRegistrationValidationSender

Now we can add the following method to our existing UserRegistrationValidationSender class to try this out:

  public static boolean IsValidUsingReceivers (UserRegistrationRequest userRegistrationRequest){
		try {
			list <IReceiver> receivers = new List<IReceiver> {
				new UserEmailValidationReceiver(),
                new UserAgeValidationReceiver(),
                new UserLocationValidationReceiver()
			};
                    
			ValidationHandler handler = new ValidationHandler(receivers);
            
			handler.Handle(userRegistrationRequest);
            
        } catch  (ApplicationException ex) {
            system.debug(ex);
            return false;
        }
        return true; 
    }  

We can now try this out just by swapping out the method we call from our anonymous code block that we wrote earlier

UserRegistrationRequest userRegistrationRequest = new UserRegistrationRequest();
userRegistrationRequest.email = 'andakscnalsnc';
userRegistrationRequest.age = 25;
userRegistrationRequest.isoLocationCode = 'GB';

//will return false
system.debug(UserLocationValidationProcessor.IsValidUsingReceivers(userRegistrationRequest));

userRegistrationRequest.email = 'someone@live.co.uk';
userRegistrationRequest.age = 36;
userRegistrationRequest.isoLocationCode = 'GB';

//will return true
system.debug(UserLocationValidationProcessor.IsValidUsingReceivers(userRegistrationRequest));

And we can see the results are the same as before.

Summary

So we have seen how this pattern works and how it can be implemented in APEX. I hope that you are able to follow along with the code samples and get this working.

Please subscribe to the blog if you found this interesting.

One thought on “Chain of Responsibility Pattern in APEX

  1. You can actually avoid having to call super.Handle() in each implementation if you make Handle() in the abstract base class an abstract method and add another (not abstract) method to the base class that does the work of calling Handle() and then calling it for the next step (if it exists). You would then call the new non-abstract method instead of .Handle() to run the chain. Probably should have flip-flopped that explanation so that you still wind up calling Handle() in the end, but oh well.

    Like

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s