Monday, November 5, 2007

Refactoring a method to the strategy pattern

An example from O`Reilly:

Refactoring to Strategy

The getRecommended() method presents several problems. First, it's long—long enough that comments have to explain its various parts. Short methods are easy to understand, seldom need explanation, and are usually preferable to long methods. In addition, the getRecommended() method chooses a strategy and then executes it; these are two different and separable functions. You can clean up this code by applying STRATEGY. To do so, you need to

  • Create an interface that defines the strategic operation

  • Implement the interface with classes that represent each strategy

  • Refactor the code to select and to use an instance of the right strategic class


Three steps involved.

Why is this important? Because, in turning this Rubik`s cube, we will make mistakes. We do not know exactly many parameters and many design decisions will have to be taken without them. Educated guesses are informing all cognitive models in the market. So if we know that we will make mistakes, we should be able to undo those mistakes easily. So here is a design principle for our architecture:

Whenever a design decision is based on an educated guess, the implementation should not be done through class methods, but with the strategy pattern.
I don`t know to what extent we will be able to follow this. That is precisely why we need to start getting used to the refactoring involved and master these three small steps.

We want to create a strategy pattern to deal with potentially different activation curves and potentially different decay curves. We are not sure of which is the best (i.e., psychologically plausible) curve, hence we want this to be easy to change. The easier to change, the higher the productivity. The higher the productivity, the most mistakes we can make without major backtracks. And mistakes we will make, lots of mistakes. On that you can be sure.

But the strategy pattern also gives us something enormously valuable. It gives us the ability to change code on the fly. It gives us the ability, for example, to change behavior should a global signal be received; we know from experience that our memory of intense moments is registered; while expected, boring, moments do not register as well. I used to believe that in the mind there was no space for global variables, or global events. I was wrong. A spike in adrenaline makes you alert, opens your eyes, raises your heart rate, raises your blood pressure, feeds your muscles with blood, preparing them for action, and halts numerous "background" processes, such as digestion. There are global signals, and behavior changes on the fly. (BTW, where in NUPIC does this happen?)

The strategy pattern gives us this ability. And this is something we do want. So we want to be masters at the craft of transforming refactoring a class method into a strategy pattern. So here we go. Here's our current class definition for activations:

Tactivation = class
private
current_state, level, increment: real;
Signals_Received: real;
procedure Recompute_Level;

public
Constructor Create;
procedure increase (step: real);
Function Get_Level:real;
function Get_CurrentState:real;
function Get_Increment: real;
Procedure Reset_Increment;
Procedure DeployChange;
procedure decay;
end;



Step 1. Creating an interface that defines the operation

(The syntax java it is easier on the eyes than in Delphi, so translating this code should be a piece of cake.)

Here we go:

IRecompute_Activation = Interface
function Recompute_Activation(Current_state: Real):Real;
end;


Compile and run. Nothing should have changed yet in functionality. No errors should appear.


Step 2. Implement the interface with classes that represent each strategy.

Let's have, for starters, a sigmoid curve, and a linear one (i.e., level=current_state).


TRecompute_Activation_Sigmoid = Class (TInterfacedObject, IRecompute_Activation)
function Recompute_Activation(Current_state: Real):Real;
end;

TRecompute_Activation_equals_state = Class (TInterfacedObject, IRecompute_Activation)
function Recompute_Activation(Current_state: Real):Real;
end;

Copy/Paste the previous method code (in the sigmoid case), without deleting the original.


function TRecompute_Activation_Sigmoid.Recompute_Activation(Current_State:Real):real;
var pyramid, sum, t:real; counter: integer;
begin
Sum:=0;
for counter:= 0 to floor (Current_State*max_Steps) do
begin
t:= counter/max_steps;
If(t<=0.5) then Pyramid:=t else pyramid :=1-t;
Sum:=(4*(1/max_steps)* Pyramid) + Sum;
end;
Result:= Sum;
end;

function TRecompute_Activation_equals_State. Recompute_Activation (Current_State:Real) :real;
begin
Result:= Current_State;
end;

Now compile and run. Nothing changed in functionality. Good.

Step 3. Refactor the code to select and to use an instance of the right strategic class.

NOW we're changing the original class. Five quick steps are involved here.

(3.1) First we need to include the strategy pattern object, named activation_strategy, then compile and run. No change. Good.

(3.2) Now on to include methods to set_activation_sigmoid, or set_activation_linear; hence we include in the TActivation Class:

Activation_Strategy: IActivation_Strategy;
Procedure set_activation_sigmoid;
Procedure set_activation_linear;

...and the respective methods on the Activation class which call the constructor of the desired strategy:

Procedure Tactivation.set_activation_sigmoid;
begin
Activation_Strategy:= TRecompute_Activation_Sigmoid.create;
end;

Procedure Tactivation.set_activation_linear;
begin
Activation_Strategy:= TRecompute_Activation_Linear.create;
end;

Compile and run. Nothing changed in functionality. Good.

(3.3) Kill the previous code, by commenting out the method and its declaration. Compile... and it doesn't run anymore! Great, because the compiler will point out to you all the previous calls made to the method, so just substitute them for your strategy. In our example, we substitute calls to Recompute_Level to the function Level:=Activation_Strategy. Recompute_Activation (Current_State).

Compile and run, and functionality should be restored!

(3.4) Now, test whether or not the whole strategy is working by changing the pattern at runtime. In my case this means including the following piece of code in the end of the DeployChange method: if Level>0.5 then set_activation_linear;

Compile and run. Now marvel at the runtime behavioral change.

(3.5) Finally, clean the code. Delete the (commented out) method calls and method implementation (and declaration).

Compile and run. Works like a charm. Congratulate yourself now.

Here's to the strategy pattern. This one is truly important. Everybody should master this technique. This MUST be trivial to do, anytime. Unless you are so overconfident to believe that you'll never get paralyzed because of a bad design decision taken ages ago.

Don't be.

0 comments: