Click here to Skip to main content
13,190,404 members (51,266 online)
Click here to Skip to main content
Add your own
alternative version

Stats

33.1K views
73 bookmarked
Posted 3 Jun 2016

Function Decorator Pattern - Reanimation of Functions

, 15 Jun 2016
Rate this:
Please Sign up or sign in to vote.
This is an alternative for "Interceptor in the Wild". The Function Decorator Pattern offers a way to inject new behaviors into existing methods without using IoC frameworks nor modifying method implementations.

Introduction

In Interceptor in the Wild by João Matos Silva has presented how to use an IoC framework, NInject, to inject new behaviors into existing methods without hacking into the method implementations. In this article, we will see an old-school yet beneficial way to reach the same goal.

Background

Days ago, when I was reviewing the code of my team members, I found a lot of boilerplate code, like this.

int retry = 0;
WebResponse r;
TRY:
try {
    r = webRequest.GetResponse();
}
catch (Exception ex) {
    if (++retry < 3) {
        goto TRY;
    }
    throw;
}

And this:

int retry = 0;
TRY:
try {
    r = socket.Send(bytes);
}
catch (Exception ex) {
    if (++retry < 3) {
        goto TRY;
    }
    throw;
}

My teammates were repeating themselves on recoverying after errors, error logging, and some other similar things. I recalled an article Interceptor in the Wild that I'd bookmarked months ago and read it again and again, wondering if IoC introduced in that article could be used to eliminate the above boilerplate code.

Notice:

The article Interceptor in the Wild by João Matos Silva was very well written and pleased to follow. This article reused examples in it. I suggest you read it before going on with this alternative.

After days of contemptation and introductions to my teammates, we refrained from adopting IoC frameworks for the following considerations.

What was our goal?

  1. The elimination of repeated code boilerplate.
  2. The separation of code responsibility.
  3. The flexible injection of new functionalities into existing methods.
  4. The underlying implementation of the injected method should remain unchanged.

What were we trying to avoid? Or what were preventing us from adopting the IoC framework solution?

  1. Added Code Complexity.
    • IoC frameworks usually hides the constructor of the types, which may be difficult for programmers to follow when the injection chains become complicated. "What instance is resolved by the IoC?" is one of the most common issue programmers will usually encounter.
    • It requires some efforts when we try to instantiate objects, if we are trying to call:
      • Constructors with parameters: relative discussions here and there. And this can usually causes problems when the programmers are refactoring the constructor, without knowing that it is somewhere referenced by the IoC module indirectly.
      • Private or internal constructors: relative discussion here and there.
      • Constructors which could throw exceptions: much harder to address the problem.
    • IoC usually requires the programmer to provide abstracted classes or interfaces for behavioral interceptors, thus more types have to be added to the project. Furthermore, by adding those interfaces or classes, some private scope code may accidently or inevitably be exposed, and herewith violates the minimal visibility design principles.
  2. Usage Restrictions:
    • An IoC binder affects all methods bounded to the type via the specific interface. Given that we have a type with three methods: M1, M2 and M3, and we are going to use all of them in the code. We want to bind behavior B1 to M1, B1 and B2 to M2, but no behavior to M3. Afterwards, we are going to call the bounded M1, bounded M2, and the unbounded M3. We need to write a lot code to achieve this with IoC frameworks. It is not so easy to understand or debug after that at all.
    • IoC binders only affect instance methods, not static methods nor private methods.
  3. Performance Palenty: The numbers from a basic performance benchmark discussed later in this article told me DON'T.

In order to reach the goal and avoid the problems, I introduced this old-school, low-tech Function Decorator pattern to my teams.

The Function Decorator Pattern

The Function Decorator pattern may trigger your memory of the Decorator Pattern which typically wraps an object.

Quote of the Decorator Pattern from Wiki:

In object-oriented programming, the decorator pattern (also known as Wrapper, an alternative naming shared with the Adapter pattern) is a design pattern that allows behavior to be added to an individual object, either statically or dynamically, without affecting the behavior of other objects from the same class.

