OOPSLA 2006 Educator's Symposium: Nifty Assignment

Temperature Calculator

Programming for Change

Dung “Zung” Nguyen, Mathias Ricken

Rice University, Houston, TX USA


Provided Stub Code

Click here to download the provided stub code that is to be used in the following programming assignment.

The Initial Problem

Your cousin Pierre has invited you to spend two weeks with his family in Paris, France. Before you leave, however, he warns you: "It's 35 degrees here!" Remembering that France measures temperatures in degrees Celsius, you frown... Does Pierre want you to pack your winter coat or your bathing suit? To figure that out, you need to convert 35 degrees Celsius to Fahrenheit.

The Constant Solution

Of course, we all know how to solve this problem. Simple algebra is sufficient, we may just have to look up the formula before we can apply it: F = (C / 5) * 9 + 32. In this case, C = 35, so the solution is

F = (35 / 5) * 9 + 32 = 7 * 9 + 32 = 63 + 32 = 95

35 degrees Celsius are equivalent to 95 degrees Fahrenheit. Pack the bathing suit.

By performing this little calculation, we have solved our simple problem: We know how hot it is in Paris and can pack accordingly. Since we have solved the problem for just one value, the 35 degrees Celsius, this solution is called a "constant solution". The good thing about this program is that it is simple, easy to understand, and most importantly it correctly solves the original problem. However, if we change slightly the problem to converting 30 degrees Celsius to Fahrenheit, the program no longer is applicable. It is correct, but not flexible.

In this assignment, you'll be changing the problem slightly to see how design choices affect the complexity of the model. To ensure that your model and view are decoupled and can thus be changed independently from each other, you'll also write two user interfaces (views) for each model: A graphical user interface (GUI) and a text interface executing in the console. We have provided the code for the constant solution to get you started (click link to execute example or download source).

Example applet of the GUI:
</COMMENT>

The Variable Solution

Intuitively, the problem doesn't really change when we change the temperature in Celsius to 30 degrees. We should still be able to solve it using the same means; the general problem is about converting any temperature measured in the Celsius scale to a value in degrees Fahrenheit. We should be able to change the temperature without necessitating a change in the code. Since the temperature can vary, this solution is called a "variable solution".

Exercise 1a: Identify the variant and the invariant of converting a temperature from Celsius to Fahrenheit.

Exercise 1b: Draw a UML diagram of the model. It should consist of a single static method.

Exercise 1c: Write a program that allows the user to convert any temperature in Celsius to its corresponding value in Fahrenheit. You can start by modifying the code for the constant solution, but remember to provide the two user interfaces!  Watch out for integer arithmetic being carried out instead of floating point arithmetic.

Example applet of the GUI (reverse engineering is prohibited!):
</COMMENT>

Converting from Celsius to Fahrenheit and Back

The solution you developed for Exercise 1 separated the variant from the invariants, and we can now convert any temperature in Celsius to the Fahrenheit scale. One shortcoming should be immediately apparent, though, especially when you look at your graphical user interface: You can only convert from Celsius to Fahrenheit, but not the other way! By using algebra and solving the above formula for C, we can derive a formula to convert degrees Fahrenheit to degrees Celsius:

C = 5/9 * (F - 32)

Exercise 2a: Draw a UML diagram of a changed model that consists of two static methods and allows conversion in both directions.

Exercise 2b: Write a program that implements this design. Again, provide two user interfaces.

Example applet of the GUI (reverse engineering is prohibited!):
</COMMENT>

Adding More Temperature Scales

The program developed for Exercise 2 allows the user to convert any temperature in either of the two temperature scales, Fahrenheit or Celsius, to the corresponding value in the other scale. Unfortunately, these aren't the only temperature scales in use: In many sciences, temperatures are measured in Kelvin, a scale whose origin is "absolute zero", the lowest possible temperature. Here is a formula to put Kelvin in relation to the Celsius scale:

K = C + 273.2

Your next program should support conversions in any direction between Celsius, Fahrenheit and Kelvin.

Exercise 3a: If you follow the pattern from the first two solutions of using one static method per possible conversion, how many methods will you need for this model?

