Within the past few years the proliferation of Agile Best Practices has pushed the importance of refactoring front and center in the world of Object Oriented Software Design, yet for some odd reason build scripts seem to have been overlooked in this regard by many. Perhaps this is due to the risk and complexity involved in such an effort as well as the lack of a tools by which refactoring build scripts can safely be accomplished.
For instance, whereas refactoring in typical OO languages relies heavily on Unit Tests for ensuring refactorings do not break existing code along the way, build scripts do not have such safety nets as Unit Tests. Ant is statically typed however it doesn’t provide compile time type checking, additionally build scripts are defined declaratively via XML mark-up however they can not be validated as there are not fixed DTD attributes to validate them against. Perhaps most importantly is that there are not many resources to turn to for guidance when it comes to refactoring Build Scripts. For example, most of what I have learned about the subject comes from Julian Simpson’s work in the ThoughtWorks Anthology, which I highly suggest reading for a much more exhaustive, yet comprehensive and succinct essay on the subject. In any case, based on the above factors I am quite certain that all of these points plays a role in Ant Scripts somehow being overlooked with regard to refactoring.
So where do you begin?
That’s a really good question, one which I was forced to ask myself awhile back while being tasked with the daunting challenge of streamlining a very complex Build / CI process. At the time, I was responsible for modifying a Build for a large enterprise class Flex application which required build time transformations of localized content with varying modules being built for n-locales depending on context specific business rules, all of which needed to be built and deployed to multiple environments via a pre-existing CI Process. Further complicating things was that the builds were wrapped by nested DOS batch files. In addition, the existing builds had dependencies on far more complex underlying build Scripts. To make matters worse, up until that point in time no one, including myself, truly knew the build structure and all of it’s dependencies, it was very much a black box. So considering the fact that I needed to modify the build and would be responsible for maintaining the builds moving forward, as well as streamlining the existing build scripts so as to allow them to scale in order to support additional applications to seamlessly become part of the build, to say the least, I was eager to learn the Build Scripts inside out if I was to refactor and maintain them.
The moral to the story I just bored you with above is that if you have ever had to maintain a build before then this story probably sounds pretty familiar: you have a Build Script which is a black box that no one wants to deal with; it works and that’s all that matters – until it needs to change of course. So again, where does one begin when refactoring a Build Script? Well lets think in terms of typical OO refactoring.
Remove duplication
Perhaps one of the most obvious and easiest places to begin consideration for refactoring candidates in an Object Oriented Design is to remove duplication; that is to isolate and thin out common functionality so as to remove redundancy and duplication. Most Ant Scripts are littered with such duplication, and as such should be viewed in the same manner as one would when refactoring Object Oriented applications. In fact, the goal of refactoring is very much the same regardless of the paradigm – beit a declaratively language such as Ant or an Object Oriented language such as ActionScript – provide more efficient, maintainable and easier to work with code.
I tend to think of Build Script design – yes, it is design – much the same as any other OO design. So just as one would strive to eliminate code duplication in an Object Oriented Design, the same should apply to the design of a Build Script. For example, consider the following build target which packages a series of distributions:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | <target name="package" depends="package.src, package.tests, package.bin, package.docs, package.dist" /> <target name="package.src" > <zip basedir="${src.dir}" destfile="${dist.dir}/${dist.src.name}.zip" /> </target> <target name="package.tests" > <zip basedir="${src.tests.dir}" destfile="${dist.dir}/${dist.src.name}.zip" /> </target> <target name="package.bin" > <zip basedir="${bin.dir}" destfile="${dist.dir}/${dist.bin.name}.zip" /> </target> <target name="package.docs" > <zip basedir="${docs.dir}" destfile="${dist.dir}/${docs.name}.zip" /> </target> <target name="package.dist" > <zip basedir="${dist.dir}" destfile="${dist.dir}/${package.name}.zip" /> </target> |
This kind of Build Script is common, however if you were to think of this in terms of OO Design, whereas each target is analogous to a method, you would quickly realize the code is very redundant. Moreover, the functionality provided by these targets: the packaging of distributions, is a very common task, so just as in an OO design this functionality should be extracted into a reusable library. In Ant 1.6+ we can achieve the same kind of code reuse by extracting these common, redundant targets using Macrodefs.
Use Macrodefs
In short, a Macrodef, which is short for “macro definition”, is basically an extracted piece of reusable functionality in an Ant that can be used across Build Scripts for performing common, or specific tasks. Macrodefs can be thought of as a reusable API for Ant. You include macrodefs in your build scripts by importing the macrodef source file. This is analogous to how one would import a class.
So consider the redundant targets outlined above. Using macrodefs we can extract these common tasks, refactoring them into a single macrodef, import the file which contains the macrodef into our build script and then call the macrodef by wrapping it in a task.
To extract the target to a Macrodef we would first begin by creating a new XML document named after the functionality of the target, in this case we could call it “dist.xml”. This document would contain a project root node just as any other Ant Script would. We would then define a macrodef node and specify an identifier via the name attribute; this is how we can reference the macrodef once imported to our build script.
1 2 3 4 5 6 7 | <?xml version="1.0"?> <project name="package"> <macrodef name="package.dist" > </macrodef> </project> |
Once we have defined the macrodef we can add dynamic properties to its definition. This could be thought of as begin analogous to arguments of a method signiture. By specifying these arguments we can then assign their values whenever we invoke the macrodef. Default values can also be added if needed.
1 2 3 4 5 6 7 8 9 10 | <?xml version="1.0"?> <project name="package"> <macrodef name="package.dist" > <attribute name="base.dir" /> <attribute name="dist.dir" /> <attribute name="dist.file" /> </macrodef> </project> |
Finally, we specify the behavior of the macrodef via the sequential
node, This is where the functional markup is defined. Note that we reference the properties internally using the @{property}
notation, just as you would normally however the token is prefixed with an @
sign rather than a $
sign.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | <?xml version="1.0"?> <project name="package"> <macrodef name="package.dist" > <attribute name="base.dir" /> <attribute name="dist.dir" /> <attribute name="dist.file" /> <sequential> <zip basedir="@{base.dir}" destfile="@{dist.dir}/@{dist.file}.zip" /> </sequential> </macrodef> </project> |
We now have a parametrized, reusable piece of functionality which we can use across Ant Builds, and as such, simplifying the build while promoting code reuse.
To use the macrodef in another Ant Build we need only import it and create a target which wraps the macrodef. So we could refactor the distribution targets from the original Build file example to the following:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | <import file = "package.xml" /> <target name="package" > <package.dist base.dir="${src.dir}" dist.dir="${dist.dir}" dist.file="${dist.src.name}" /> <package.dist base.dir="${tests.dir}" dist.dir="${dist.dir}" dist.file="${dist.tests.name}" /> <package.dist base.dir="${bin.dir}" dist.dir="${dist.dir}" dist.file="${dist.bin.name}" /> <package.dist base.dir="${docs.dir}" dist.dir="${dist.dir}" dist.file="${dist.doc.name}" /> <package.dist base.dir="${dist.dir}" dist.dir="${dist.dir}" dist.file="${dist.package.name}" /> </target> |
And that’s the basics of using macrodefs to refactor an Ant Build. There is a lot more which can be accomplished with macrodefs in regards to designing and refactoring Ant Builds, specifically antlib, and I encourage you to give it a try as I am sure you will be happy with the results.
Thanks Eric a very helpful blog post :),
Cheers,
Simon