The biggest difference of Function Decorator pattern compared with the classic decorator pattern is that it wraps an instance method or a static method, or a Lambda Expression, and it does not have class inheritance chains or explicit interface implementations. The following is an example of a Function Decorator. It can be as simple as the following lines:

public static Func<TArg, TResult> WaitAMinute<TArg, TResult>(this Func<TArg, TResult> func) {
    return (arg) => {
        // added functionality
        System.Threading.Thread.Sleep(TimeSpan.FromMinutes(1));
        // original functionality
        return func(arg);
    };
}

The Function Decorator is an extension method of Func(or Action). In short, it wraps a function with another function.

The Usage of Function Decorators

In this part, I will use the NativeClient example in Interceptor in the Wild to demonstrate how Function Decorators can be an alternative to IoC injections.

Decreasing Fault Rate / Increasing Success Rate

In the Silva's article Interceptor in the Wild, the NativeClient very well shows us a service which can be broken at any time, and the RetryInterceptor remedies this by retrying when something goes wrong. The same pattern can be implemented with a Function Decorator.

To demonstrate the result. Let's take a look back to the primitive client code:

const int TimesToInvoke = 1000;

static void Main(string[] args) {
    var counter = new StatsCounter();
    var client = new NaiveClient(counter);
    counter.Stopwatch.Start();
    for (var i = 0; i < TimesToInvoke; i++) {
        try {
            // this method fails about 3 times out of 10
            client.GetMyDate(DateTime.Today.AddDays(i % 30));
            counter.TotalSuccess++;
        }
        catch (Exception ex) {
            counter.TotalError++;
        }
    }
    counter.Stopwatch.Stop();
    counter.PrintStats();
    Console.WriteLine("Press any key to exit");
    Console.ReadKey();
}

In the above code, you don't need to care about how GetMyDate was implemented, since we will not touch it and we will just change the client side code which calls that function. Please just keep in mind that it was a broken function and it had a probability of 0.3 to throw exceptions.

The result of execution was, at no doubt, very bad. It took a long time to run and execution fail rates were as high as 325/1000. Here was a possible output.

Execution Time: 36.0658004 seconds
Total Executions: 1000
Execution Sucess: 675
Total Sucess: 675
Execution Fail: 325
Total Fail: 325

Now, we will introduce a new RetryIfFailed action interceptor to automatically retry the action after failures, and we can easily configure how many times we want to retry at run time by assigning value to the maxRetry parameter.

public static Func<TArg, TResult> RetryIfFailed<TArg, TResult>
                                  (this Func<TArg, TResult> func, int maxRetry) {
    return (arg) => {
        int t = 0;
        do {
            try {
                return func(arg);
            }
            catch (Exception) {
                if (++t > maxRetry) {
                    throw;
                }
            }
        } while (true);
    };
}

Then, we apply it to the calling procedure.

var counter = new StatsCounter();
var client = new NaiveClient(counter);
counter.Stopwatch.Start();
// get the method we are going to retry with
Func<DateTime, string> getMyDate = client.GetMyDate;
// intercept it with RetryIfFailed interceptor, which retries once at most
getMyDate = getMyDate.RetryIfFailed(1);
for (var i = 0; i < TimesToInvoke; i++) {
    try {
        // call the intercepted method instead of client.GetMyDate
        getMyDate(DateTime.Today.AddDays(i % 30));
        counter.TotalSuccess++;
    }
    catch (Exception ex) {
        counter.TotalError++;
    }
}
counter.Stopwatch.Stop();

The above program gave a better result. The execution fault rate dropped from the original 325/1000 to 91/1000, simply because we have retried once more.

Execution Time: 59.6016893 seconds
Total Executions: 1302
Execution Sucess: 909
Total Sucess: 909
Execution Fail: 393
Total Fail: 91

If you are not happy with the result, you can tweak the maxRetry parameter in the interceptor to a larger value, for instance 3. Here was the result of "getMyDate.RetryIfFailed(3)" and the fault rate was reduced to around 8/1000.