Exercise 3b: Draw the UML diagram of the changed model, with one static method for each possible conversion.

Exercise 3c: Write a program that implements this design. Again, provide two user interfaces.

Example applet of the GUI (reverse engineering is prohibited!):
</COMMENT>

Analysis of our Approach

You are probably getting annoyed by now, so let's not write more code for a moment and just analyze our approach so far. Your program now supports conversion between three temperature scales in any direction, for any input value.

Exercise 4a: Compare the model from Exercise 2 to the model from Exercise 3. What is the ration between the number of methods in the models of Exercise 2 and Exercise 3?

There are even more temperature scales. One that has fallen out of use in the 20th century is the Reaumur scale developed by the French naturalist Rene-Antoine Ferchault de Reaumur, with the freezing point of water at its origin (just like Celsius) and the boiling point of water at 80.

Exercise 4b: Assume we added full support for the Reaumur scale to our program. Using our current approach of one method per possible conversion, how many methods will you need?

Exercise 4c: Using your answers to Exercises 3a, 4a and 4b, derive a formula - f  - that allows you to calculate the number of methods, given the number of temperature scales - n: f(n) =

Exercise 4d: What is the complexity (i.e. the number of methods) of this approach, expressed in "big O" notation?

A Better Approach

As you can tell from your answer to Exercise 4c, the number of methods with our current approach grows very quickly. Even if you add just one more scale, you will have to write many more methods - two for each of the scales that were already there. If you had 100 scales and added a 101st, you would have to write 200 additional methods! There has to be a better way.

Let's reconsider what we are doing: We are converting temperatures, and even though we can express the temperature in different scales, resulting in different values, the meaning of that value is still the same: 100 degrees Celsius is the same as 212 degrees Fahrenheit, which is the same as 373.2 Kelvin. That means that we do not have to be able to convert from Fahrenheit to Kelvin directly, we can convert from Fahrenheit to Celsius first and then convert from Celsius to Kelvin! As long as we can somehow, using the conversion formulas we have, get from each scale to each other scale, our program can still perform conversions between arbitrary scales.

Exercise 5a: Using this new approach, what is the minimum number of methods we need to convert between three scales? Four scales? n scales?

Exercise 5b: What is the complexity (i.e. the number of methods) of this new approach, expressed in "big O" notation?

If we draw a directed graph (a "web" with "dots" and "arrows" between the dots) of this minimal arrangement with the temperature scales as nodes ("dots") and the conversion methods as edges ("arrows"), the graph is star-shaped: There is one node in the middle, all other nodes are arranged around it, and there is an arrow going from the central node to each of the outer nodes, and an arrow going from each of the outer nodes back to the central node.

Exercise 5c: Draw a UML diagram of the model using the new approach. The model should have the minimum number of methods to still perform arbitrary conversions between Celsius, Fahrenheit, and Kelvin.

Exercise 5d: Write a program that implements this design. Again, provide two user interfaces; they should look the same as in the program for Exercise 3c.

Separating Variants and Invariants Again

Our new approach lets us add new temperature scales by adding only a small, constant number of methods. We say that an "epsilon" (i.e. small) change in the specification will only cause a "delta" (i.e. proportional or manageable) change in the code. Theoretically, we can quite easily add an arbitrary number of scales. Can we come up with a design that allows us to do that without touching existing code at all? To do that, we need to separate variants from invariants again.

Before doing that, we are going to make one change to our graph of temperature scales: Let's put Celsius both in the center of the graph and on the outside. Even though the scale in the center is a Celsius scale, from now on we will call it "base scale". It may look like we just made our life more complicated, since we now have four nodes instead of just three, but this is actually a simplification: Before Celsius was treated differently because it was in the center; now Celsius is on the outside as well, and all scales are treated the same. This is what the graph looks like now:

Star-shaped graph of conversion functions from Celsius, Fahrenheit, and Kelvin to a "base scale" and back.

To be more precise, let's give our conversion functions names. These are the six conversion functions our program will be able to handle:

CF(x) - convert x from Celsius to Fahrenheit
CK(x) - convert x from Celsius to Kelvin
FC(x) - convert x from Fahrenheit to Celsius
FK(x) - convert x from Fahrenheit to Kelvin
KC(x) - convert x from Kelvin to Celsius
KF(x) - convert x from Kelvin to Fahrenheit

