Here’s what is given:
public interface Request {}
// there are 20 subclasses of Request
public class CreateUserRequest implements Request {
@NotEmpty
public String userName;
}
// request processor is a thing that aimed to process requests
public interface RequestProcessor<TRequest extends Request> {
boolean processRequest(TRequest request);
}
public class ServiceFacade {
// 20 processors like this one
private final RequestProcessor<CreateUserRequest> createUserRequestProcessor;
public ServiceFacade(
RequestProcessor<CreateUserRequest> createUserRequestProcessor) {
this.createUserRequestProcessor = createUserRequestProcessor;
}
// 20 methods like this one
public boolean createUser(CreateUserRequest request) {
createUserRequestProcessor.processRequest(request);
}
}
An object implementing RequestProcessor<T>
is aimed to process requests of type T
. Moreover, it is responsible for handling all the related errors (like invalid request, DB access failure and so forth). So, the workflow for CreateUserRequest
should look like this:
request
validate/throw
make sure user doesn't exist yet/throw
create user
As usual, other types of requests may require more steps to be performed.
As an illustration: in case of AddUserAsFriendRequest
we’ll have to check that both users exist and that friend candidate has allowed others to add him as friend. Oh, and also, if one user adds more than 10 friends in a day, he’s then limited to 1 friend per 2 hours for the rest of the day.
The question is, which hierarchy/structure/approach should I use in order to be able to extend this functionality in both “number of request types” and “request process workflow”.
There are only 2 approaches I see.
Inheritance
public abstract class ValidatingRequestProcessor<TRequest extends Request>
implements RequestProcessor<TRequest> {
protected final Validator validator;
public ValidatingRequestProcessor(Validator validator) {
this.validator = validator;
}
public boolean processRequest(TRequest request) {
Set<ConstraintViolation<TRequest>> violations =
validator.validate(request);
if(!violations.isEmpty()) {
return false;
}
return processValidatedRequest(request);
}
protected abstract boolean processValidatedRequest(TRequest request);
}
public class CreateUserValidatingRequestProcessor
extends ValidatingRequestProcessor<CreateUserRequest> {
public CreateUserValidatingRequestProcessor(Validator validator) {
super(validator);
}
protected boolean processValidatedRequest(TRequest request) {
// request is valid, so I can try to create user here
}
}
and so forth here. In case of N
“logical steps”, I’m going to have (N-1)
nested extends
and N
classes:
class A implements RequestProcessor {}
class B extends A {}
class C extends B {}
class D extends C {}
...
class Z extends Y {}
RequestProcessor requestProcessor = new Z(); // TADA!
Composition
public class ChainingRequestProcessor<TRequest extends Request>
implements RequestProcessor<TRequest> {
private final RequestProcessor<TRequest> processorA;
private final RequestProcessor<TRequest> processorB;
public ChainingRequestProcessor(
RequestProcessor<TRequest> processorA,
RequestProcessor<TRequest> processorB) {
this.processorA = processorA;
this.processorB = processorB;
}
public boolean processRequest(TRequest request) {
return processorA.processRequest(request) &&
processorB.processRequest(request);
}
}
public RequestValidatorProcessor<TRequest extends Request>
implements RequestProcessor<TRequest> {
private final Validator validator;
public RequestValidatorProcessor(Validator validator) {
this.validator = validator;
}
public boolean processRequest(TRequest request) {
return !validator.validate(request).isEmpty();
}
}
public CreateUserProcessor
implements RequestProcessor<CreateUserRequest> {
public boolean processRequest(CreateUserRequest request) {
// not sure whether request is valid or not
// can try to create user here
}
}
and so forth. In case of N
“logical steps” I’m just going to have N
classes. So:
class A implements RequestProcessor {}
class B implements RequestProcessor {}
class C implements RequestProcessor {}
class D implements RequestProcessor {}
...
class Z implements RequestProcessor {}
// just guess
RequestProcessor requestProcessor = new A(new B(), new C(new D())); // TADA!
Which approach is better for this task? Better means:
- Easier to modify logic
- Easier to add logic
- Easier to test
4
Based on your example, I would definitely go for composition over inheritance. It would be much easier to rearrange the order in which the processors are executed using composition, thus giving you the logic modifiability. As far as adding logic, you could easily implement a new RequestProcessor and inject it anywhere in the processor chain that you would like without have to untangle a deep inheritance tree. For testing, it will be a lot easier to isolate a single processor for testing when it does not extend a bunch of other processors.
If you are going to use composition, I would not recommend having processors take other processors in their constructors. This level of coupling can probably be avoided. Instead, have a collection of processors and just loop through the processors to process the request. If one processor needs data from another processor to do its job, represent this with a state bag dictionary or a context object. Avoiding coupling different processors together will also improve testability.