A Groovy temperature conversion with Categories

I have been getting more interested with the capabilities of the Groovy language since I have used it more and more as part of my daily work on a Grails web application. I have also been having some interesting conversations with one of my co-workers about the possibilities provided by Groovy’s MOP (Meta Object Protocol) for creating new ways of expressing meaning in the contextual space of a given problem area using DSLs (Domain Specific Languages). I have seen a few examples of augmenting existing Java classes with new functionality by using Categories wrapped around the existing classes, but have mainly seen these related to distances and conversion of units of measure within that domain. I thought back to one of the first programs I wrote (can’t remember the language, maybe it was C) for doing temperature conversion between degrees Fahrenheit and Celsius. So, I thought I would give an example of using Groovy Categories to add new functionality to existing Java classes.

The problem

So let’s assume that you are tasked with writing a temperature conversion application. This converter should be able to convert temperatures given in either degrees Fahrenheit or Celsius and give the result in degrees Fahrenheit. Sounds easy enough, you can model things within the class(es) exactly how you need to solve the problem. But wait! There is another stipulation. You are told that there is already an existing class in some existing archaic Java library that was written ten years ago, and you have you use this class within your code. You think, alright, even if the class is wretched, I can write some new Java code and extend the existing class to offer the new functionality. Presenting the Java class you are required to use:

AncientTemperature.java

package com.asoftwareguy.temperature;

/**
 * This is an "old" existing Java class representing temperature.
 * It assumes the unit of measure is degrees Fahrenheit.
 *
 */
public final class AncientTemperature {
	// in degrees Fahrenheit
	private int temp;

	public AncientTemperature(int temp) {
		this.temp = temp;
	}

	public String toString() {
		return new StringBuilder().append("The current temperature is " + temp + " degrees F.").toString();
	}
}

The class is declared final! So the creator of this class, a decade or so ago, found it in their infinite wisdom that their representation of temperature was perfect and no one would ever need to add any more functionality. Awesome! :/

The solution

You could obviously also create new functionality by creating a decorator class and using composition to delegate calls when necessary out to the existing class, but you are using Groovy, so why would you do that! Here the requirements stated above are as follows:

  1. We need to be able to give the temperature inputs in either degress Fahrenheit or Celsius.
  2. We need to be able to give the resulting temperature in degrees Fahrenheit.
  3. We have to use the existing AncientTemperature.java class.

Requirement #3 is de facto by mandate, so let’s ignore that one. After looking at what we have to work with, it seems that we have requirement #2 already met by the functionality of the existing class. The toString() method spits out the temperature in degrees Fahrenheit:

package com.asoftwareguy.temperature;

public final class AncientTemperature {

	// other methods

	public String toString() {
		return new StringBuilder().append("The current temperature is " + temp + " degrees F.").toString();
	}
}

Let’s look at how we can meet requirement #1 using Categories. Categories in Groovy are somewhat similar to the concept of static extension methods in C# with one key difference: In C#, you can only add new methods to the class which you are extending; you cannot override methods that already exist in the class. In Groovy, you can add new methods as well as override existing methods in the class you are extending! This a powerful feature and one that I will use in this example. Let’s take a look at our Category:

TemperatureConversion.groovy

package com.asoftwareguy.temperature

class TemperatureConversion {

	static AncientTemperature getFahrenheit(String fahrenheit) {
		new AncientTemperature(fahrenheit as int)
	}

	static AncientTemperature getCelsius(String celsius) {
		BigDecimal fahrenheit = (celsius.toInteger() *  (9/5) + 32)
		fahrenheit = fahrenheit.setScale(0, BigDecimal.ROUND_DOWN)
		new AncientTemperature(fahrenheit.toString() as int)
	}

	static AncientTemperature getFahrenheit(Integer fareheit) {
		new AncientTemperature(fareheit)
	}

	static AncientTemperature getCelsius(Integer celsius) {
		BigDecimal fahrenheit = (celsius *  (9/5) + 32)
		fahrenheit = fahrenheit.setScale(0, BigDecimal.ROUND_DOWN)
		new AncientTemperature(fahrenheit.toString() as int)
	}
}

If you have done any kind of temperature conversion before, the code above should look familiar. It uses the standard formulas for converting temperature between degrees Fahrenheit and Celsius, and vice-verse. With this code in place, it allows you to write code like the following, using a ‘use’ block in Groovy:

TemperatureTest.groovy

package com.asoftwareguy.temperature

use(TemperatureConversion) {
	assert  "100".fahrenheit.toString() == 'The current temperature is 100 degrees F.'
	println "100".fahrenheit

	assert 	"50".fahrenheit.toString() == 'The current temperature is 50 degrees F.'
	println "50".fahrenheit

	assert 	"32".fahrenheit.toString() == 'The current temperature is 32 degrees F.'
	println "32".fahrenheit

	assert 	"100".celsius.toString() == 'The current temperature is 212 degrees F.'
	println "100".celsius

	assert 	100.fahrenheit.toString() == 'The current temperature is 100 degrees F.'
	println 100.fahrenheit

	assert 	50.fahrenheit.toString() == 'The current temperature is 50 degrees F.'
	println 50.fahrenheit

	assert 	32.fahrenheit.toString() == 'The current temperature is 32 degrees F.'
	println 32.fahrenheit

	assert 	100.celsius.toString() == 'The current temperature is 212 degrees F.'
	println 100.celsius
}

All of the methods defined in TemperatureConversion.groovy only add new functionality to the AncientTemperature.java class. The ‘use’ block is key to this working, as it provides that any types declared within the block expose the methods of the Category. I had mentioned earlier that in Groovy, you can also override existing methods and I also said I would give an example. So here is the version of the Category with overriding methods:

TemperatureConversion.groovy

package com.asoftwareguy.temperature

class TemperatureConversion {

	// other methods

	static AncientTemperature plus(AncientTemperature first, AncientTemperature second) {
		int tempFirst = first.temp
		int tempSecond = second.temp;
		int newTemp = tempFirst + tempSecond
		return new AncientTemperature(newTemp)
	}

	static AncientTemperature minus(AncientTemperature first, AncientTemperature second) {
		int tempFirst = first.temp
		int tempSecond = second.temp;
		int newTemp = tempFirst - tempSecond
		return new AncientTemperature(newTemp)
	}
}

As you can see above, we have actually overrode the ability to add and subtract objects of AncientTemperature with each other. We can do this even though we do not have this ability in Java because by default, Groovy adds the plus and minus (along with many other methods) to all objects. So now we can write code like this:

package com.asoftwareguy.temperature

use(TemperatureConversion) {
	// other methods

	assert (100.fahrenheit + 50.fahrenheit).toString() == 'The current temperature is 150 degrees F.'
	println 100.fahrenheit + 50.fahrenheit

	assert (100.fahrenheit + 0.celsius).toString() == 'The current temperature is 132 degrees F.'
	println 100.fahrenheit + 0.celsius

	assert (100.fahrenheit - 0.celsius).toString() == 'The current temperature is 68 degrees F.'
	println 100.fahrenheit - 0.celsius
}

Groovy Categories are a powerful tool in providing new functionality to existing types at run-time and for creating DSLs that can describe your problem space. Hopefully this has provided you with a good example of using Groovy Categories to add such functionality.

All of the source code for this post can be found in this Gist.

This entry was posted in DSL, Groovy, MOP and tagged , , , , , . Bookmark the permalink.

5 Responses to A Groovy temperature conversion with Categories

  1. vasya10 says:

    Good example in showing that final does not have a final say anymore.

    I would draw attention to one minor dichotomy between code and usage.

    “TemperatureConversion” – What you define now is not any more an utility class. Instead it is the “DSL” which is close to the business you are modeling.

    Now lets say you rename the class as “Temperature(s)”. Now your code’s readbility is

    use(Tempetarue) {
    100.farenheit + 5.celsius
    }

    The above reads more close to a normal speech. Conversion is only a part of the process. Representation and Qualification of your data is more significant. In regular programming, what one calls their methods/classes doesnt matter. But in DSL it does matter because you are providing a “language” interface to the business :-)

    • asoftwareguy says:

      Yeah, my naming is misleading. I originally had the Java class named ‘Temperature’ and couldn’t have the Groovy Category class named that as well since they were in the same package. I renamed the Java class but never renamed the Groovy class. My intent was to actually name the category class ‘Temperature’ so it read better in the code, as you stated.

  2. Pingback: Celsius convesion | Plaquitalandia

  3. anonymous says:

    Actually Groovy too has extension methods like C#
    And this feature is used in groovy more often than categories

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.