Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
I am the chief technical officer at InVision App, Inc - a prototyping and collaboration platform for designers, built by designers. I also rock out in JavaScript and ColdFusion 24x7.
Meanwhile on Twitter
Loading latest tweet...
Ben Nadel at cf.Objective() 2013 (Bloomington, MN) with: Ben Koshy

Separate Asynchronous Thread Logic From Your Business Logic

By Ben Nadel on
Tags: ColdFusion

Over the weekend, I had some time to kill. And, as it does often, my mind drifted into the wonderful world of programming. I started to think about some recent problems that I've run into with CFThread in ColdFusion. The problem wasn't with the way ColdFusion implements threads; but, rather, with the way that I was using them. I think, traditionally, my threading logic and my business logic have been very tightly coupled. This makes the code less flexible and harder to test. I think, going forward, I'm going to try to make sure that my threading logic is completely separated from my business logic.

In the long run, I don't know just how far removed the threading logic should be from the business logic; but, I do know that having them intertwined has caused problems. To start with, I think I can simply create two different versions of a function: the Synchronous one that contains all the business logic and the Asynchronous one that handles the threading around the invocation of the Synchronous version.

By having two top level component methods for Synchronous and Asynchronous invocation, it means that I can more easily isolate, test, and debug the business logic. It also means that I can choose to call the same logic synchronously if I feel that the situation requires it (as it usually does at some point in the life of the application).

To see what I'm talking about, I've put together a sample ColdFusion component that has a synchronous method - doSomething() - and an asynchronous method - doSomethingAsync() - that invokes doSomething() inside of a ColdFusion thread.

  • <!--- NOTE: CFScript tag for Gist color coding only. --->
  • <cfscript>
  •  
  • component
  • output = false
  • hint = "I do some stuff asynchronously, like a boss."
  • {
  •  
  • // I am the normal, synchronous, isolated version of the method.
  • // I can be easily tested without threading. Inputs, outputs, and
  • // raised exceptions can all easily be tracked.
  • public boolean function doSomething(
  • required numeric id,
  • required string value
  • ) {
  •  
  • // ---
  • // Do something business-logic-related...
  • // ---
  •  
  • return( true );
  •  
  • }
  •  
  •  
  • // I am the asynchronous version of the method, which will be
  • // spawned in a separate ColdFusion thread. This keeps all the
  • // thread-relevant code in one place, separate from the business
  • // logic of the target method.
  • public void function doSomethingAsync(
  • required numeric id,
  • required string value
  • ) {
  •  
  • thread
  • action = "run"
  • name = "doSomethingAsync-#createUUID()#"
  • id = id
  • value = value
  • {
  •  
  • try {
  •  
  • // It can be nice to store the result in the TRHEAD
  • // scope as well, for debugging. This way, the result
  • // will show up in a CFDump of the CFThread scope.
  • var result = thread.result = doSomething( id, value );
  •  
  • // Do something with result. Since we can't "return"
  • // value from a thread (though we can expose values
  • // in the "thread" scope), we may want to record the
  • // result in some way.
  • // --
  • // logResult( result );
  •  
  • } catch ( any error ) {
  •  
  • // Log error since it will not present as an error in
  • // the primary page request control flow. It will
  • // simply die quietly.
  • // --
  • // logException( error );
  •  
  • // It can be nice to store the error in the THREAD
  • // scope as well, for debugging. By rethrowing the
  • // error, it will be stored in the native "error"
  • // property on the THREAD scope. You cannot manually
  • // overwrite this, but rethrow will.
  • rethrow;
  •  
  • }
  •  
  • } // END: Thread.
  •  
  • }
  •  
  • }
  •  
  • </cfscript>

The doSomething() method doesn't really show much logic in this example; but, the idea would be that this method would run as if it doesn't know anything about threading. Then, it is up to the doSomethingAsync() method to do the threading. This way, the core business logic is contained entirely within doSomething(); and, the doSomethingAsync() does nothing but handle the threading, including managing error handling.

Right now, these two responsibilities are inside the same component. I suppose you could further separate them by leaving the one component synchronous and the creating a separate "wrapper" or proxy component that handles the threading. This would truly isolate the responsibilities and allow for specific types of dependency injection; but, perhaps at a level of complexity that is unnecessary? Mostly likely, the pros and cons will depend on the situation.




Reader Comments

I've always loved the phrase "As simple as possible, but as complex as necessary."

In our never-ending quest to profile every render and maximize performance, I would think that keeping sync'd and async'd code separate would be ideal on the face of it, however...

I wouldn't go so far as to move the async'd method into a different file, especially if the nature of their work is similar. At a certain point, separating logic into more files turns the octopus into a squid. It's one thing if you were passing off information to a method that managed thread creation, but I'd personally keep them all wrapped up together.

Reply to this Comment

This is actually very similar to the way I handle threading in my apps. I am a strong believer in not implementing logic inside a thread block but rather separating it out into an independent function exactly this way. Like you said this makes testing much easier.

Where I differ is that I don't separate out threaded functions into different files. I feel it adds an unnecessary layer which gives no benefit especially when debugging logic later.

To be clear your threading logic *is* part of the business logic. What you are doing here is simply breaking it down into smaller more manageable chucks, but both functions are implementing parts of your overall business logic.

Move over. What if down the line you discover that threading is not the best approach for a particular business function?

The way I approach this is to first create my business logic without any threading. So I would create function "doSomething" and use that directly. (Don't optimize too soon)

Then if I find need threading I copy this to a private Function in the component "doSomethingPrivate" and rewrite the first function to use threading. This way I don't have to go back and change any references to "doSomething" in the rest of my code and if I find out that threading is not any better I can reverse it quickly and invisibly to the rest of my code.

Reply to this Comment

@Aaron,

I tend to agree. That's why I started with simple separation of concerns within the same component. Off the cuff, the separate component stuff feels more appropriate when you need to augment an existing component with a new set of behaviors. For example, taking a "queue" component and then wrapping it in another component that add "synchronization" with additional locking. That way, you have your Queue that does nothing but worry about queuing... and then a different component that creates a synchronized queue.

But, that's just off the top of my head. I definitely like keeping it simple when possible.

Reply to this Comment

@Steven,

I definitely optimize too fast in certain cases. I just get in my head that action XYZ "needs" to be asynchronous. I like your approach where you keep the name the same and then refactor the internal wiring. Very interesting!

Reply to this Comment

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.