Search code examples
javadesign-patternsarchitecturesoftware-designdesign-principles

Is it better to wrap chain of responsibility functionality than have it directly in a class?


I have been focusing on learning programming principles and patterns but the chain of responsibility examples I have found all seem to contradict other principles/patterns. The placement of sethandler and nexthandler directly in a class that will do more than just that seems like a really really bad idea.

While trying to learn previously patterns I did find lots of unrealistically simple examples so at first I thought that to was the case with the examples I found. But after lots of googling I could not find any examples coming close to what I imagined it should be. So then I started checking articles and I could not find even a short sentence about how the handler functionality should be separate from the class that is using it.

It got me thinking maybe it is not such a big deal to have the chain of responsibility on a concrete class since its just two methods.

But again, that just seems really really wrong. If you want to use the class in a non chain of responsibility pattern way somewhere else what would you do since the Chain of Responsibility functionality is built directly into the class.

After going through pages and pages of google articles about the chain of responsibility and finding nothing about this topic I did eventually find one example which implements the functionality with a wrapper class. It pretty much delegates a concrete class into a wrapper class so that the concrete class could still be used without needing the chain of responsibility functionality.

So my question is.. will most real world implementations of the chain of responsibility principle use a wrapper class or will it be directly in the class? Maybe I just misunderstood the situations this pattern would actually be used in?

To really be able to compare and understand each method I decided to write my own examples using both methods. Below I pasted the most important parts of both and linked to the full examples.

Normal Method

ILoanApprover

interface ILoanApprover{
  public void approveLoan(Loan loan);
  public void setNextApprover(ILoanApprover nextApprover);
}

Employee

abstract class Employee implements ILoanApprover{
  protected ILoanApprover nextApprover;
  protected int approvalLimit;
  private String rank;

  public Employee(String rank, int approvalLimit){
    this.rank = rank;
    this.approvalLimit = approvalLimit;
  }

  @Override
  public void setNextApprover(ILoanApprover nextApprover){
    this.nextApprover = nextApprover;
  }
  @Override
  public void approveLoan(Loan loan){

    if (approvalLimit > loan.getAmount() ){
      System.out.println(rank+" approves loan of "+loan.getAmount());
      return;
    }
    nextApprover.approveLoan(loan);
  }
}

Manager

class Manager extends Employee{
  public Manager(){
    super("Manager", 5000);
  }
}

Loan

class Loan{
  private int amount;
  public Loan(int amount){
    this.amount = amount;
  }
  public int getAmount(){ return amount; }
}

Wrapper Method

ILoanApprover

interface ILoanApprover{
  public boolean approveLoan(Loan loan);
}

ILoanHandler

interface ILoanHandler{
  public void setNextLoanHandler(ILoanHandler nextHandler);
  public void handleLoanApproval(Loan loan);
}

LoanHandler

class LoanHandler implements ILoanHandler{

  protected ILoanApprover approver;
  protected ILoanHandler nextHandler;

  public LoanHandler(ILoanApprover approver){
    this.approver = approver;
  }

  @Override
  public void setNextLoanHandler(ILoanHandler nextHandler){
    this.nextHandler = nextHandler;
  }

  @Override
  public void handleLoanApproval(Loan loan){

    boolean approved = approver.approveLoan(loan);

    if (!approved && nextHandler != null){
      nextHandler.handleLoanApproval(loan);
    }
  }
}

Employee

abstract class Employee implements ILoanApprover{
  protected int approvalLimit;
  private String rank;
  public Employee(String rank, int approvalLimit){
    this.rank = rank;
    this.approvalLimit = approvalLimit;
  }

  @Override
  public boolean approveLoan(Loan loan){

    if (approvalLimit > loan.getAmount() ){
      System.out.println(rank+" approves loan of "+loan.getAmount());
      return true;
    }

    return false;
  }
}

Manager

class Manager extends Employee{
  public Manager(){
    super("Manager", 5000);
  }
}

Loan

class Loan{
  private int amount;
  public Loan(int amount){
    this.amount = amount;
  }
  public int getAmount(){ return amount; }
}

Main

ILoanHandler man = new LoanHandler(new Manager());

Solution

  • Well, although I think the intuition is right, I have a few points :

    • The COR pattern doesn't specify precisely how to implement it in real code, because there are many ways to do this (directly in concrete class, inheritance, generics, containment, composition etc), and I think the pattern is trying to describe the essence of a single idea, ignoring the details.

    • Using the terminology of the wikipedia article : The most important aspect of the pattern is the Handler interface. Given a stable and well defined interface you can implement your Receiver classes in numerous ways and that choice depends on other considerations. You mentioned one such consideration, which is re-usability, and with this you can be guided by the Single Responsibility Principle (SRP): since, in your specific case, the Employee class is suggestive of a class which should manage employee details, then the loan-approval responsibility perhaps ought to be entirely delegated to a LoanApproval class, such that Employee has no direct dependency on LoanApproval (and hence does not implement ILoanApprover). For this, you can be guided by Clean Architecture principles where Use-Cases (e.g. LoanApproval) should depend on Entities (e.g. Employee and Loan) and not the other way around (in fact, more precisely, dependent on segregated interfaces of Entities, which is a separate matter). Another way of phrasing this is to say that the Employee should probably not be acting as either a Reciever or a Sender. Given this, it may (or may now not!), be the case, that you want to use LoanApproval outside of the COR pattern, and this is a good argument for separation again, both for reusability and to remove duplicated logic. You can do that with some of the methods mentioned above e.g. inheritance, generics etc...

    • Another related way of thinking about the above, is to think about what happens to your designs when you need to add other tasks/use-cases for the Employee. Employee will need to implement more Handler interfaces and some of those might have conflicting function names etc and this is where you get into a mess, and the result is you have a core class that is continually being extended with more and more responsibilities.

    So, you are definitely right to think about decoupling responsibilities, and re-usability, and removing duplicated code, and there are different ways to do this, but I consider these issues are orthogonal to the COR pattern, which is purely about the advantage of being able to chain tasks together dynamically at runtime.

    P.S. in the real world I have seen this pattern implemented in all sorts of horrible ways... Sometimes using the real world as a guide is not helpful!