2

I have code that I want to modify applying the Chain of Responsibility (CoR) pattern, but I have a doubt about performance if I have many successors. First, this is the code without CoR:

public class OperationService
{
    public Response Exec_Operation_1(Data data)
    {
        Operation_1 op = new Operation_1();
        return op.ExecOperation();
    }

    public Response Exec_Operation_2(Data data)
    {
        Operation_2 op = new Operation_2();
        return op.ExecOperation();
    }
}
public class Operation_1
{
    private Data _data;
    public Operation_1(Data data)
    {
        this._data = data;
    }
    public Response ExecOperation()
    {
        //process data. So much code. Many instantiations.
        //Many references to other assemblies.
        //serialize/deserialize xml. Call webservices. 
        return new Response();
    }
}
//Operation_2 class seems like Operation_1 class...

Well, really there are many Operation classes: Operation_3,....Operation_N, maybe between 11 and 15 classes, but in this moment there are only Operation_1 and Operation_2. If I want to add Operation_3, then I must update the OperationService class adding a new method: Exec_Operation_3(Data data).

I see that all methods return the same type (Response) and receive as a parameter the same type (Data), also methods of Operation_1 and Operation_N will have the same signature, so I think I can rewrite the code this way: (Solution 1)

public class OperationService
{
    public Response Exec_Operation(Data data)
    {
        OperationCaller caller = new OperationCaller();
        return caller.ExecOperation(data);
    }
}
public class OperationCaller
{
    public Response ExecOperation(Data data)
    {
        IOperation unknownOperation = new UnknownOperation();
        IOperation operation_2 = new Operation_2(unknownOperation);
        IOperation operation_1 = new Operation_1(operation_2);        
        return operation_1.ExecOperation(data);
    }
}
public interface IOperation
{
    bool CanExecute(Data data);
    Response ExecOperation(Data data);
}
public class Operation_1:IOperation
{
    private IOperation _succesor;
    public Operation_1(IOperation succesor)
    {
        this._succesor = succesor;
    }
    public CanExecute(Data data)
    {
        return data.OperationType.equals("1");
    }
    public Response ExecOperation(Data data)
    {
        if(this.CanExecute)
        {
            //process data. So much code. Many instantiations.
            //Many references to other assemblies.
            //serialize/deserialize xml. Call webservices.
            return new Response();
        }
        else
        {
            return this._succesor.ExecOperation(data);
        }
    }
}
//Other IOperation implementations seems like this Operation_1.

If you see I'm applying CoR pattern, this way I will not have to modify the OperationService class when I add a new Operation_X class, I will have to update the OperationCaller class only.

But in this solution I only modify the class Operation_1 (and Operation_2,..., Operation_N) but this class have so much code that reading is a little difficult so I think that it's better to create another class to use CoR and create an instance of Operation_1,..,Operation_N in that class, like this: (Solution 2)

public class OperationCaller
{
    public Response ExecOperation(Data data)
    {
        IOperation unknownOperation = new CoRUnknownOperation();
        IOperation operation_2 = new CoROperation_2(unknownOperation);
        IOperation operation_1 = new CoROperation_1(operation_2);        
        return operation_1.ExecOperation(data);
    }
}
public interface IOperation
{
    bool CanExecute(Data data);
    Response ExecOperation(Data data);
}
public class CoROperation_1:IOperation
{
    private IOperation _succesor;
    public Operation_1(IOperation succesor)
    {
        this._succesor = succesor;
    }
    public CanExecute(Data data)
    {
        return data.OperationType.equals("1");
    }
    public Response ExecOperation(Data data)
    {
        return (this.CanExecute) ? new Operation_1().ExecOperation(data);
                                 : this._succesor.ExecOperation(data);
    }
}

In this last solution I have not modified Operation_1, I have only created a layer to decouple OperationService (it's the goal).

But I think that Operation_1 instance -in this last solution- or IOperation instances -in the first solution- are a bit heavy objects and OperationCaller creates many of them so I have a doubt about this, I don't know if this implementation/solution will have performance issues.

What do you think. I will have performance issues using CoR in this case? if not, What solution is better the first (Solution 1) or the last (Solution 2)?.

Divya Jain
  • 393
  • 1
  • 6
  • 22
AiApaec
  • 660
  • 1
  • 6
  • 12
  • 3
    Try it out, but more important, does it matter at this point? Usually clarity is more important than performance, as long as performance meet your requirements. – Jesper Fyhr Knudsen Jul 19 '14 at 19:20
  • Thanks @Arkain, you are right, clarity is more important and really at this point performance does not matter. – AiApaec Jul 19 '14 at 21:44

1 Answers1

0

I will have performance issues using CoR in this case?

Yes, there are performance issues when using Chain of Responsibility:

  1. Every operation in the chain is recreated for every data call
  2. Operation lookup time is proportional to operation chain length

These performance issues come from the fact that IOperation violates Single Responsibility Principle and couples operation lookup algorithm CanExecute to operation execution algorithm ExecOperation. Every time you want to do operation lookup you have to create execution algorithm.

In Solution 2 you decouple execution algorithm contained in Operation class from lookup algorithm by using a proxy class CoROperation but this introduces complexity.

In the example there is a clear discriminator OperationType that can be used for operation lookup.

The solution is to make a dictionary of operation constructors Func(of Data, IOperation) hashed by OperationType.

dim Dictionary as new Dictionary(of String, Func(of Data, IOperation))

Dictionary provides the lookup algorithm.

Operation constructor determines how operation is obtained and provides the ability to specify lifetime of the operation.

For example,

  • LightOperation1 is data independent and not heavy,
  • HeavyOperation2 is also data independent but heavy and should be created once needed,
  • InjectedOperation3 is also data independent and dependency injected
  • DataDependentOpartion4 is data dependent and must be recreated for new data
  • operation 5 delegates actual execution call to some service

then

class OperationCaller
    private readonly OperationDictionary as IDictionary(of String, Func(of Data, IOperation))

    public sub new(InjectedOperation3 as IOperation, OperationService as IOperationService)

        dim LightOperation1 as new LightOperation1
        dim HeavyOperation2 as new Lazy(of HeavyOperation2)(function() new HeavyOperation2)

        dim Dictionary as new Dictionary(of String, Func(of Data, IOperation)) from {
            {"1", function(Data) LightOperation1},
            {"2", function(Data) HeavyOperation2.Value},
            {"3", function(Data) InjectedOperation3},
            {"4", function(Data) new DataDependentOperation4(Data)},
            {"5", function(Data) new DelegateOperation(addressof OperationService.Exec_Operation_5)}
        }

        me.OperationDictionary = Dictionary
    end sub

    public function ExecOperation(Data as Data) as Response
        return OperationDictionary(Data.OperationType)(Data).ExecOperation(Data)
    end function
end class

Method ExecOperation of OperationCaller class finds an appropriate operation constructor by dictionary, obtains and executes operation.

Pros:

  1. Operation lookup time is constant and independent to operation count thanks to dictionary

  2. Very flexible way to control each operation lifetime provides mechanism to cache operations and / or postpone heavy operation creation

Cons:

  1. Lambda functions

  2. If there is no clear operation discriminator you have to use pattern matching instead of dictionary

Lightman
  • 1,078
  • 11
  • 22