4

I've been struggling with a design so I figured I'd ask here and see if anyone's able to help :)

High level overview

I'm designing an app to gamify exercise by creating mini competitions (ex. Who can lose the most weight in 3 months or bike the most miles in a month).

Users can create competitions (calling them races for now) and they need to be able to specify constraints - participants have to be in a certain age range, over a certain weight, maximum number of participants in a race, etc.

My Objects

RaceParticipant - for brevity, the only pertinent property is int Age.

Race:

public class Race
{
    public virtual int RaceId { get; set; }
    public virtual string RaceName { get; set; }
    public virtual int RaceTypeId { get; set; }
    public virtual TrainingType RaceType { get; set; }
    public virtual IEnumerable<RaceParticipant> RaceParticipants { get; set; }
    public virtual IEnumerable<RaceConstraint> RaceConstraints { get; set; }
}

RaceConstraint abstract class:

public abstract class RaceConstraint
{
    public string DisplayText { get; set; }
    public string ValidationText { get; set; }
    public virtual bool PassesConstraint(Race race, RaceParticipant participant); 
}

RaceConstraint implementation MaxNumberOfParticipants:

public class MaxNumberOfParticipants : RaceConstraint
{
    public int MaximumNumberOfParticipants { get; set; }

    public MaxNumberOfParticipants()
    {
        DisplayText = "Maximum number of racers";
        ValidationText = "This race has reached the maximum number of racers.";
    }

    public override bool PassesConstraint(Race race, RaceParticipant participant) 
    {
        bool result = false;
        if (race.RaceParticipants.Count() <= MaximumNumberOfParticipants)
        {
            result = true;
        }
        return result;
    }
}

RaceConstraint implementation AgeRange:

public class AgeRange : RaceConstraint
{
    public int MinimumAge { get; set; }
    public int MaximumAge { get; set; }

    public AgeRange()
    {
        DisplayText = "Age range of racers";
        ValidationText = string.Format("Racers must be between the ages of {0} and {1}.", MinimumAge, MaximumAge);
    }

    public override bool PassesConstraint(Race race, RaceParticipant participant)
    {
        bool result = false;
        if (MinimumAge <= participant.Age && participant.Age <= MaximumAge)
        {
            result = true;
        }
        return result;
    }
}

The question

My question is where to store MinimumAge, MaximumAge, and MaximumNumberOfParticipants. I'm using Entity Framework code-first so I know I don't need to worry about the schema directly, but I do need to think about which properties to add to EF and which object they'll be connected to.

Currently EF would make a separate table for each implementation of RaceConstraint which seems like overkill and I definitely don't want DisplayText and ValidationText stored in the db for each instance.

I thought about moving MinimumAge, MaximumAge, and MaximumNumberOfParticipants to properties of Race because each race will have values for these, but that feels like Race and RaceConstraint would be too coupled. I'd like to be able to add a RaceConstraint in the future as requirements change without having to add a property to Race and therefor add a column to the Race db table.

I think I'm going to have to make a container object for RaceConstraint that holds a collection of all possible RaceConstraints. Would it make sense to put these properties in that object and then remove IEnumerable RaceConstraints from Race and instead add a property for the RaceConstraintContainer?

Or maybe I'm way over thinking this and there's a design pattern I can follow. I'm sure I'm not the first person to come across this scenario!

Thanks in advance.

UPDATE

After mulling this over for a few days and trying out each of the answers, I think @maple_shaft really nailed the problem of EF getting in the way of OO design. I think I found a compromise somewhere in the middle. I added properties like MinimumAge, MaximumAge, and MaximumNumberOfParticipants to the Race object because from a database perspective, these are all very related and each race will have entries for each of these.

I changed the abstract class RaceConstraint to an interface that just requires bool PassesConstraint(). I made better use of DisplayText by making it a data annotation of the properties in race (ex. [Display(Name = "Maximum number of racers")]) since I really only wanted it for labels in the UI. I moved the text from ValidationText to an exception message if a PassesConstraint() fails.

Finally, I made a RaceManager class to hold a List of RaceConstraints and handle adding users by checking each RaceConstraint. This class will also handle removing users, determining winners, etc.

I think this will work well from an MVC standpoint too because I'm not passing models to the view that have methods. The Race class will really just be there to hold EF data and will make for a clean model to use for adding or updating races from a view.

Thanks for the help everyone!

