The Facade Pattern
When you're working with a third party API, such as ESAPI, it's often convenient to wrap it behind a class (or set of classes), known as a facade. Your app benefits in at least the following ways:
It's easier to unit test.
It's more adaptive because its dependencies are isolated.
It's more readable because you define how you want to interact with the API.
In this blog post, I'll create a very simple facade (a class with one method) for calculating the mean dose of the active plan. In addition, I'll make the method asynchronous with the ability to report its progress and to be cancelled.
The example here is based on my previous post Create ESAPI Scripts That Don't Freeze the UI. I recommend that you read that post before reading this one. I've placed the entire example solution on GitHub: https://github.com/redcurry/EsapiAsyncExample, so I won't be including all of the code here—only the relevant pieces to the discussion.
Mean Dose Calculator
If you look at the example solution, it contains two projects. The EsapiAsyncExample project contains the Script class and other supporting classes. This project is very similar to the one I described in the previous post I mentioned, but I cleaned it up some more.
The EsapiAsyncExample.EsapiService project contains classes related to ESAPI. I like to separate anything ESAPI-related on its own project so as to ensure that any use of ESAPI occurs exclusively through the facade.
The IMeanDoseCalculator interface in the EsapiAsyncExample.EsapiService project defines the facade interface:
public interface IMeanDoseCalculator
{
Task<DoseValue> CalculateForActivePlanSetupAsync(
IProgress<double> progress, CancellationToken cancellation);
}
The return type is a generic Task, which allows this method to be called asynchronously. It takes a generic IProgress, used for reporting the progress, and a CancellationToken, used to cancel the calculation, if desired. I'll discuss these concepts below, but see this article for more details.
The implementation of this interface uses the EsapiWorker class that I described in the previous post I mentioned. In brief, it runs operations on the main thread (the one Eclipse is tied to), while the rest of the script operates on a separate thread. This prevents ESAPI operations from freezing the UI.
Here's the implementation of the method CalculateForActivePlanSetupAsync:
public Task<DoseValue> CalculateForActivePlanSetupAsync(
IProgress<double> progress, CancellationToken cancellation)
{
return _esapiWorker.RunAsync(scriptContext =>
{
var dose = scriptContext.PlanSetup.Dose;
var meanDose = new DoseValue(0.0, dose.DoseMax3D.Unit);
for (int z = 0; z < dose.ZSize; z++)
{
cancellation.ThrowIfCancellationRequested();
progress.Report((double)(z + 1) / dose.ZSize);
var buffer = new int[dose.XSize, dose.YSize];
dose.GetVoxels(z, buffer);
for (int x = 0; x < dose.XSize; x++)
for (int y = 0; y < dose.YSize; y++)
meanDose += dose.VoxelToDoseValue(buffer[x, y]);
}
return meanDose / (dose.XSize * dose.YSize * dose.ZSize);
});
}
There are three important elements here. First, the calculation is executed on the main thread using the EsapiWorker. Second, the cancellation object will throw an exception if cancellation is requested. Third, the calculation's progress is reported at each slice of the dose image (the progress amount is calculated based on the current slice).
The Main View Model
In order to use the mean dose calculator in our MainViewModel, we can hook up a button to a command that will call a private method that will then call the calculator method in the facade. Here's the private method that calls the facade's method:
private async void CalculateMeanDose()
{
_isCalculationInProgress = true;
var progress = new Progress<double>(UpdateProgress);
_cancellation = new CancellationTokenSource();
try
{
MeanDose = await _meanDoseCalculator
.CalculateForActivePlanSetupAsync(progress, _cancellation.Token);
}
catch (OperationCanceledException)
{
MeanDose = DoseValue.UndefinedDose();
}
ResetProgress();
_isCalculationInProgress = false;
}
Notice that it is an async method, which allows the caller (the command in the UI thread) to continue processing UI events without waiting for the method to finish. In this method, we create a Progress object, which calls the UpdateProgress delegate whenever we report the progress in the calculation method. The UpdateProgress delegate simply updates a property that's data-bound to a progress bar.
We also create a CancellationTokenSource and store it in a private field. When the user wants to cancel the calculation, the Cancel method on this field is called in another private method (I don't show this here, but it's in the example code). When Cancel is called on the CancellationTokenSource, it will cause the cancellation token in the calculation method to throw an exception, thereby cancelling the operation.
In order to handle this exception (of type OperationCanceledException), we put the calculation inside a try-catch block. We use the await keyword in order to let the caller (the UI) continue with handling events while the calculation proceeds. The result is saved into MeanDose, which is data-bound to a TextBlock.
If an OperationCanceledException occurs, we set the MeanDose to an "undefined" value.
Final Thoughts
This simple facade contains a single class and a single method, but of course a facade may comprise several classes, each with several methods. Every application will be different, so likely your application will need its own facade. But if the facade you're creating could be used for other applications, then you could put it into its own solution and turn it into a shared library.
You may have noticed that there isn't a complete separation of dependencies in this solution. The calculator method returns a DoseValue. We could have instead returned a simple double or our own Dose implementation, if we wanted complete isolation from ESAPI.
In this example, the calculation method didn't take any parameters other than those needed to report progress and handle cancellation. In other cases, however, the method may need to know more ESAPI-related information, such as the PlanSetup or Structure that the calculation should apply to. Instead of passing these ESAPI structures directly (which would cause your main project to manage these objects), you could pass in the IDs, which are simple strings.
Comments