Execution Time: 65.8972493 seconds
Total Executions: 1421
Execution Sucess: 992
Total Sucess: 992
Execution Fail: 429
Total Fail: 8

Caching the Result

In Interceptor in the Wild, it was also demonstrated how caching can be injected to methods via a PoorMansCacheProvider and a CacheInterceptor. The same result can be achieved with the Function Decorator pattern.

public static Func<TArg, TResult> GetOrCache<TArg, TResult, TCache>
                                  (this Func<TArg, TResult> func, TCache cache)
    where TCache : class, IDictionary<TArg, TResult> {
    return (arg) => {
        TResult value;
        if (cache.TryGetValue(arg, out value)) {
            return value;
        }
        value = func(arg);
        cache.Add(arg, value);
        return value;
    };
}

We can easily apply it to the calling procedure, after the RetryIfFailed interceptor.

var counter = new StatsCounter();
var client = new NaiveClient(counter);
// create a cache
var cache = new Dictionary<DateTime, string> ();
counter.Stopwatch.Start();
Func<DateTime, string> getMyDate = client.GetMyDate;
// apply the cache interceptor
getMyDate = getMyDate.RetryIfFailed(3).GetOrCache(cache);
for (var i = 0; i < TimesToInvoke; i++) {
    try {
        getMyDate(DateTime.Today.AddDays(i % 30));
        counter.TotalSuccess++;
    }
    catch (Exception ex) {
        counter.TotalError++;
    }
}
counter.Stopwatch.Stop();
Note:

Although GetOrCache is appended after RetryIfFailed, since it is actually wrapping around the original function, the cache provided to GetOrCache will be firstly accessed before RetryIfFailed is call.

In short, the later attached methods will be called earlier.

The result may look like the following, similar to the final result in Interceptor in the Wild. Since cache was in the play, dramatically the execution time was shortened and the total execution fault rate was reduced to around zero.

Execution Time: 0.981474 seconds
Total Executions: 36
Execution Sucess: 30
Total Sucess: 1000
Execution Fail: 6
Total Fail: 0

Uptil now, what we have done is simply adding two extension methods and changing 4 lines of code in the calling procedure.

  1. No need to modify the implemetation of the underlying function.
  2. No need to NuGet a single bit from the Internet.
  3. No need to learn a single new framework.
  4. No need to struggle with object instantiation.
  5. No need to think about how to alter parameters in interceptors.

Jobs got done and our days were saved.

Points of Interest - Performance

On performance, I enclosed a simple benchmark which compared the speed of Function Decorator and NInject (an IoC framework) Interceptor in the code.

The code will call a NopClient, which merely does nothing, and then compare the time spent on different types of patterns.

public class NopClient : INaiveClient {
    private StatsCounter _counter;

    public NopClient(StatsCounter counter) {
        _counter = counter;
    }

    public string GetMyDate(DateTime date) {
        return null;
    }
}

The setup code of NInject interceptor looks like the following:

public class Module : NinjectModule
{
    public override void Load() {
        Kernel.Bind<StatsCounter>().ToConstant(new StatsCounter());
        var binding = Kernel.Bind<INaiveClient>().To<NopClient>();
        binding.Intercept().With<RetryInterceptor>();
    }
}

During practical development, we typically create client objects which are not reusable and call their methods. For instance, we create WebRequest objects to load a web page or DbConnection objects to manage the database. Hence, in performance benchmarks, we must take object initialization into account.

We are going to benchmark the following three groups of operations.

1, The hard coded procedure.

for (var i = 0; i < TimesToInvoke; i++) {
    var nopClient = new NopClient(counter); // instantiation 
    int t = 0;
    do {
        try {
            nopClient.GetMyDate(DateTime.MinValue);
            break;
        }
        catch (Exception) {
            if (++t > 3) {
                throw;
            }
        }
    } while (true);
}

2, The fuction decorator.

for (var i = 0; i < TimesToInvoke; i++) {
    var nopClient = new NopClient(counter);  // instantiation 
    Func<DateTime, string> getMyDate = nopClient.GetMyDate; // get the function pointer
    getMyDate = getMyDate.RetryIfFailed(3); // setup the decorator, end of instantiation part
    getMyDate(DateTime.MinValue);
}

