Type-Oriented Programming

I’ve found myself using an interesting pattern recently.

For a horribly contrived example, let’s assume you want a class, Foo, to have a member, Bar.  This member is an integer, and we want it to only be even.  A typical way of writing this (in C# land, at least), might be:

    public class Foo
    {
        private int m_bar = 0;
        public int Bar
        {
            get { return m_bar; }
            set { m_bar = (value / 2) * 2; }
        }
    }

Simple enough, until we add the method Baz()…

        public void Baz()
        {
            m_bar = 3;
        }

Oh no!  m_bar is now odd!  While in this case it’s doubtful that there’d be a major issue with an integer simply being odd, there’s a wide range of cases where making sure that your validation rules are followed is vital.  Sure, we can beat up the programmer that wrote Baz(), but really what we want to do is make sure that this type of error can’t happen.

What we can do in this case is encapsulate the validation into a type, like this:

    public struct EvenInteger
    {
        public EvenInteger(int value)
        {
            m_value = (value / 2) * 2;
        }
        private int m_value = 0;
        public int Value
        {
            get { return m_value; }
        }
    }

By not applying a setter for m_value, we ensure that it is used in an immutable way - by doing so, and using a struct rather than a class, EvenInteger can be used pretty much exactly like an int, except that it’s guaranteed to be even.

Here’s Foo, rewritten to use EvenInteger:

    public class Foo
    {
        private EvenInteger m_bar = new EvenInteger(0);
        public EvenInteger Bar
        {
            get { return m_bar; }
            set { m_bar = value; }
        }
        public void Baz()
        {
            m_bar = new EvenInteger(3);
        }
    }

So, now Baz() can’t break m_bar by writing to it directly.  But we’ve polluted our interface with an extra type that the user probably doesn’t care about, and writing to it requires (on an API level) the construction of a new object type (EvenInteger).  It works, but it’s kind of poor API.  Even using the results of EvenInteger requires you to know to get its value, as in the following code:

            Foo f = new Foo();
            f.Bar = new EvenInteger(5);
            int a = f.Bar.Value * 3;

 

That’s pretty ugly.  Fortunately, there’s a few ways that we can fix this.  First, we can modify the getter and setter of Foo to hide the type for us.

        public int Bar
        {
            get { return m_bar.Value; }
            set { m_bar = new EvenInteger(value); }
        }

That’s better, as at least the client code doesn’t have to deal with it.  However, if we decide to use EvenInteger elsewhere in our code, we’ll have a bunch of repeats of that conversion all over the place.  So, we can define some implicit conversion operators to convert to and from integers for us.

        static public implicit operator int(EvenInteger i)
        {
            return i.m_value;
        }

        static public implicit operator EvenInteger(int i)
        {
            return new EvenInteger(i);
        }

Now, we can treat EvenIntegers as regular ints, anywhere in the code.  We don’t even need the Value property any more.  We don’t even have to really think about validating the value - doing so is handled by the language and the compiler.  And any time we can remove a chance for human error is good as far as I’m concerned.

Here’s the final Foo class:

    public class Foo
    {
        private EvenInteger m_bar = 0;
        public int Bar
        {
            get { return m_bar; }
            set { m_bar = value; }
        }
        public void Baz()
        {
            m_bar = 3;
        }
    }

The difference here is that we’ve moved our validation rules out of the Foo class and into the EvenInteger type.  Normally, when we think of object-oriented programming, we think of classes of objects as medium-to-large collections of data combined with their associated logic.  But in this case, we’ve used object-oriented programming to extend the type system, which I think is a slightly different concept.  Additionally, since the rules about being an EvenInteger are encapsulated within the type, we can use this extended type elsewhere in this, or other, programs.  In other words, instead of having our classes handle validation manually, we encode the validation into a new type that can be used just like any of the built-in simple types.

That might not sound too interesting for an integer that can’t be odd, but there’s a lot more interesting possibilities.  If you have a limit on the number of characters that can be in a name, create a Name type and you won’t have to check the length everywhere.  Or for something that’s very common, and very dangerous:

    public struct SafeSqlString
    {
        private string m_string;
        public SqlSafeString(string s)
        {
            s = s.Replace(‘\”, ‘”‘);
        }
        static public implicit operator string (SqlSafeString s)
        {
            return s.m_string;
        }
        static public implicit operator SqlSafeString(string s)
        {
            return new SqlSafeString(s);
        }
    }

While that’s probably the worst SQL sanitization code I’ve ever seen, it serves to show the concept.  In this case, we might not want to hide the type - a function that takes a SqlSafeString is guaranteed to have gone through the sanitization code in the constructor (which would hopefully be a little better).

        public void SaveName(SafeSqlString name)
        {
            string query = “insert into names values (’” + name + “‘);”;
            ExecuteQuery(query);
        }

Because we use the SafeSqlString as a parameter, we know that name has been validated.  We know that if we update the sanitization rules in SafeSqlString, that this method doesn’t have to change at all - by using the type, it’ll pick up the new rules without a single change in client code.

BTW, yes, I know you should use parameterized queries instead of building them manually…