Solved

Java designing a System of Units

Posted on 2014-09-14
8
277 Views
Last Modified: 2014-09-16
The following is my exercise in attempting to design a simple system of Units so I can add some type safety to my numbers and calculations. It sounded simple at first, but soon it gets very complicated.

At least it's an interesting example of my design thought process.

I'd like to know if there's an easier way. Or if my design direction went astray somewhere. Or any comments (like become a plumber instead :)

I started with:

class Measurement

Also:

class Unit
class Celsius extends Unit
class Fahrenheit extends Unit
class Inches extends Unit
class Feet extends Unit

So far makes sense.
Celsius is a Unit
Fahrenheit is a Unit
Inches is a Unit
Feet is a Unit

now some test code:
Measurement m = new Measurement(23.3, Celsius);

m has stored within it the value 23.3 and the units are Celsius.

And it would be nice if I could convert m to Fahrenheit:

m.getValueAs(Fahrenheit);

I'm not yet convinced m should know how to convert Celsius to Fahrenheit, as this immediately leads to trouble. m is implicitly a Temperature measurement because the unit within m is Celsius, and Celsius is a unit of measure of Temperature. However I can also say:

m.getValueAs(Inches);

I would like the compiler to catch this mistake.

In an attempt to add some type safety, since m could contain any type of unit, I expanded this to:

Measurement<Temperature> m = new Measurement<Temperature>(23.3, Celsius)

Now anywhere m is used within the same method, I know its more than just a Measurement, I know it's a Measurement<Temperature>

But how much does this really help me?

I have externally declared that m is a measurement of type Temperature, for the sake of the compiler.

However, considering

Measurement.getValueAs(Units u)

this still doesn't prevent me from calling:

m.getValueAs(Inches);


So I try expanding Units to Units<Q>

Now I have:

Measurement.getValueAs(Units<Q> u)

Now the compiler at least has a chance of detecting an attempt to convert a Temperature measurement to a Length measurement. That is, the compiler will complain if I try:

m.getValueAs(Inches);

because m is a Measurement<Temperature> but Inches is not a Unit<Temperature>

When I go back and look at what happened to my classes for Celsius, Fahrenheit, and Inches. For Celsius, I started with:

class Celsius extends Unit
this eventually became:
class Celsius<Q extends Temperature> extends Unit<Q>

Initially I could do:
m.getValueAs(new Celsius());

now I have to do:
m.getValueAs(new Celsius<Temperature>());

which seems a bit excessively redundant but at least I'm prevented from doing:
m.getValueAs(new Celsius<Inches>());

Then I tried adding to the Celsius class a constant CELISUS:
public class Celsius<Q extends MeasurementType> extends Unit<Q> {
		
	public static final Celsius<Temperature> CELSIUS = new Celsius<Temperature>();

}

Open in new window

Now I can do:
m.getValueAs(Celsius.CELSIUS);

Still a bit redundant.


I still have the problem of coding:

Measurement.getValueAs(Units<Q> u)

Units can still be any type: Temperature, Length, Weight,...
and I really don't want a huge table of conversions in the Measurement class.
Or maybe I do.

Or maybe there should be some external UnitConverter class.

Or perhaps Units should know how to convert to other Units.
e.g. Celsius should know how to convert to Fahrenheit.

Even if Celsius did know how to convert a Celsius measurement to Fahrenheit, My .getValueAs in my Measurement class would look like:
public class Measurement<Q extends MeasurementType> {

	private final double value;
	private final Unit<Q> unit;

	public Measurement(double value, Unit<Q> unit) {
		this.value = value;
		this.unit = unit;
	}

	public Double getValueAs(Unit<Q> targetUnitOfMeasurement) {

		Unit<Q> currentUnitOfMeasurement = this.unit;
		return currentUnitOfMeasurement.convertValueTo(this.value, targetUnitOfMeasurement);
	}

Open in new window

and my Units<Q> class is now:
public class Unit<Q extends MeasurementType> {