3, The NInject interceptor.

for (var i = 0; i < TimesToInvoke; i++) {
    var client = kernel.Get<INaiveClient>(); // instantiation, setting up of the interceptor
    client.GetMyDate(DateTime.MinValue);
}

The following was a result taken from my computer. Function Decorator although was about 10 times slower than the hard coded, but was about hundreds of times faster than the NInject Interceptor.

[__strong__]Hard coded Execution Time: 0.0377 milliseconds
Function decorator Execution Time: 0.4576 milliseconds
NInject interceptor Execution Time: 109.8437 milliseconds

In some cases, the instance in which the function was decorated can be reusable, thereby, we should not include the instantiation in the benchmark. Having the instantiation parts moved outside of the loop of benchmark, the result on my computer was like the following:

[__strong__]Hard coded Execution Time: 0.0093 milliseconds
Function decorator Execution Time: 0.1898 milliseconds
NInject interceptor Execution Time: 30.0647 milliseconds

Choosing between Function Decorators and IoC Interceptors

Note:

Actually I was not very sure whether this chapter should be included in this article. For those two patterns having similar effects, I myself did make the comparison in my team before writing this article. I guessed it might still be useful for someone else.

The Function Decorator pattern has the following features:

  1. It does inject new behaviors to methods we want to change as IoC does (introduced in the Interceptor in the Wild): increasing execution success rates, providing cachability and efficiency, and more. It brings new vividity to closed functions.
  2. The underlying basic implementation remains unchanged. It does not require you to change a single character in the injected method.

The difference of changes made to the original code:

  1. Function decorator:
    • Does not change the initialization of objects (you can still use IoC framework or other abstract factory pattern if needed).
    • Does not change decorated objects (no interface or base type is needed to be exposed).
    • Changes the part of method calls where new behaviors should be injected.
  2. IoC interceptor:
    • Changes object initialization.
    • The decorated objects may need to expose new interface or base type for injecting new behaviors.
    • Does not change the part of method calls.

The Function Decorator pattern is more preferrable over the IoC Interceptor pattern if the following aspects are important:

  1. Control over the construction logic of types. The old-school new className(parameters) fashion to instantiate objects or kinds of abstract factory pattern can be used when needed. The object creation logic remains clean, simple and fully in control without IoC frameworks.
  2. Performance and scalablity.
  3. Low educational costs (it only took about 10 minutes for my teammates and they got it).
  4. Selectively injecting new behaviors to existing methods.
  5. Keeping types in your project simple, not willing to manually create new classes or interfaces simply for injection.
  6. Alternatively switching between injected and uninjected versions of methods.
  7. Injecting new behaviors to previously injected methods at run time.
  8. Injecting new behaviors to static methods, private methods or even Lambda expressions.
  9. Simple deployment. Less reference to third party assemblies.
  10. Ease of debugging.
  11. Refactoring constructors with IDE.

Side notes for the Function Decorator:

  1. It is not object wide. But you may consider passing the wrapped object to the decorator method besinds the decorated function.
  2. It does not wrap properties.
  3. It does not inject behaviors to all methods to a type by default. You must manually apply it to methods, one by one.

Acknowledgements

  1. The major gratitude must be attributed to the author of the article Interceptor in the Wild by . The thought and examples in this article are inspired from his well written demonstration. And he also gave me profound insights on the rewrite of this article.
  2. Readers of this article,  wim4you, Mr. Javaman, etc. have given great encouragement and valuable suggestions.

History

  • 2016-6-10: Rewrote and retitled the article. Supplied acknowledgements.
  • 2016-6-4: Initial post (titled the Action Interceptor Pattern)

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

wmjordan
Team Leader
China China
Chinese Poetry Advocator.
Programmer.

You may also be interested in...

