Fake Java enum implementation revisited

No Comments December 2, 2012

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.


No Comments