Yesterday, I was discussing multi-step algorithms in which each step depended on the successful completion of all of the steps before it. I had put some thoughts down about some sort of ColdFusion custom tag system that would allow you to chain these kinds of logical steps together. In the comments to my post, Bradley Moore suggested that I use ColdFusion exceptions to handle the flow of the algorithm. To be honest, this thought had never even crossed my mind; I rarely use manually thrown exceptions - plus, I have heard a lot of people hate on the use of exceptions to do anything even slightly sneaky. I don't really know enough about the theory of it all to say whether or not exception usage is good or bad, so I figured I would try it out and see how it felt.
In this example, we are going to use a file upload page again. I like this kind of scenario because I feel that it is a very common one in the world of web-based business software, especially for any kind of application that allows users to upload data files that need to be parsed and processed in some way (think contact relations management, think CSV files, think bulk action modules). In the demo below, the algorithm uses 5 steps:
- Upload file
- Check file type (for CSV)
- Read in file data
- Process file data (sum all values)
- Direct user to confirmation page
That last step isn't really a part of the algorithm, but I listed it since it should only execute if steps 1-4 processed properly.
Ok, so here is my attempt to follow Bradley Moore's lead on exception-driven multi-step dependent algorithms:
<!--- Param form variables. ---> <cfparam name="FORM.file" type="string" default="" /> <!--- Create an array to hold the errors. ---> <cfset REQUEST.Errors =  /> <!--- Check to see if the form was uploaded. ---> <cfif Len( FORM.file )> <!--- Put a try/catch around the entire multi-step process. Each sub-step will have the potential to throw an exception to hault the algorithm from continuing. ---> <cftry> <!--- STEP 1: Upload file. ------------------------ ---> <cftry> <cffile action="upload" filefield="file" destination="#ExpandPath( './' )#" nameconflict="makeunique" /> <cfcatch> <!--- Set error. ---> <cfset ArrayAppend( REQUEST.Errors, "There was a problem uploading the file." ) /> <!--- Rethrow error to hault algorithm. ---> <cfrethrow /> </cfcatch> </cftry> <!--- STEP 2: Check file type. -------------------- ---> <cfif (ListLast( CFFILE.ServerFile, "." ) NEQ "csv")> <!--- Set error. ---> <cfset ArrayAppend( REQUEST.Errors, "Only CSV files are allowed." ) /> <!--- Throw error to hault algorithm. ---> <cfthrow type="File.InvalidType" message="Invalid file type" detail="You have uploaded an invalid file type. Only CSV files are allowed." /> </cfif> <!--- STEP 3: Read in file. ----------------------- ---> <cftry> <cffile action="read" file="#ExpandPath( './#CFFILE.ServerFile#' )#" variable="strFileData" /> <cfcatch> <!--- Set error. ---> <cfset ArrayAppend( REQUEST.Errors, "There was a problem reading your file." ) /> <!--- Rethrow error to hault algorithm. ---> <cfrethrow /> </cfcatch> </cftry> <!--- STEP 4: Process data. ----------------------- ---> <cftry> <!--- Get initial sum. ---> <cfset flSum = 0.0 /> <!--- Loop over records in file. ---> <cfloop index="strLine" list="#strFileData#" delimiters="#Chr( 13 )##Chr( 10 )#"> <!--- Do something with the data that might throw an error; for example, interacting with a database. For our example, we are going to assume that each line contains a NUMERIC value that we are going to add to our sum. ---> <cfset flSum += strLine /> </cfloop> <cfcatch> <!--- Set error. ---> <cfset ArrayAppend( REQUEST.Errors, "There was a problem processing the uploaded data. The value [#strLine#] could not be converted to a number. Please make sure that it is a valid CSV file." ) /> <!--- Rethrow error to hault algorithm. ---> <cfrethrow /> </cfcatch> </cftry> <!--- ASSERT: If we have made it this far without any errors being thrown, then our algorithm has completed successfully. However, if any of the above steps threw an error, this area will be bypassed and the following CFCATCH will be executed. ---> <!--- Redirect to confirmation page. ---> <cflocation url="#CGI.script_name#?success" addtoken="false" /> <!--- ----------------------------------------- ---> <!--- Handle any errors that were thrown above. ---> <!--- ----------------------------------------- ---> <cfcatch> <!--- One of the steps went wrong. Check to see if we have an error message. If we don't then an uncaught exception occurred. ---> <cfif NOT ArrayLen( REQUEST.Errors )> <!--- Set error. ---> <cfset ArrayAppend( REQUEST.Errors, "An unkown error has occurred (#CFCATCH.Message#)." ) /> </cfif> <!--- ASSERT: Since our algorithm didn't execute fully, the server might have misc. garbage on it. Try to clean up the system as best as we can. ---> <!--- Try to delete uploaded file. ---> <cftry> <cffile action="delete" file="#ExpandPath( './#CFFILE.ServerFile#' )#" /> <cfcatch> <!--- File could not be deleted. ---> </cfcatch> </cftry> </cfcatch> </cftry> </cfif> <cfoutput> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html> <head> <title>Dependent Algorithm Steps</title> </head> <body> <h1> Dependent Algorithm Steps </h1> <!--- Check to see if we have a success. ---> <cfif StructKeyExists( URL, "success" )> <p> <em>File upload was successful!</em> </p> </cfif> <!--- Check to see if there were any errors. ---> <cfif ArrayLen( REQUEST.Errors )> <h4> Please review the following: </h4> <ul> <cfloop index="strError" array="#REQUEST.Errors#"> <li> #strError# </li> </cfloop> </ul> </cfif> <form action="#CGI.script_name#" method="post" enctype="multipart/form-data"> <input type="file" name="file" size="60" /><br /> <br /> <input type="submit" value="Process File" /> </form> </body> </html> </cfoutput>
As you can see, the entire algorithm is wrapped in a CFTry / CFCatch block. This will handle any uncaught exceptions as well as any exceptions explicitly thrown by the individual steps of the process. Then, each step of the algorithm is wrapped in its own CFTry / CFCatch block to handle low-level exceptions that might be generated. If any of the sub-steps generate an exception, an error is stored and the error is rethrown; this will prevent the "confirmation" code from executing inappropriately.
I have to say, I really liked this methodology. I love the fact that all the steps were on the same tab level (each step was tabbed in the same amount from the page gutter) making the algorithm very easy to follow from a visual standpoint. I know most people couldn't give to hoots about that, but that sort of stuff is hugely important to me. I also felt like the logic of the process was very easy to read and follow. Plus, I like that the parent CFCatch block provided a centralized place to clean up the server for any miscellaneous garbage that was produced as a side-product of the algorithm; this way, you are not muddling up the individual steps with sanitzation code - you are leaving all the steps intent-driven.
I know that a lot of people don't feel that exceptions should be used in this way; and I would tend to agree, not so much based on education, but more on gut feelings; however, after having written up this exercise, my thoughts on the topic are definitely changing. Something about it felt very good. Plus, after having read what Sean Corfield said about inexpensive exceptions in Java (ColdFusion), I have to say I am even more inclined to like this methodology.
I'd look at the Chain of Responsibility pattern, since that is exactly what it is meant for. http://en.wikipedia.org/wiki/Chain-of-responsibility_pattern
Someone suggested the Chain of Responsibility pattern to me the other day and it does look very interesting. My concern is that it is not aligned with the procedural nature of my code and therefore, I probably would not be able to leverage it in a good way.
For example, one step performs a file upload which results in a CFFILE variable existence which is utilized by subsequent steps. If the business logic was encapsulated in functions, how could I handle variable creation and usage across steps?
If you can give me some insight on that, I might be able to use this better.
Couldn't you just pass the necessary data (whatever is needed out of the cffile variable) into the next step?
For fun, I added a CF version of the COR pattern to the Wikipedia article.
I suppose you could. But, then, I would have to pass in the error array AND return it (since the array is passed by value). Which means, I then have to return the error array and the CFFILE variables. Which means I have to now return some struct that contains those values.
I guess I could come up with a common return structure, something like:
. . . Errors: Array,
. . . Data: Struct
But then, on top of that, I have to create the CFCs and put them in a meaningful place.
This just starts to feel like a lot of overhead for something that will be a one-off piece of functionality.
Do you use this pattern a lot? Have you felt the benefits of it?
Yeah that's when you probably want to package up the necessary data into a common format as a CFC instance, that can be passed to each object in the chain.
I think you might be reaching about "having to" create CFCs and put them in the proper place. ;-) You know that would take all of 10 minutes.
I suppose it might be considered a lot of effort, though I think having it neatly encapsulated and ready for changes is a compelling point. In 6 months it would probably be easier and quicker to add a new item to the chain or to modify one item in the chain instead of having to deal with the large code block and the worry that a change in one spot breaks something further down.
I don't use it often but I have used it and found it helpful. Of course, there's no need to over-engineer things. But it is something to keep in mind. You might also keep an eye on performance, since throwing exceptions is a farily expensive operation.
I don't want you to get the wrong idea - I love CFCs and I love trying to create objects that have most excellent encapsulation and reusability. My gripe here is that this is not really reusable - it's for a given form post. Yes, you could abstract it out into some generic chain for certain aspects and then maybe override it with CFC's that have form-specific actions, but that is probably gonna be over kill.
I am not sure that this is more usable / maintainable than the CFTry / CFCatch method. In the long run, they are both long files that have to be read; the difference is that one of the long files is inbetween CFComponent tags.
I want to like the CFC idea, because as I said, I really like CFCs in general, but I am just not sure I feel it for this situation; of course, if it wasn't useful, people wouldn't have created a "design pattern" for it.... just struggling to wrap my head around it.