	public Double convertValueTo(double value, Unit<Q> unit) {
		//need to override this in each derived class (e.g. Celsius, Fahrenheit, etc.)
		return null;
	}

}

Open in new window

and I've just introduced the dreaded "null".

My Celsius class now looks like:
public class Celsius<Q extends MeasurementType> extends Unit<Q> {
		
	public static final Celsius<Temperature> CELSIUS = new Celsius<Temperature>();

	@Override
	public Double convertValueTo(double value, Unit<Q> unit) {
		if (unit instanceof Fahrenheit) {
			double fahrenheitValue = (value * 1.8) + 32;
			return new Double(fahrenheitValue);
		}
		return null; //actually probably better to throw an error here..
	}
}

Open in new window

Well, except for the Celsius.CELSIUS, maybe it's not too bad? I'll have to sleep on it.

Any suggestions?
0
Comment
Question by:deleyd
  • 4
  • 2
  • 2
8 Comments
 
LVL 26

Accepted Solution

by:
dpearson earned 400 total points
Comment Utility
Design is usually in the eye of the beholder.

For me this just looks wrong:
Measurement m = new Measurement(23.3, Celsius);

Because in your system a Measurement really isn't a type - it's sort of a generic container of "stuff".  And then you try to impose the type.

I'd suggest a significantly different structure:

public class Celsius extends Temperature {
    private final double m_Value ;
    public Celsius(double value) { m_Value = value ; }
    public double getValue() { return m_Value ; }
    public Farenheit getAsFarenheit() { return new Farenheit(m_Value * 9.0/5.0 + 32.0) ; }
    public String toString() { return m_Value + "degrees C") ;
}

public class Farenheit extends Temperature {
    private final double m_Value ;
    public Farenheit(double value) { m_Value = value ; }
    public double getValue() { return m_Value ; }
    public Celsius getAsCelsius() { return new Celsius((m_Value - 32.0) * 5.0 /9.0) ; }
    public String toString() { return m_Value + "degrees F") ;
}

public abstract class Temperature extends Unit {
}

public abstract class Unit {
    public abstract double getValue() ;
}

Open in new window


So now we have:
Celsius(22.2) isa Temperature
Farenheit(100) isa Temperature
Celsius isa Unit

The logic for how to convert temperatures is where it belongs - inside the temperature classes.

If I have a Celsius, I can convert it to Farenheit and vice versa.
But I cannot convert it to Inches - as Celsius doesn't have a getAsInches() method.

I can write methods like this:

setBoilingPoint(Temperature t) { }

and call it with
 setBoilingPoint(new Farenheit(212)) ;
 setBoilingPoint(new Celsius(100)) ;

or pass the values around as generic Units if I really wish:
log(String message, Unit units) {
     System.out.println("Message is " + message + " and units are " + units) ;
}

setBoilingPoint(Temperature t) {
   log("Boiling point set to ", t) ;
}

Any good?

Doug
0
 
LVL 26

Assisted Solution

by:dpearson
dpearson earned 400 total points
Comment Utility
In case it's not clear, the extension to the other types would be something like:

public abstract class Length extends Unit {
}

public class Inches extends Length {
   ... Very similar to Celsius in here, except can convert to Feet or Meters
   public Meters getAsMeters() { return new Meters(m_Value * 36.0 * 0.9144) ; }
   public Feet getAsFeet() { return new Feet(m_Value * 12.0 ; }
}

public class Feet extends Length {
   ... Very similar to Celsius in here, except can convert to Inches or Meters
}

public class Meters extends Length {
   ... Very similar to Celsius in here, except can convert to Inches or Feet
}

So Feet isa Length
and Length isa Unit
but Length is not a Temperature

(You could make this all a little bit cleaner by only having conversion methods between inches and centimeters say and then re-using them for feet to centimeters and feet to meters etc. but that's getting into finer grained details of the design.  We should first agree on the main strokes of the approach.)

Doug
0
 
LVL 26

Assisted Solution

by:dpearson
dpearson earned 400 total points
Comment Utility
One final observation and then I'll shut up for a bit and let others talk :)

This design also has the nice side effect of making "Unit", "Length" and "Temperature" abstract.  Which to me makes a certain amount of sense.

I know things can have a Temperature, but to actually provide a concrete temperature I need to pick the actual units - so I need a concrete Farenheit, Celsius or Kelvin instance.  Temperature itself is abstract.  Celsius is concrete (a temperature reading with specific units).  

This design captures that distinction.  I can't do "new Temperature(100)" because I didn't say 100 what.  But I can do "new Celsius(100)".

Doug
0
 
LVL 37

Assisted Solution

by:zzynx
zzynx earned 100 total points
Comment Utility
This is my suggestion:

A Measurement class.

The value is common for all measurements. But you don't know the Unit of measurement yet. That is still abstract.
However, you can already define how toString() will look like: a value and a unit.

A Temperature class.
Still abstract, since you still don't know the unit yet. But you know it's a temperature. Hence the method getTemperature()

The Celcius and Fahrenheit class.
These are "real " classes. You know it's a measurement of temperature in the units Celcius resp. Fahrenheit.


public class Celsius extends Temperature {
    public Celsius(double value) { 
       super(value); 
    }

    public Farenheit getAsFarenheit() { 
       return new Farenheit(getTemperature() * 9.0/5.0 + 32.0) ; 
    }

    public String getUnit() {
        return "degrees C";
    }
}

public class Farenheit extends Temperature {
    public Farenheit(double value) { 
       super(value); 
    }

    public Celsius getAsCelsius() { 
       return new Celsius((getTemperature() - 32.0) * 5.0 /9.0) ; 
    }

    public String getUnit() {
        return "degrees F";
    }
}

public abstract class Temperature extends Measurement {
    public Temperature(double value) {
       super(value);
    }
   
    public double getTemperature() {
       return getValue();
    }
}

public abstract class Measurement {
    private final double value;

    public Measurement(double value) {
        this.value = value;
    }

    public double getValue() {
	return value
    }
    
    public String toString() { 
        return getValue() + " " + getUnit();
    }

    public abstract String getUnit();
}

Open in new window


And then of course you can extend to your own needs:

public class Meters extends Length {
    public Meters(double value) { 
       super(value); 
    }

    public Feet getAsFeet() { 
       return new Feet( some calculations with getLength() ); 
    }

    public String getUnit() {
        return "meters";
    }
}

public class Feet extends Length {
    public Feet(double value) { 
       super(value); 
    }

    public Meters getAsMeters() { 
       return new Meters( some calculations with getLength() ); 
    }

    public String getUnit() {
        return "feet";
    }
}

public abstract class Length extends Measurement {
    public Length(double value) {
       super(value);
    }
   
    public double getLength() {
       return getValue();
    }
}

Open in new window

0
Why You Should Analyze Threat Actor TTPs

After years of analyzing threat actor behavior, it’s become clear that at any given time there are specific tactics, techniques, and procedures (TTPs) that are particularly prevalent. By analyzing and understanding these TTPs, you can dramatically enhance your security program.

 

Author Comment

by:deleyd
Comment Utility
Thank you Doug and zzynx; both recommending I go back to my original design of adding a value to my units Celsius, Fahrenheit, Kelvin, Inches, etc.

That was my original design; then I thought it was a mistake because "Celsius is a unit of measurement, not a measurement."

But what do I care for semantics of the English language... Let's see, something didn't end right when I took that approach, but let me go back to that design and let's see what happens.
0
 
LVL 26

Assisted Solution

by:dpearson
dpearson earned 400 total points
Comment Utility
True, Celsius is a unit of measurement (which I think we can read as the class Celsius extends Unit), but "100 Celsius" is a measurement (it's not a unit of measurement to my mind).  So we can have:

Temperature myMeasurement = new Celsius(100) ;

Anyway, as you say, not worth getting too concerned over exactly how English maps to class hierarchies.  It's not super reliable :)

Doug
0
 

Author Comment

by:deleyd
Comment Utility
I made some UML diagrams.

uml-a.gif shows Celsius expands AbstractTemperature implements ICelsius. (This was from my early design.) Problem here was my getValueAs(TemperatureUnits u) I don't have a TemperatureUnits collection.

Fixed that in uml-b.gif where it's renamed getValueAs(Temperature u)

But that didn't sound right. So I modified again to get uml-c.gif where I tried explicitly stating what were Measurements.

CelsiusMeasurement expands TemperatureUnits and implements ITemperatureMeasurement

I notice here in order to get something like multiple inheritance (which I know little about, I never cared for C++), I'm branching off in one direction to get the Units (via inheritance) and branching off in another direction to get the Measurement (via 'implements').
That lead to .getValueAs(ITemperatureMeasurement u)
which doesn't look right. So I tried swapping the branching of inherits/implements to see what that looked like, and came up with uml-d.gif where CelsiusMeasurement inherits abstract TemperatureMeasurement and implements ICelsiusUnit, which expands ITemperatureUnits.

Now I can say CelsiusMeasurement.getValueAs(TemperatureUnits u)

(and I could try saying Celsius.getMeasurementAs(TemperatureMeasurement u)
as in m.getMeasurementAs(FahrenheitMeasurement)
though it might look better if I just renamed it .to
as in FahrenheitMeasurement f = m.to(FahrenheitMeasurement)
but really that whole concept is silly as it only changes the internal representation for the same temperature.)
I notice I'm using "implements interface" to create a collection. Interface ITemperatureUnits is a collection of Temperature units, but it's not a List (or Set).

I suppose I could create numerous collections in this manner, by just creating an interface called IMySet and whatever classes I want in that set, have them implement that interface.

I've learned a new way of creating Sets of classes!

Unfortunately there's no way to iterate over this set of Temperature Units. (But I'll ignore that little inconvenience for now.)

I did learn a design pattern here. If a class is two things, (my Celsius class is both a Unit and a Measurement) I can use interfaces to split it up so Unit goes one way and Measurement goes another (see CelsiusMeasurement.gif)

I'll go one last step and make both ways be Interfaces, and throw in some abstract classes to see what the result looks like.

(See uml-e.gif)

I'll assign points since this is such a huge complex design (it started out as a simple idea!) and maybe tomorrow open another followup question.

Thank you for your help everyone! (This little endeavor has become more of an academic exercise to learn design.)
uml-a.gif
uml-b.gif
uml-c.gif
uml-d.gif
CelsiusMeasurement.gif
uml-e.gif
0
 
LVL 37

Expert Comment

by:zzynx
Comment Utility
Thanx 4 axxepting
0

Featured Post

Find Ransomware Secrets With All-Source Analysis

Ransomware has become a major concern for organizations; its prevalence has grown due to past successes achieved by threat actors. While each ransomware variant is different, we’ve seen some common tactics and trends used among the authors of the malware.

Join & Write a Comment

What is Waterfall Model? Waterfall model is the classic Software Development Life Cycle method practiced in software development process. As the name "waterfall" describes, this development is flowing downwards steadily like waterfall, i.e., procee…
Introduction This article is the last of three articles that explain why and how the Experts Exchange QA Team does test automation for our web site. This article covers our test design approach and then goes through a simple test case example, how …
Viewers learn about the third conditional statement “else if” and use it in an example program. Then additional information about conditional statements is provided, covering the topic thoroughly. Viewers learn about the third conditional statement …
Viewers will learn about basic arrays, how to declare them, and how to use them. Introduction and definition: Declare an array and cover the syntax of declaring them: Initialize every index in the created array: Example/Features of a basic arr…

762 members asked questions and received personalized solutions in the past 7 days.

Join the community of 500,000 technology professionals and ask your questions.

Join & Ask a Question

Need Help in Real-Time?

Connect with top rated Experts

10 Experts available now in Live!

Get 1:1 Help Now