JAL Computing
|
Chapter 38 "Type Safe Unit Libraries, A Temperature Class" Many rather dramatic software failures can be traced to improper assumptions about unit types. In one incident the error can be traced to one module using Metric units while another module used Imperial units. In this chapter, we extend a MSDN sample Temperature class into a type safe Temperature class that can be used to enforce unit type safety at compile time! The Problem
Unit Type Safety C# is a strongly typed language so it is possible to design a class hierarchy that allows the coder to enforce unit type safety at compile time. Type safe unit libraries are available in C++ that provide compile time unit checking to "try to make it more difficult to make coding mistakes that incorrectly mix units of measure." In this chapter, we code a proof of concept type safe unit of temperature. A Strongly Typed Temperature Hierarchy Here is a prototype of a strongly type Temperature hierarchy with concrete classes Celsius, Fahrenheit, Kelvin and Rankine that inherit from an abstract base class, Temperature. Parameters and return values can be declared as a concrete type (Celsius,Fahrenheit,Kelvin,Rankine) or as the base class (Temperature) depending on the need for type safety. Coders may create an instance of a concrete class as in: Rankine r = new Rankine(491.688); System.Console.WriteLine("Value is R: "+ r.Value); But the following calls to Value will not compile: ((Temperature)r).Value; // will not compile, good behavior, ambiguous Temperature t = new Rankine(491.688); t.Value; // will not compile, good behavior, ambiguous This class hierarchy is based on the Temperature class found in this sample code from MSDN. This twisted version differs in the following ways:
Note: This is not a criticism of the sample MSDN Temperature class which was designed to demonstrate the use of the double class and the IComparable and IFormattable interfaces. In fact, the decision to extend this MSDN class is based on the desire to reuse the functionality in this sample class. Here is a link to a conversion chart between the four supported temperature units. This class hierarchy is extensible to some degree. For instance, new concrete classes can be added for the more obscure units such as Delisle, Newton, Reaumur and Romer. However, the base class Temperature will not provide getters and setters, parse or format in these units. Rationale The concrete classes may only return a Value in units appropriate for the class of the object. A reference of type Temperature should not return a default Value of class Fahrenheit since the value is not formatted and may not be appropriate for the concrete class. References of type Temperature should not return a an unformatted value appropriate for the concrete class as this would be extremely confusing, returning unformatted units of mixed types. Any concrete class can explicitly return a value in alternative units using explicit getters such as c.Fahrenheit. A concrete class may return alternative units in response to explicit formatting such as ToString("F",null). ToString() is overridden so that the actual value of the concrete class is returned as a formatted string. There is no ambiguity calling ToString() since the value is formatted. If the call to ToString() is on a reference of type Temperature, the units are still appropriate for the actual concrete class. Compile Time Type Safety Given a class TestConcrete with a method BoilingH2O. public class TestConcrete { public bool BoilingH2O(Fahrenheit f) { if (f.Value >= 211.9710) { return true; } else {return false;} } } The method compiles as below when passed a parameter of type Fahrenheit. TestConcrete test= new TestConcrete(); Fahrenheit fBoil = new Fahrenheit(212); bool b = test.BoilingH2O(fBoil); System.Console.WriteLine(b); // true However, the compiler will generate an error at compile time if a parameter of type Celsius is passed to Boiling. Celsius cBoil = new Celsius(100); b = t.BoilingH2O(cBoil); // compile time error Error 1 The best overloaded method match for
'TestTemperature.TestConcrete.BoilingH2O(TestTemperature.Fahrenheit)' has some
invalid arguments Without a type safe temperature unit, the method might be executed at runtime returning false, a runtime error. Awwk. This could result in some very overcooked spaghetti! Using the Base Class Temperature It is also possible to write a more generic version that takes a parameter of type Temperature and then extracts an explicit value in Fahrenheit from the concrete object. Here is our TestBase class and method which takes any Temperature t: public class TestBase { public bool BoilingH2O(Temperature t) { if (t.Fahrenheit >= 211.9710) { return true; } else { return false; } } } The compiler will not complain if we pass a reference of type Celsius to this version of BoilingH2O. TestBase tb = new TestBase(); System.Console.WriteLine(tb.BoilingH2O(cBoil)); // true Alternatives One alternative is a set of enums and a Length class that combines the data type as an enum and the value as a double. This could be use to enforce runtime unit type safety. Usage Since the base class Temperature implements the IComparable and IFormattable interfaces, it supports the IComparable.CompareTo and IFormattable.ToString(string format, IFormatProvider provider) methods. Temperature also provides a Parse method, but not a TryParse method (A TryParse method would require the proper concrete type as an out parameter, defeating the purpose of parsing the string for type). public abstract class Temperature : IComparable, IFormattable Constructor First we can create an object of class Celsius by calling the constructor and passing a valid temperature value. Celsius c = new Celsius(0); Passing a value below absolute zero will throw an ArgumentOutOfRange exception. Temperature t2 = new Celsius(-274); // throws exception Temperature t2 = new Kelvin(-1); // throws exception Temperature.ToString(string format, IFormatProvider provider) Next we output a formatted string. System.Console.WriteLine(c.ToString("F",null)); // 32'F We can also embed the formatting within a string. System.Console.WriteLine("Value is {0:C}", c); // 0'C System.Console.WriteLine("Value is {0:K}", c); // 273.15'K System.Console.WriteLine("Value is {0:R}", c); // 491.67'R System.Console.WriteLine("Value is {0:F}", c); // 32'F These calls to WriteLine in turn call the ToString(format,provider) method which, in turn, calls the appropriate getter. As a result the proper temperature value is returned for the given type of format C, K, R, or F. You can verify this by adding a breakpoint in the debugger at ToString(format,provider). Note: c.Fahrenheit.ToString() calls double.ToString() not Temperature.ToString() The behavior of the concrete class ToString() method is to return the value in the appropriate unit for the concrete class. System.Console.WriteLine(c.ToString()); // "0'C" Fahrenheit f = new Fahrenheit(32); System.Console.WriteLine(f.ToString()); // "32'F" Kelvin k= new Kelvin(273.15); System.Console.WriteLine(k.ToString()); // "273.15'K" Rankine r= new Rankine(491.67); System.Console.WriteLine(r.ToString()); // "491.67'R" Calling ToString() on a reference of type Temperature also returns the value in the appropriate unit type for the class of the actual object. Temperature parent= (Temperature)c; System.Console.WriteLine(parent.ToString()); // "0'C" Pass null parameters as in c.ToString(null,null) again returns the value in Centigrade, but formatted as '? System.Console.WriteLine(c.ToString(null,null)); // "0'?" Temperature.CompareTo(Temperature t) We can also compare two temperatures. Temperature c1 = new Fahrenheit(212); Temperature c2 = new Celsius(100); int compare= c1.CompareTo(c2); if (compare == 0) {System.Console.WriteLine("Equal :{0:F} {1:C}",c1,c2);} else if (compare < 0) { System.Console.WriteLine("Less :{0:F} {1:C}", c1, c2); } else if (compare > 0) { System.Console.WriteLine("Greater :{0:F} {1:C}", c1, c2); } // Equal Comparisons are best done between two temperatures of the same concrete class. Any valid temperature is consider greater than a null. The static call Compare(c1,c2) is aesthetically more appealing since it works with (null,null) (see String.Compare(s1,s2) for example). Static Temperature.Compare(Temperature t1, Temperature t2) OK, so I added a static Compare method that takes null. // return -1 if t1 is less than t2 or t1 is null, but t2 is not null // return 0 if t1 == t2 or both t1 and t2 are null // return 1 if t1 is > t2 or if t1 is not null and t2 is null public static int Compare(Temperature t1, Temperature t2) { if (t1 == null && t2 == null) { return 0; } else if (t1 == null && t2 != null) { return -1; } else if (t1 != null && t2 == null) { return 1; } return t1.Celsius.CompareTo(t2.Celsius); }
Parse(string s) We can create an object by parsing a string in the form of [ws][sign]digits['F|'C|'K|'R][ws]. Temperature.Parse("100'C")); Temperature.Parse("100'C", NumberStyles.Any)); Temperature.Parse("100'C", NumberStyles.Any, null)); Parse will throw a FormatException if a properly formatted string is not passed. This prevents the creation of an ambiguous unit value. Parse returns a reference of type Temperature while creating an object of the appropriate concrete class. Automatic Unit Conversion The base class Temperature provides getters and setters for automatic conversion between units. Celsius c = new Celsius(0); // direct conversion System.Console.WriteLine("Value is "+ c.Celsius+"C"); // 0C System.Console.WriteLine("Value is "+ c.Fahrenheit+"F"); // 32F System.Console.WriteLine("Value is "+ c.Kelvin+"K"); // 273.15K System.Console.WriteLine("Value is "+ c.Rankine+"R"); // 491.67R Code Here is our prototype Temperature class and hierarchy. In Chapter 39 I coded a unit test module to verify this code. Enjoy! using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Globalization; // numbers styles using System.Diagnostics; namespace Temperature { class Program { static void Main(string[] args) { // http://en.wikipedia.org/wiki/Rankine_scale // test in line formatting string System.Console.WriteLine("Testing IFormattable"); Celsius c = new Celsius(Celsius.FREEZE_H2O); System.Console.WriteLine("Value expected is {1}'C--> {0:C}", c, Celsius.FREEZE_H2O); System.Console.WriteLine("Value expected is {1}'F--> {0:F}", c, Fahrenheit.FREEZE_H2O); System.Console.WriteLine("Value expected is {1}'K--> {0:K}", c, Kelvin.FREEZE_H2O); System.Console.WriteLine("Value expected is {1}'R--> {0:R}", c, Rankine.FREEZE_H2O); // ****************************************************** // //((Temperature)r).Value; // will not compile, good behavior, ambiguous //Temperature t = new Rankine(491.688); //t.Value; // will not compile, good behavoir, ambiguous // ****************************************************** // UnitTest unit = new UnitTest(); unit.DoUnitTest(); System.Console.WriteLine(unit.Log); System.Console.ReadLine(); } } ////////////////////////////////////////////////////////////////////////// // // // **************** PROTOTYPE CODE ** DO NOT USE ********************** // // JAL 12.10.07 Extends MSDN Temperature Class // // http://msdn2.microsoft.com/en-us/library/system.double.aspx // // // ////////////////////////////////////////////////////////////////////////// // DEFINITION OF BOIL_H2O IS OLD SCHOOL 100'C NOT NEW SCHOOL 99.9839'C (see alt) // http://en.wikipedia.org/wiki/Rankine_scale // derived classes are used to enforce unit type safety at compile time /// <summary> /// *** The Celsius Class *** /// Extends the abstract Temperature class /// Conversion is handled by the base class Temperature /// The Value property is NOT available in the base class /// JAL 12.15.08 /// </summary> public class Celsius : Temperature { static public readonly double BOIL_H2O = 100; // alt 99.9839 static public readonly double FREEZE_H2O = 0; static public readonly double ABSOLUTE_ZERO = -273.15; //public Celsius() : this(ABSOLUTE_ZERO) { ;} // ASSERT t >= -273.15 public Celsius(double t) { if (t < -273.15) { throw new ArgumentOutOfRangeException(); } this.m_valueCelsius = t; } public double Value { get { return this.m_valueCelsius; } } public override string ToString() // overriding base class returns X'C { return base.ToString("C", null); } } // end Celsius /// <summary> /// *** The Farhenheit Class *** /// Extends the abstract Temperature class /// Conversion is handled by the base class Temperature /// The Value property is NOT available in the base class /// JAL 12.15.08 /// </summary> public class Fahrenheit : Temperature { static public readonly double BOIL_H2O = 212; // alt 211.9710 static public readonly double FREEZE_H2O = 32; static public readonly double ABSOLUTE_ZERO = -459.67; //public Fahrenheit() : this(ABSOLUTE_ZERO) { ;} //ASSERT t>=-459.67 public Fahrenheit(double t) { //if (t < -459.67) { throw new ArgumentOutOfRangeException(); } Celsius c = new Celsius(0); c.Fahrenheit = t; this.m_valueCelsius = c.Celsius; } public double Value { get { return this.Fahrenheit; } } public override string ToString() // overriding base class Temperature.ToString returns X'F { return base.ToString("F", null); } } // end Fahrenheit /// <summary> /// *** The Kelvin Class *** /// Extends the abstract Temperature class /// Conversion is handled by the base class Temperature /// The Value property is NOT available in the base class /// JAL 12.15.08 /// </summary> public class Kelvin : Temperature { static public readonly double BOIL_H2O = 373.15; // alt 373.1339 static public readonly double FREEZE_H2O = 273.15; static public readonly double ABSOLUTE_ZERO = 0; //public Kelvin() : this(ABSOLUTE_ZERO) { ;} //ASSERT t>=0 public Kelvin(double t) { //if (t < 0) { throw new ArgumentOutOfRangeException(); } Celsius c = new Celsius(0); c.Kelvin = t; this.m_valueCelsius = c.Celsius; } public double Value { get { return this.Kelvin; } } public override string ToString() // overriding base class Temperature.ToString returns X'K { return base.ToString("K", null); } } // end Kelvin /// <summary> /// *** The Rankine Class *** /// Extends the abstract Temperature class /// Conversion is handled by the base class Temperature /// The Value property is NOT available in the base class /// JAL 12.15.08 /// </summary> public class Rankine : Temperature { static public readonly double BOIL_H2O = 671.67; // alt 671.641 static public readonly double FREEZE_H2O = 491.67; static public readonly double ABSOLUTE_ZERO = 0; //public Rankine() : this(ABSOLUTE_ZERO) { ;} //ASSERT t>=0 public Rankine(double t) { //if (t < 0) { throw new ArgumentOutOfRangeException(); } Celsius c = new Celsius(0); c.Rankine = t; this.m_valueCelsius = c.Celsius; } public double Value { get { return this.Rankine; } } public override string ToString() // overriding base class Temperature.ToString returns X'R { return base.ToString("R", null); } } // end Rankine /// <summary> /// *** The Abstract Base Class Temperature *** /// Adapted from the MSDN Temperature Class /// A type safe Temperature hierarchy of /// Concrete classes Celsius, Fahrenheit, Kelvin, Rankine /// derive from Temperature /// Internally temperature is stored in Celsius as private data /// Temperature class stores the value as Double /// and delegates most of the functionality /// to the Double implementation. /// DO NOT provide a Value property for this base class, ambiguous! /// JAL 12.10.08 /// </summary> public abstract class Temperature : IComparable, IFormattable { // return -1 if t1 is less than t2 or t1 is null, but t2 is not null // return 0 if t1 == t2 or both t1 and t2 are null // return 1 if t1 is > t2 or if t1 is not null and t2 is null public static int Compare(Temperature t1, Temperature t2) { if (t1 == null && t2 == null) { return 0; } else if (t1 == null && t2 != null) { return -1; } else if (t1 != null && t2 == null) { return 1; } return t1.Celsius.CompareTo(t2.Celsius); } /// <summary> /// IComparable.CompareTo implementation. /// "Equal" is not "accurate" between different concrete types due to numeric inaccuracy /// ASSERT obj is of type Temperature /// This version takes obj as null /// </summary> public int CompareTo(object obj) { if (obj == null) { return 1; } // any non null string is greater than null if (obj is Temperature) { Temperature temp = (Temperature)obj; return m_valueCelsius.CompareTo(temp.m_valueCelsius); } throw new ArgumentException("Object is not a Temperature."); } /// <summary> /// ToString() /// </summary> public override string ToString() { return ToString(null, null); } /// <summary> /// IFormattable.ToString implementation. /// </summary> public string ToString(string format, IFormatProvider provider) { if (format != null) { if (format.Equals("F")) { return String.Format("{0}'F", this.Fahrenheit.ToString()); } else if (format.Equals("C")) { return String.Format("{0}'C", this.Celsius.ToString()); } else if (format.Equals("K")) { return String.Format("{0}'K", this.Kelvin.ToString()); } else if (format.Equals("R")) { return String.Format("{0}'R", this.Rankine.ToString()); } } // default return String.Format("{0}'?", this.Celsius.ToString()); ; } /// <summary> /// Parse /// </summary> /// <param name="s">String</param> /// <param name="ns">NumberStyles</param> /// <returns>Temperature</returns> public static Temperature Parse(string s, NumberStyles ns) { return Temperature.Parse(s, ns, null); } /// <summary> /// Parse /// </summary> /// <param name="s">String</param> /// <returns>Temperature</returns> public static Temperature Parse(string s) { return Temperature.Parse(s, NumberStyles.Any, null); } /// <summary> /// Parses the temperature from a string in form /// [ws][sign]digits['F|'C|'K|'R][ws] /// Class Factory Pattern /// ASSERT s != null /// ASSERT format is valid ** DO NOT ** allow construction of ambiguous temperature /// </summary> public static Temperature Parse(string s, NumberStyles styles, IFormatProvider provider) { Temperature temp; if (s.TrimEnd(null).EndsWith("'F")) { temp = new Fahrenheit(Double.Parse(s.Remove(s.LastIndexOf('\''), 2), styles, provider)); } else if (s.TrimEnd(null).EndsWith("'C")) { temp = new Celsius(Double.Parse(s.Remove(s.LastIndexOf('\''), 2), styles, provider)); } else if (s.TrimEnd(null).EndsWith("'K")) { temp = new Kelvin(Double.Parse(s.Remove(s.LastIndexOf('\''), 2), styles, provider)); } else if (s.TrimEnd(null).EndsWith("'R")) { temp = new Rankine(Double.Parse(s.Remove(s.LastIndexOf('\''), 2), styles, provider)); } else // invalid format prohibit ambiguous type { throw new FormatException(); } return temp; } // TryParse makes no sense as you need to pass concrete type as out parameter // The value holder in Celsius protected double m_valueCelsius; //ASSERT value >=-273.15 public double Celsius { get { return m_valueCelsius; } set { if (value < -273.15) { throw new ArgumentOutOfRangeException(); } m_valueCelsius = value; } } //ASSERT value >=-459.67 public double Fahrenheit { get { return (m_valueCelsius * 1.8) + 32; } set { if (value < -459.67) { throw new ArgumentOutOfRangeException(); } m_valueCelsius = (value - 32) / 1.8; } } //ASSERT value >= 0 public double Kelvin { get { return m_valueCelsius + 273.15; } set { if (value < 0) { throw new ArgumentOutOfRangeException(); } m_valueCelsius = value - 273.15; } } //ASSERT value >=0 public double Rankine { get { return (m_valueCelsius + 273.15) * 1.8; } set { if (value < 0) { throw new ArgumentOutOfRangeException(); } m_valueCelsius = (value / 1.8) - 273.15; } } } // end Temperature } This extended version of the MSDN Temperature class is type safe and can be used to enforce unit type safety at compile time. Code Safe! |
Send mail to [email protected]
with questions or comments about this web site. Copyright © 2001, 2002, 2003,
2004, 2005, 2006, 2007, 2008, 2009 ©
|