And these are the six conversion functions we actually write:

CB(x)  - convert x from Celsius to base scale
BC(x) - convert x from base scale to Celsius
FB(x) - convert x from Fahrenheit to base scale
BF(x) - convert x from base scale to Fahrenheit
KB(x) - convert x from Kelvin to base scale
BK(x) - convert x from base scale to Kelvin

Since the base scale is identical to the Celsius scale, the functions CB and BC are just the identity function: CB(x) = BC(x) = x. We can now express the six functions in the first group using the six functions in the second group:

CF(x) = BF( CB(x) )  CK(x) = BK( CB(x) )
FC(x) = BC( FB(x) )   FK(x) = BK( FB(x) )
KC(x) = BC( KB(x) )   KF(x) = BF( KB(x) )

To convert from one temperature scale to another, we first apply the input value to the function that takes us to the base scale, and then apply the result to a second function, the one that takes us from the base scale to the target scale:

  1. To convert from Fahrenheit to Kelvin, we first apply FB to x; then we apply BK to the result of the first application.
  2. To convert from Celsius to Fahrenheit, we first apply CB to x; then we apply BF to the result of the first application.
  3. To convert from Fahrenheit to Celsius, we first apply FB to x; then we apply BC to the result of the first application.

Or abstractly:

  1. To convert from a source scale "s" to a target scale "t", we first apply sB to x; then we apply Bt to the result of the first application.

Exercise 6a: What differs and what always remains the same when performing conversions this way? Identify the variants and the invariants. Remember that we can pass functions as data in an object-oriented language like Java.

If we want to add Reaumur as a fourth scale to our model, all we need to do is provide two additional functions:

RB(x) - convert x from Reaumur to base scale
BR(x) - convert x from base scale to Reaumur

These two functions, together with the six others, give us the ability to handle the six conversions from and to Reaumur:

CR(x) = BR( CB(x) ) FR(x) = BR( FB(x) ) KR(x) = BR( KB(x) )
RC(x) = BC( RB(x) ) RF(x) = BF( RB(x) ) RK(x) = BK( RB(x) )

Exercise 6b: What three pieces of data are necessary to define a temperature scale in our model? Hint: These are the invariants you identified earlier.

In functional languages, a function that is passed as data so it can later be applied is called a lambda; in object-oriented languages, it is also called the command design pattern. For this exercise, you can implement the ILambda interface for this purpose.

public interface ILambda<R, P> {
      public abstract R apply(P param);
}

It is interesting to note that the two functions that describe a temperature scale (e.g. RB and BR for the Reaumur scale) are not just any functions: They are inverses of each other, i.e. BR( RB(x) ) = x. A function that has an inverse is called a bijection. We have therefore provided an IBijection interface that extends ILambda and is able to provide its own inverse, which is also an IBijection.

public interface IBijection<R, P> extends ILambda<R, P> {
      public abstract IBijection<P, R> getInverse();
}

Since an IBijection can provide its inverse, one IBijection instance can be used to represent both the conversion function to the base scale and the function from the base scale back. The class that represents an abstract temperature scale, AUnit, therefore needs only two fields for the variants, and one method expressing the invariant.

Just like in physics, you need to keep track of both a number and a unit (i.e. the temperature scale). To make sure the correct temperature scale remains associated with the number, it is a good idea to introduce a class to represent this number-unit pair. We have provided the Measurement class for this purpose.

Exercise 6c: Using the IBijection interface and the AUnit and Measurement classes, draw a UML diagram of the model with the Celsius, Fahrenheit, Kelvin, and Reaumur temperature scales.

Exercise 6d: Write a program that implements this design. Again, provide two user interfaces. We have provided some stub code to help you get started.

Press the button on the applet to see an example of the GUI (reverse engineering is prohibited!):
</COMMENT>

Note: Adding new scales on-the-fly using reflection may not work when using the applet. For a demonstration of reflection, please download the fullNS.jar file and run the application using the following command line:

java -cp fullNS.jar controller.TempCalcApp

Good luck!