Building the Basics: Towards a Better Task
Posted by Craig Sutherland on 16 June, 2009
It’s All Changing!
I was chatting with one of the other developers on the CruiseControl.NET project yesterday. During the chat he mentioned he was concerned about how much things have changed from 1.4.4 to 1.5.0, and that he was having a hard time keeping up with all the changes.
So, in this post I’ll cover one of the areas that has recently changed – TaskBase.
Warning: This is a CruiseControl.NET development topic. If you don’t care about developing tasks for CruiseControl.NET, then this post is not for you!!!
Some History
In CruiseControl.NET up to version 1.4.3, a task could only be asked to do one thing – run. You could not ask it to do anything else. This lead to a number of assumptions and limitations. As an example, it was assumed that if NetReflector could load the task from the XML, it was valid. Another example, if a task completed without an exception, then whatever the status of the result would be the status of the task (definitely not a valid assumption for publishers!)
Basically, there was no way for the system to interact with a task beyond asking it to run.
Now, this is because a task must implement ITask. This interface has only one method, Run(), so the system is limited to calling this method.
So, how then can the functionality of a task be expanded? The answer was to add additional, optional interfaces to a task. The system checks to see if these interfaces are present when it needs them, and then calls the methods on those interfaces. This allowed tasks to expose new functionality, without breaking previous tasks.
An example of this is IConfigurationValidation. This was a new interface added in 1.4.4 that would allow a task to be validated after it has loaded. After the configuration was loaded, the configuration would be checked to see if any of the loaded classes had this interface. If so, it would be called, and the class could validate itself. Nice and simple.
An Explosion of Interfaces
In 1.5.0, we added a couple more interfaces to allow even more system/task interaction. These interfaces are IParamatisedTask and IStatusSnapshotGenerator. IParamatisedTask allows dynamic build parameters (i.e. values that the user can select when the task is built, as opposed to be hard-coded in the configuration) and IStatusSnapshotGenerator allowed for individual task statuses (prior to this, you could only see the status of the overall build).
If task now wants to validate itself, accept parameters or generate a status item, all it has to do is implement the required interface.
But, this is where it started to get messy!
Nearly every task in the standard build would like to have parameters, plus people like seeing what is happening inside their builds. And most of the time, this functionality is the same, no matter which task. As you can guess, this would lead to a lot of duplicated code (never a good thing).
A New Base
There are a couple of approaches we could use to minimise the duplicate. We could add a helper class that handles the duplicate code, but then we’d still need to add the interfaces and methods to all of the tasks. An alternate approach is to make an abstract base class that tasks can inherit from.
So, as you might have guessed, I went with the abstract base class approach and added a new class called TaskBase.
This class implements ITask, IParamatisedTask and IStatusSnapshotGenerator. There are default implementations of each of the interfaces. These implementations are virtual, so they can be overridden.
Additionally, TaskBase provides a number of other common items. It exposes Description and Name properties (Description comes from the config, Name defaults to the class name). It includes a default skeleton for the Run() method, the actual functionality must now be implemented in the Execute() method.
Finally, it adds a method called PreprocessParameters(). This method uses a recent extension to NetReflector that allows the XML of a class to be pre-processed before it is loaded by NetReflector. This allows us to convert $[…] blocks within a property into dynamic value definitions – thus providing a simpler way to configure tasks.
Multi-task Tasks
We have actually added two new base task types to CruiseControl.NET – TaskBase and TaskContainerBase. TaskContainerBase is a further specialisation of TaskBase. Given that we have TaskBase, why do we also need TaskContainerBase?
In CruiseControl.NET we have added some task-containing tasks. These tasks contain other tasks (child tasks). Again, these tasks have a number of common functionality areas. All of these tasks need to validate (just in case any of their children require validation), they need to handle parameters (same reason) and also need to generate status.
So TaskContainerBase was born.
The functionality that this class provides is running child tasks (including applying parameters), validation and status generation. It is up to the actual task implementations to define the order of running the tasks. For example, ParallelTask runs all the tasks at the same time (using threads) while SequentialTask runs them one after the other.
Finally
Hopefully this shows how useful the new TaskBase and TaskContainerBase classes can be. We don’t have to worry about breaking existing 3rd party tasks by changing the underlying interface, and at the same time we’re not duplicating lots of code.
If you are developing tasks for CruiseControl.NET, you can continue to use ITask, with or without the extra interfaces. However, if you’re feeling lazy, TaskBase and TaskContainerBase will save some time, plus ensure your tasks utilises the new functionality we will be adding in future.
RSS - Posts