Celsiuses / Fahrenheits

An example of a program to convert from celsiuses to fahrenheits

I've been given some code by a developer attempting to understand the reasoning behind No Getters & Setters.

The code is written in Java and has been copied as is.

public class Thermostat {
  private double temperatureFahrenheit;
  public double temperatureCelsius {
    get {
      return (temperatureFahrenheit - 32) * 5 / 9;
    }
    set {
      if (value < -273.15) {
        throw new ArgumentException("The temperature can't be below absolute zero.");
      }
      temperatureFahrenheit = (value * 9 / 5) + 32;
    }
  }
}

We're trying to simulate a thermostat able to convert from Celcius to Fahrenheits. That's a very common exercise proposed to beginners, especially in OO courses, and one that never fails to show how NOT to do OO.

With all due respect to this programmer, the code above isn't OO. There's many aspects of this code I don't like.

  • Why is the Fahrenheit temperature private and the Celsius public ? I can figure out the Thermostat is necessarily initialized with Fahrenheits, but why ? I should be able to setup the Thermostat in Celcius, shouldn't I ?

  • Why are these variables prefixed with temperature ? It's a classic case of Abandon Composed Names. Temperature is the real name, it has a semantic. Fahrenheit & Celcius are units, they're types.

  • Why setting the Celcius temperature should update the Fahrenheit temperature ? That's another hint that temperatureFahrenheit and temperatureCelsius refer to the same thing, conceptually. As far as the thermostat is concerned, it deals with temperatures, no matter the unit.

  • Why getting the temperature should compute it ? What if I have to get the temperature 1000 times per second, will I perform the same computation 1000 times ? That's just lost cycles.

  • Why is the thermostat controlling the conversion ? Is it the role of thermostat ? I don't think it is. To me, the job of the thermostat is to control the temperature, not to convert it.

These problems are caused by the lack of OO design in this code.

How can we improve it ? The very first step is to analyze what the brief for this code would look like. Remember, in OO we always try to model the world inside our code, so the world is the first step.

We want a program to convert from Fahrenheits to Celsius

There we go : this sentence has two nouns : Fahrenheits & Celsius. This simple brief leads us necessarily to the creation of two objects.

class Celsius {}
class Fahrenheit {}

This sentence also has a verb : convert. This is the behavior expected from our objects. We can further refine our brief to :

We want a program to convert Fahrenheits to Celsius and Celsius to Fahrenheits

class Celsius {
    toFahrenheit(): Fahrenheit;
}

class Fahrenheit {
    toCelciuses(): Celcius;
}

Oh good heaven, we've introduced a circular dependency !

So what ? That's what the brief is about : converting back & forth. So both objects must know the existence of one another, and that's perfectly fine.

But now both Celsius & Fahrenheit know about one another and depend on each other !

Still don't see the problem. The formula describing how to convert celsiuses to fahrenheits and forth depends on these two values. So each object necessarily know how to construct the other. Actually, it can't be any other way : only Celsiuses know how to transform themselves into Fahrenheits, and only Fahrenheits know how to transform themselves into Celsiuses.

And how about the validation ?

Once again, each object is responsible for verifying the input value. Celsiuses know they can't ever be below 273.15°, it's part of their definition. So it's up to Celsiuses to protect themselves from wrong values.

Same goes for Fahrenheits.

The final code looks like this.

class Celsius {
  constructor(private readonly value: number) {
    if (value < -273.15) {
      throw new Error('Celsius cannot be below the absolute zero');
    }
  }

  toFahrenheits() {
    return new Fahrenheit(this.value * 1.8 + 32);
  }
}

class Fahrenheit {
  constructor(private readonly value: number) {
    if (value < -459.67) {
      throw new Error('Fahrenheit cannot be below the absolute zero');
    }
  }

  toCelsiuses() {
    return new Celsius((this.value - 32) / 1.8);
  }
}

it('should convert from celsiuses to fahrenheits', () => {
  const celsiuses = new Celsius(0);
  expect(celsiuses.toFahrenheits()).toEqual(new Fahrenheit(32));
});

it('should convert from fahrenheits to celsiuses', () => {
  const fahrenheit = new Fahrenheit(32);
  expect(fahrenheit.toCelsiuses()).toEqual(new Celsius(0));
});

it('should throw when creating a celsius below the absolute zero', () => {
  expect(() => new Celsius(-273.151)).toThrow(
    'Celsius cannot be below the absolute zero',
  );
});

it('should throw when creating a fahrenheit below the absolute zero', () => {
  expect(() => new Fahrenheit(-459.671)).toThrow(
    'Fahrenheit cannot be below the absolute zero',
  );
});

This is proper object-orientation. Each object represents something in the real world, and each object contains behavior. No master controller above them to tell them how to do it. They're proper individuals of our society of objects, they behave as expected and deserved to be respected.

Once again, it all starts by the brief. Code is meaningless without a brief. By carefully listening to your clients / users / domain experts, you can start modeling your object's world according to their vocabulary and close the gap between their mind and the computer's mind.

Last updated