The underlying problem is one I seem to run into a lot. You have a collection of objects with some things in common, but other information about the objects may apply to some of them but not to others. Furthermore, as the application grows, the amount of such information is certain to grow over time. The challenge is to manage this in a sane way. Coding time is an issue; storage space is not.

Here is the core of my latest attempt to deal with this problem. I'll put some related classes below.

public class TournamentModel
{
    public Guid Id { get; set; }
    public string Name { get; set; }
    public HashSet<TournamentEntrantModel> Entrants { get; set; }
    public List<TournamentRoundModel> Rounds { get; set; } = new List<TournamentRoundModel>();
    public GameModel Game { get; set; }
    public Dictionary<string, ITournamentDetails> Details { get; set; }
}

Everything in the above except Details is something I expect the vast majority of tournaments to have. But then we come to things like seeding, prizes, and other properties that a tournament may or may not have. As a further challenge, neither seeding nor prizes would always take the same form. For example, a seeding may or may not seed all players, and some seedings put players into groups rather than ordering them linearly. One tournament might award prizes for 1st through 3rd places, while another might award a prize for most fun to play against.

Here are the details classes for LinearSeeding and for PlacePrizes:

public class LinearSeedingModel: ITournamentDetails
{
    public List<TournamentEntrantModel> SeededEntrants { get; set; }
}

public class PlacePrizesModel: ITournamentDetails
{
    public List<string> Prizes { get; set; }
}

Here is the TournamentDetailsService that glues things together. It could be expected to grow over time.

public class TournamentDetailsService
{
    public const string LinearSeedingKey = "LinearSeeding";
    public const string PlacePrizesKey = "PlacePrizes";

    public LinearSeedingModel GetSeeding(TournamentModel tournament)
        => tournament.Details.GetValue(LinearSeedingKey) as LinearSeedingModel;

    public void SetSeeding(TournamentModel tournament, LinearSeedingModel seeding)
        => tournament.Details.SetValue(LinearSeedingKey, seeding);


    public PlacePrizesModel GetPlacePrizes(TournamentModel tournament)
        => tournament.Details.GetValue(PlacePrizesKey) as PlacePrizesModel;

    public void SetPlacePrizes(TournamentModel tournament, PlacePrizesModel prizes)
        => tournament.Details.SetValue(PlacePrizesKey, prizes);
}

And here is the DictionaryExtensions class used by the service:

public static class DictionaryExtensions
{
    public static T GetValue<T>(this Dictionary<string, T> dictionary, string key, T defaultValue = default(T))
    => dictionary.ContainsKey(key) ? dictionary[key] : defaultValue;

    public static void SetValue<T>(this Dictionary<string, T> dictionary, string key, T value)
        where T : class
    {
        if (value == null)
        {
            dictionary.Remove(key);
        }
        else
        {
            dictionary[key] = value;
        }
    }
}

If I go with the above pattern, it might get repeated a lot. For example, one could imagine Details fields on both TournamentEntrantModel and TournamentRoundModel.

As an alternative, I haven't ruled out having a fat TournamentModel class with a list of properties that would be certain to grow over time, and many of which would not be applicable to a particular tournament. No idea if that is likely to be more trouble or less.

The model would be persisted via Sql (not shown, but it wouldn't be a problem) and displayed in various front ends, including web and probably eventually mobile. In general, classes that need to access details would have the TournamentDetailsService injected in their constructor.

  • 1
    What is your problem? What is your question? How to work with it? How to persist it? How to display it? Data modeling is means to an end, not excersise on it's own. – Euphoric Nov 12 at 13:58
  • I've edited my question above, adding some detail, and bolding the key issue – William Jockusch Nov 12 at 14:07

If I understand your problem correctly, then you want to model all these heterogeneous/mixed object types that only have some subset of functionality in common, while simultaneously wanting to deal with them as uniformly as possible (ex: storing them all in one polymorphic container).

That can pose a lot of design challenges because it might be too difficult to come up with one central abstraction that unifies all these things (at the very least the amount of thought required upfront might skyrocket as well the cost of changing your mind later) without just creating some hasty monolithic interface which models the superset of all functionality required of which much of it is often irrelevant for a given subtype. If that's the general problem, then I've encountered some solutions (might have missed some):

1) Just do type checking and downcasting when needed. I'm not saying that's a good solution whatsoever; I'm just listing as one I've encountered. But that said, as smelly and as problematic as that can be, it might be arguably preferable to the worst of the alternatives: the monolithic interface. The monolithic interface tends to defeat a lot of the point of abstractions and polymoprhism since if you have this uber interface for which many functions are often irrelevant for a particular type, then it often has the practical effect of requiring the user to sort of know what type of thing they're dealing with (not to mention overwhelming them completely). In those cases it might sometimes be preferable to downcast here and there if the alternative is some uber interface consisting of 300 methods to wade through in documentation while 250 of them are irrelevant for the thing you're working with.

