Refactoring in NAnt generic-ness

I recently needed to create a copasetic NAnt script that handled database schema management– i.e. the script would create a database and populate it with seed data. Such a process is central to the notion of Continuous Database Integration (which happens to be the subject of chapter 5 in Addison Wesley’s forthcoming book entitled “Continuous Integration: Improving Software Quality and Reducing Risk“).

Using NAntContrib‘s sql task, I proceeded to create two hip targets– one which ran a DDL script that dropped any existing tables and then re-created then. The second target ran a DML script, which then proceeded to populate the database schema with data. This is essentially an up front process that enables one to, say, deploy a fully functioning application for testing, in an automated fashion from within a build process. Lastly, I created an additional boss task, dubbed database-prepare, which called the first two tasks and acted as a handy facade.

Once completed, I was essentially left with a NAnt script that screamed in the face of the DRY principle as I had two tasks that did essentially the same thing. For example, below is a snippet of the offending build file:

<target name="database-prepare">
 <call target="database-create"/>
 <call target="database-load"/>
</target>

<target name="database-create">
 <echo message="Creating database definition with ${data-definitions}"/>
 <sql connstring="${project.db.conn}"
     delimiter=";" delimstyle="Normal"
     print="true" source="${data-definitions}"/>
</target>

<target name="database-load">
 <echo message="Loading database with data found in ${data-load}"/>
 <sql connstring="${project.db.conn}"
     delimiter=";" delimstyle="Normal"
     print="true" source="${data-load}"/>
</target>

At this point, because it’s my bag, I found myself feeling somewhat disquieted (of course, in a disco sort of way) longing for a more expressive form of build scripting, say with something like Boo. But I found myself not needing a script task, for example, but a more rich way of structuring a build– like what one finds in Rake. With more expressive build platforms (that don’t rely on XML) one can create reusable functions, for example, which is exactly what I wanted– a generic sql task that I could then pass in the desired load file at build time depending on the command.

In the end, rather than rewrite the entire build, I chose to force target reuse in NAnt via the property task. While not perfect, it works– the build file has a generic sql target that relies on a smokin’ property being set– if the property isn’t set, the target will fail. The way the build is structured, the only way to cause a failure is to call the generic target directly (to my knowledge, there isn’t a way to explicitly deny calling a target directly– private targets anyone?). I chose to name the target with a pythonic underscore to signify this target’s private-ness.

The API of the build is essentially still the same– there are three public targets, which correspondingly initialize the property to the proper DDL or DML file and then call the generic sql target as follows:

<property name="data-load" value="./sql/data-load.sql" />
<property name="data-definitions" value="./sql/data-definitions.sql" />

<target name="database-create">
 <property name="data-file" dynamic="true" value="${data-definitions}"/>
 <call target="_database-do"/>
</target>

<target name="database-load">
 <property name="data-file" dynamic="true" value="${data-load}"/>
 <call target="_database-do"/>
</target>

<target name="_database-do">
 <echo message="loading ${data-file}"/>
 <sql connstring="${project.db.conn}"
     delimiter=";" delimstyle="Normal"
     print="true" source="${data-file}"/>
</target>

<target name="database-prepare">
 <call target="database-create"/>
 <call target="database-load"/>
</target>

While not perfect, it certainly is more in line with what I’d do if I were coding this logic in C#, for example. A build file (or files) is one of the most important assets a project has– without an effective build, no matter how many tests you write or how perfect the code is, customers will have a hard time receiving working, reliable applications. So the next time you find yourself wincing at your build script, take the time to refactor it, man!

Post to Twitter

Related odds and ends
 

One Response to “Refactoring in NAnt generic-ness”


  1. [...] Because it’s my bag, I largely agree with Martin, however, given Ant’s deep penetration within the Java market, compared to Maven (which, I think is one step closer to a Rake-like build platform for Java) I wonder if the Rake model can come to fruition and obtain any reasonable mindshare. Sure, there are hip projects underway like Raven (which combined forces with JRake recently), which does a number of useful things such as compilation, testing, etc, but ultimately, as Ant is the de facto build platform for Java, practically every tool out there bundles an Ant task. If you want to use Raven and would like to generate a report with JavaNCSS, for example, you might have to write some custom Ruby code to fire the JavaNCSS process off. Hopefully, the increased velocity of development on JRuby will alleviate this issue as it appears JRake makes use of Ant tasks. [...]