Comments and Discussions

 
GeneralMy vote of 5 Pin
Vincent Maverick Durano20-Jul-16 6:02
professionalVincent Maverick Durano20-Jul-16 6:02 
GeneralRe: My vote of 5 Pin
wmjordan20-Jul-16 17:52
memberwmjordan20-Jul-16 17:52 
SuggestionInteresting approach ! Pin
Fitim Skenderi18-Jun-16 2:06
professionalFitim Skenderi18-Jun-16 2:06 
GeneralRe: Interesting approach ! Pin
wmjordan20-Jul-16 17:51
memberwmjordan20-Jul-16 17:51 
QuestionLittle West Street Pin
Samridhi Ganeriwala15-Jun-16 21:04
groupSamridhi Ganeriwala15-Jun-16 21:04 
SuggestionConstructor overhead Pin
ArchAngel12315-Jun-16 8:55
memberArchAngel12315-Jun-16 8:55 
GeneralRe: Constructor overhead Pin
wmjordan15-Jun-16 13:20
memberwmjordan15-Jun-16 13:20 
GeneralRe: Constructor overhead Pin
ArchAngel12315-Jun-16 14:32
memberArchAngel12315-Jun-16 14:32 
PraiseRe: Constructor overhead Pin
wmjordan15-Jun-16 14:46
memberwmjordan15-Jun-16 14:46 
SuggestionCan you compare performance of handwritten decorators, injecters, AOP frameworks? Pin
SergioRykov15-Jun-16 0:28
memberSergioRykov15-Jun-16 0:28 
GeneralRe: Can you compare performance of handwritten decorators, injecters, AOP frameworks? Pin
wmjordan15-Jun-16 3:34
memberwmjordan15-Jun-16 3:34 
GeneralRe: Can you compare performance of handwritten decorators, injecters, AOP frameworks? Pin
wmjordan15-Jun-16 13:22
memberwmjordan15-Jun-16 13:22 
QuestionI'm not sure this will replace IoC as my primary tool Pin
Pete O'Hanlon14-Jun-16 22:06
protectorPete O'Hanlon14-Jun-16 22:06 
AnswerRe: I'm not sure this will replace IoC as my primary tool Pin
SergioRykov15-Jun-16 0:06
memberSergioRykov15-Jun-16 0:06 
Question很棒 Pin
fengzijun14-Jun-16 17:33
memberfengzijun14-Jun-16 17:33 
AnswerRe: 很棒 Pin
wmjordan20-Jul-16 17:51
memberwmjordan20-Jul-16 17:51 
QuestionGreat ! Pin
PierreTessier14-Jun-16 5:19
memberPierreTessier14-Jun-16 5:19 
PraiseWell Done Pin
Aoi Karasu 10-Jun-16 2:34
professional Aoi Karasu 10-Jun-16 2:34 
QuestionExcellent! Just Brilliant! Pin
Mr. Javaman9-Jun-16 19:00
memberMr. Javaman9-Jun-16 19:00 
AnswerRe: Excellent! Just Brilliant! Pin
wmjordan10-Jun-16 0:16
memberwmjordan10-Jun-16 0:16 
QuestionWOW! Pin
Mr. xieguigang 谢桂纲9-Jun-16 3:31
professionalMr. xieguigang 谢桂纲9-Jun-16 3:31 
AnswerRe: WOW! Pin
wmjordan10-Jun-16 0:17
memberwmjordan10-Jun-16 0:17 
Praisewow! Pin
wim4you6-Jun-16 0:30
memberwim4you6-Jun-16 0:30 
GeneralRe: wow! Pin
wmjordan6-Jun-16 20:30
memberwmjordan6-Jun-16 20:30 
GeneralRe: wow! Highlight extension method for functions? Pin
wim4you7-Jun-16 0:17
memberwim4you7-Jun-16 0:17 
GeneralRe: wow! Highlight extension method for functions? Pin
wmjordan7-Jun-16 4:08
memberwmjordan7-Jun-16 4:08 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

Permalink | Advertise | Privacy | Terms of Use | Mobile
Web02 | 2.8.171016.2 | Last Updated 16 Jun 2016
Article Copyright 2016 by wmjordan
Everything else Copyright © CodeProject, 1999-2017
Layout: fixed | fluid