1
  • 1
    Entity Framework supports more than multiple-table inheritance: weblogs.asp.net/zeeshanhirani/archive/2008/08/16/… I'm not sure it's a good idea, in your case, to go for single-table inheritance. I'm just saying that you're not forced into a separate table, as you seem to think.
    – pdr
    Jan 18, 2013 at 17:42

3 Answers 3

4

You are running into a limitation of Entity Framework that encourages what each type of object in your domain model to map directly to a single schema construct. Creating sub types of RaceConstraint sounds like a good idea from an OO perspective but then the Anemic Domain Model is a more encouraged pattern using this framework anyway.

RaceConstraint should be the entity type and all the different possible constraint properties should exist in this one entity. Display and Validation text are not well represented here, and may be more appropriate as references to an enum or a resource file as each individual constraint property in your RaceConstraint object may have a unique message.

To maintain the OO design however, you can translate your domain model to a view model that is not Anemic, however some would consider it bad practice to have business logic like PassesConstraint in your View Model or your Domain Model as well. These are the two schools of thought on this.

5
  • Agreed +1, I really think EF is getting in the way of good OO design here. It's times like these I begin to understand the value of document repositories. Jan 18, 2013 at 20:39
  • @JimmyHoffa And I don't think you are wrong on this either. It comes down to OO vs. Transactional based services. Transactional services basically discount OO in favor of Functional Programming. So it is the same thing as saying Functional vs. Object Oriented Programming or Gorilla vs. Shark
    – maple_shaft
    Jan 18, 2013 at 23:32
  • I'm not sure I completely follow the thread you're drawing, but it was working on a SOA in a transactional system that drove me to desire for functional programming personally. Side-effect free focus in a highly transactional system just makes sense, as do fixed boundaries provided by services; this I know, but where you made the connection to transactional services in this Q or my comment.. I'm frankly curious, I feel like what you said resonates with me but I'm not sure how you got to that from here heh. Jan 19, 2013 at 1:52
  • @JimmyHoffa If you read Martin Fowlers criticism of the Anemic Domain Model and the Transactional System then he outright compares it to full circle return to functional programming, which in his opinion is inferior to the OO approach. That was my thread of thought, I hope this was clearer.
    – maple_shaft
    Jan 19, 2013 at 4:48
  • Shark gets distracted by dolphin, gorilla punches shark for the win. Everyone knows this. Jan 19, 2013 at 6:16
4

It looks like you need to separate the Constraint metadata (DisplayText, ValidationText) from the values for each instance (MinimumAge, MaximumAge, etc.)

If the Display/Validation text isn't going to change unless you're making code updates anyway, I'd just declare them const and load them from a resource file.

MinimumAge, MaximumAge, age MaximumNumberOfParticipants should stay on the object where they have the most meaning and the least duplication. It looks like the RaceConstraint subclasses are a good place.

FWIW, you are already using something very similar to the Specification pattern with the PassesConstraint method.

4

I would implement a RangeConstraint with an indicator of the type if you're concerned about the database layout, this will give you one table for range constraints and an identifier (1=Age, 2=Weight, 3=IQ, etc), then the min and max is all you need otherwise. THen the PassesConstraint bit should just use a selector from a dictionary somewhere Dictionary<RangeConstraintType, Func<RaceParticipant, int>> rangeSelectors where you give it the constraint type, it returns a function that given a participant will give back the value to be validated against the range. Something like:

var _rangeSelectors = new Dictionary<RangeConstraintType, Func<RaceParticipant, int>>() {
    { RangeConstraintType.Age, (participant) => participant.Age },
    { RangeConstraintType.Weight, (participant) => participant.Weight },
    ...
};

Then in your PassesConstraint it just..

public override bool PassesConstraint(Race race, RaceParticipant participant)
{
    var participantsMetric = _rangeSelectors[_rangeConstraintType](participant);
    return participantsMetric < maxValue && patricipantsMetric > minValue;
}
2
  • Would this work against metrics that aren't integers? I may compare against strings or complex types too.
    – allen.mn
    Jan 18, 2013 at 20:24
  • @allen.mn I was saying you have a supertype here of RangeConstraint, and presume it's going to have numbers that it's checking ranges on. There are presumably other Constraint types aside from RangeConstraint, but look for commonalities where you can abstract a main constraint type out of them where possible such as I have shown here. Jan 18, 2013 at 20:36

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.