Operator overloading your domain model with interfaces and base classes

One of the challenges in rewriting my online baseball game is dealing with enormous amounts of data that needs to be stored as aggregates, and coming up with a domain model and data mapping pattern that works. In this blog post, I’ll outline how I addressed some of those issues.

The Data Model

Baseball is very much a statistics-oriented game. Consider fielding statistics: putouts (PO), assists (A), errors (E) and others. These stats need to be stored:

  • Per game, for each player who played in the game, for each position he played (key fields: game, player, position)
  • Per season, for each player, for each team he played for, for each position he played (key fields: season, player, team, position)
  • Career, for each player, for each position he played (key fields: player, position)

On the database side, that results in three tables: GameFieldingStats, SeasonFieldingStats, and CareerFieldingStats. Each has the same set of fields to store the statistics (PO, A, and E); the differences are in the key fields for each, as outlined in the diagram below. (Note: For the remainder of this post, I’ll include only the first two of those tables to keep things short.)

image

Writing the domain model for each of these tables is quite easy. The following snippets shows the class declaration for each, including Castle ActiveRecord attributes. Note that these classes inherit from a base class, EntityBase<T>, which in this project is a requirement for all domain objects.

[ActiveRecord("bb_gamefieldingstats", Lazy = true)]
public class GameFieldingStats : EntityBase<gamefieldingstats>
{
    //...
}
[ActiveRecord("bb_seasonfieldingstats", Lazy = true)]
public class SeasonFieldingStats : EntityBase<seasonfieldingstats>
{
    //...
}

Use cases, interfaces, base classes, and operator overloads (whew!)

Now that I have these classes, I want to use them! After each baseball game is simulated, a number of GameFieldingStats records are created to reflect the statistics of that game. I want to add those stats to the player’s SeasonFieldingStats. I could access the similar properties in each class (PO, A, E) and add them together, but that’s not what I want. I want to do this:

GameBattingStats gbs = new GameBattingStats();
SeasonBattingStats sbs = new SeasonBattingStats(); 
// use cases follow
SeasonBattingStats combined = sbs + gbs;
SeasonBattingStats removed = sbs - gbs;
sbs += gbs; //!!

It’s easy enough to do that using operator overloads in C#. To make that work, I need all fielding stats classes to inherit from a base FieldingStats class that implements the operator overloading. I’m going to take it one step further and add an IFieldingStats interface. The class diagram below shows how this was done.

image

Unfortunately, by doing this, I can no longer have GameFieldingStats and SeasonFieldingStats inherit from my domain object base class, EntityBase<T> (since C# doesn’t allow multiple inheritance of classes). To resolve that, I made FieldingStats into a generic class, and had it inherit from EntityBase<T>. The class diagram shows the adjusted model.

image

The operator overloads are implemented on the generic FieldingStats<T> class, as follows:

public static T operator +(FieldingStats<t> fs1, IFieldingStats fs2)
{
    if (fs1 == null)
        throw new ArgumentNullException("fs1");
    if (fs2 == null)
        throw new ArgumentNullException("fs2");
    T fs3 = new T();
    fs3.A = fs1.A + fs2.A;
    fs3.E = fs1.E + fs2.E;
    fs3.PO = fs1.PO + fs2.PO;
    return fs3;
}

So far, so good. Everything compiles, and on paper it works. But how do we test it?

Unit testing: does it work?

To test this, I wrote the following unit tests.

[TestFixture]
public class FieldingStatsTests
{
	[Test]
	public void AddFieldingStatsTests()
	{
		GameFieldingStats fs1 = new GameFieldingStats();
		fs1.PO = 1;
		fs1.A = 2;
		fs1.E = 3;

		IFieldingStats fs2 = new GameFieldingStats();
		fs2.PO = 100;
		fs2.A = 200;
		fs2.E = 300;

		IFieldingStats fs3 = fs1 + fs2;
		Assert.AreEqual(fs3.PO, (fs1.PO + fs2.PO), "Adding FieldingStats yields incorrect PO");
		Assert.AreEqual(fs3.A, (fs1.A + fs2.A), "Adding FieldingStats yields incorrect A");
		Assert.AreEqual(fs3.E, (fs1.E + fs2.E), "Adding FieldingStats yields incorrect E");
	}
}

All tests pass!

By combining the base classes, interfaces, generics, and operator overloads, I can now add and subtract domain objects from each other, and my code has become much more readable. It wasn’t obvious how to do it at first, but once the pieces fell together, it made perfect sense.

0 thoughts on “Operator overloading your domain model with interfaces and base classes

  • hi brian! i’m writing this from my rooftop watching the lunar eclipse…
    don’t forget to use this restriction

    public abstract class EntityBase
    where T : EntityBase {}

    this makes sure that T can only be a class which inherits EntityBase so you can’t do something wierd like:

    public class Person : EntityBase { } // WTF?

    (disclaimer: not looking at code right now, might have syntax errors)

  • *sigh*
    public class Person : EntityBase { }
    is
    public class Person : EntityBase<Address> { }

    i hope you just edit the original and delete all this spam 🙂

  • Hey, I like comment spam when it’s not spam. 🙂

    The generic of EntityBase<T> requires T to be the entity type, which is why I made FieldingStats<T> generic in the first place — so it can pass the type to the underlying EntityBase<T>.

    So it looks like this:

    public class GameFieldingStats : FieldingStats<GameFieldingStats> { }
    public class FieldingStats<T> : EntityBase<T>, IFieldingStats where T : FieldingStats<T>, new()
    public class EntityBase<T> : ActiveRecordBase<T>, IEquatable<T> where T : ActiveRecordBase<T>

    whew!

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes:

<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

This site uses Akismet to reduce spam. Learn how your comment data is processed.