Fake Java enum implementation revisited
A while ago, I blogged about a fake Java style enum using C# using extension methods. As I initially suspected, I’ve ended up using this pattern on a fairly frequent basis. An updated version of the code from the original post is here:
using System; using System.Collections.Generic; namespace Planets { internal class Program { private static void Main(string[] args) { var p = new Program(); p.Run(); } private void Run() { double earthWeight = 175; double mass = earthWeight / Planet.Earth.SurfaceGravity(); foreach (Planet planet in Enum.GetValues(typeof (Planet))) Console.WriteLine("Your weight on {0} is {1}", planet, planet.SurfaceWeight(mass)); } } public enum Planet { Mercury, Venus, Earth, Mars, Jupiter, Saturn, Uranus, Neptune, } public static class PlanetExtensions { private const double G = 6.67300E-11; private static readonly IDictionary<Planet, PlanetData> _planetMap; static PlanetExtensions() { _planetMap = new Dictionary<Planet, PlanetData> { { Planet.Mercury, new PlanetData(3.303e+23, 2.4397e6) }, { Planet.Venus, new PlanetData(4.869e+24, 6.0518e6) }, { Planet.Earth, new PlanetData(5.976e+24, 6.37814e6) }, { Planet.Mars, new PlanetData(6.421e+23, 3.3972e6) }, { Planet.Jupiter, new PlanetData(1.9e+27, 7.1492e7) }, { Planet.Saturn, new PlanetData(5.688e+26, 6.0268e7) }, { Planet.Uranus, new PlanetData(8.686e+25, 2.5559e7) }, { Planet.Neptune, new PlanetData(1.024e+26, 2.4746e7) } }; } public static double Mass(this Planet planet) { return GetPlanetData(planet).Mass; } public static double Radius(this Planet planet) { return GetPlanetData(planet).Radius; } public static double SurfaceGravity(this Planet planet) { PlanetData planetData = GetPlanetData(planet); return G * planetData.Mass / (planetData.Radius * planetData.Radius); } public static double SurfaceWeight(this Planet planet, double mass) { return mass * SurfaceGravity(planet); } public static PlanetData GetPlanetData(this Planet planet) { if (!_planetMap.ContainsKey(planet)) throw new ArgumentOutOfRangeException("planet", "Unknown Planet"); return _planetMap[planet]; } } public struct PlanetData { private readonly double _mass; private readonly double _radius; public PlanetData(double mass, double radius) { _mass = mass; _radius = radius; } public double Mass { get { return _mass; } } public double Radius { get { return _radius; } } } }
The only real drawback of using this implementation is that it can be quite brittle as it relies on a central Dictionary being maintained to map enumerated values to data values.
Fortunately, this can be easily caught by appropriate unit tests;
using System; using System.Collections.Generic; using System.Linq; using FluentAssertions; using NUnit.Framework; namespace Planets.Tests { [TestFixture] public class PlanetTests { public static IEnumerable<Planet> AllPlanets() { return Enum.GetValues(typeof (Planet)).Cast<Planet>(); } [TestCaseSource("AllPlanets")] public void AllPlanetsShouldHaveAssociatedData(Planet planet) { Action action = () => planet.GetPlanetData(); action.ShouldNotThrow<ArgumentOutOfRangeException>(); } } }
With this test in place, if anything is added to the Planet enumeration (say, Pluto gets upgraded in status again) the AllPlanetsShouldHaveAssociatedPlanetData test case will fail; in addition, if you use a continuous testing toolkit like NCrunch as I do, the test will fail immediately in the IDE giving you instant feedback.