2) This isn't always applicable but sometimes I find I can just use more than one container if the number of analogical subtypes (or "sub-interfaces') is relatively small in number and can be mostly, if not entirely, anticipated in advance.

As an oversimplified GUI example, consider that we have like a generalized Control interface which has functionality all controls easily have in common (no empty implementations of any virtual function) but we also have like a Text interface specific for controls that input text and a Button interface specific for clickable controls. In that case I might store these controls in 3 separate lists.

Controls which implement the Text interface go into a list of Text controls in addition to the generalized Control list.

Controls which implement the Button interface go into a list of Button controls in addition to the generalized Control list.

Then when the code doesn't need any of these specific interfaces and can just operate on a reference/pointer to Control like when painting controls, it just iterates through the most generalized Control list and uses the most generalized abstraction which unifies all controls. But when the code needs to specifically deal with text controls, then it can iterate through the Text list to retrieve controls implementing that interface instead, and that avoids the need to do type checking and downcasting when applicable.

Pragmatically it's worth realizing in the worst case that you can always do like:

for each text_control in text_controls:
    do_something_general(text_control);
for each button_control in button_controls:
    do_something_specific(button_control);

function do_something_general(Control control)
    // Work through the most generalized abstraction.

function do_something_specific(Button button)
    do_something_general(button);
    // Now do something specific with the button.

Which is a little bit of a hassle to write multiple loops like so and poses maintenance issues which could become serious if you keep adding more interfaces to handle separately from the generalized abstraction in hindsight while simultaneously having many cases which need to do the sort of thing above, but it can be a reasonable solution if the number of interfaces can be largely anticipated upfront and you write some utility functions to help you loop through these controls without too much redundant code.

If the alternative is one uber abstract interface, then being able to split away some of the things more specific to a subset of types away from the generalized abstraction might afford you enough breathing room to get a nice design that doesn't model some superset of largely irrelevant functionality for many types without thinking too hard about it.

3) This is more like a solution to mitigate the issues of the "superset, uber abstraction" but that's to just "hard-code" the abstraction to return access to certain "sub-interfaces", and it could return null/nil when it's not relevant to that sub-type like:

// Returns null/nil if the control doesn't provide a text interface.
IText text = control.text_interface();

That still has functionality in the generalized abstraction that's not relevant to everything but it's at least not "flattened" in a way where you have like 300 methods to wade through in one centralized interface.

It looks like you're already doing that some degree with like Details. It can become a mild nuisance if you keep wanting to add new interfaces like this which aren't relevant to everything to keep having to go back and modify the central abstract interface and all the subtypes that implement it.

4) Another one I've encountered is like downcasting on steroids. An example is like Microsoft's COM design where you can do things like this:

// Returns the text interface if available or null/nil otherwise.
IText text = control.QueryInterface<IText>();

And you can generalize so that given a list of controls, you can get a list of all the ones that implement one or more interfaces with a single query, for example. If that does involve type checking and down-casting under the hood, it tends to be more manageable than littering your code with type checks and casting because the system generalizes it to one place and provides these types of queries to make that more manageable.

Another approach that uses somewhat the same basic concept just to retrieve particular interfaces (though in this case the interfaces just provide access to data) is an entity-component system where you can do like this:

// Retrieves the text component from the control entity if one is associated
// or null/nil otherwise.
TextComponent text = control_entity.get<TextComponent>();

And likewise you can generalize the ECS so that you can do sophisticated queries through a list of entities (a "scene") to fetch all the entities that are associated to/provide one or more component types.

Those are some solutions I've encountered to avoid having to design the "uber abstraction" to unify them all while simultaneously wanting to unify as much of the code which operates on the homogeneous subset of functionality (or data) which all these mixed types have in common. I might have missed some solutions and possibly some more common or specific to other languages. I'd be curious to see others.

For properties, I often find reflection (either built into the language or something you build on top of it) useful to query what properties an object has at runtime and, say, generate the corresponding UI controls based on those properties to allow you to edit the properties of a given object/type/entity.

New contributor
Dragon Energy is a new contributor to this site. Take care in asking for clarification, commenting, and answering. Check out our Code of Conduct.

Your Answer

By clicking "Post Your Answer", you acknowledge that you have read our updated terms of service, privacy policy and cookie policy, and that your continued use of the website is subject to these policies.

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