Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Generic Identity Map in C#. Don't want public constructor

I'm trying to implement an identity map using generics. I have an abstract class, Entity, and a derivation constraint on my map for Entity. Since my map needs to be able to instantiate entities, my map also has a constructor constraint.

However, for the map to be useful, Entity subclasses should not be able to be instantiated from client code, which means I would want an internal constructor and no public constructors. This conflicts with the constructor constraint, though.

Is there something I'm missing? Is there some way of refactoring this to get the desired result?

The following code compiles as-is but, ideally, Entity's subclasses' constructors would be internal:

public abstract class Entity
{
    public int Id { get; protected internal set; }
}

public sealed class Widget : Entity
{
    // Client code should not be allowed to instantiate entities.
    // But the constraints on EntityMap require that entities have
    // a public constructor.
    public Widget() { }
}

public sealed class Gadget : Entity
{
    public Gadget() { }
}

// The new() constraint is required so that Get() can instantiate Ts.
public class EntityMap<T> where T : Entity, new()
{
    private Dictionary<int, T> _entities = new Dictionary<int, T>();
    private object _getLock = new object();

    public T Get(int id)
    {
        lock (_getLock)
        {
            if (!_entities.ContainsKey(id))
                _entities.Add(id, new T() { Id = id });
        }

        return _entities[id];
    }

    // Client code should not be allowed to instantiate maps.
    internal EntityMap() { }
}

// Ideally, the client would only be able to obtain Entity
// references through EntityMaps, which are only accessible
// through the ApplicationMap.
public static class ApplicationMap
{
    public static EntityMap<Widget> Widgets = new EntityMap<Widget>();
    public static EntityMap<Gadget> Gadgets = new EntityMap<Gadget>();
}
like image 578
Lobstrosity Avatar asked Sep 10 '09 15:09

Lobstrosity


2 Answers

Instead of requiring a constructor constraint, pass a Func<T> to the map constructor. That way the constructor can be internal, but the map can still effectively call it:

public class EntityMap<T> where T : Entity
{
    private readonly Dictionary<int, T> _entities = new Dictionary<int, T>();
    private readonly object _getLock = new object();
    private readonly Func<T> _entityGenerator;

    public T Get(int id)
    {
        lock (_getLock)
        {
            T ret;
            if (!_entities.TryGetValue(id, ret))
            {
                ret = entityGenerator();
                newEntity[id] = ret;
                ret.Id = id;
            }

            return ret;
        }
    }

    internal EntityMap(Func<T> entityGenerator)
    {
        _entityGenerator = entityGenerator;
    }
}

Then initialize it with:

EntityMap<Widget> widgetMap = new EntityMap(() => new Widget());

You could potentially make it a Func<int, T> instead and make the delegate responsible for creating an entity with the right ID. That way you could make your ID properly read-only, taking it as a parameter to the Entity constructor.

(I've taken the liberty of making your Get method more efficient, btw.)

like image 166
Jon Skeet Avatar answered Oct 27 '22 21:10

Jon Skeet


Thanks to Jon, here's the working code:

public abstract class Entity
{
    private readonly int _id;

    public int Id
    {
        get { return _id; }
    }

    internal Entity(int id)
    {
        _id = id;
    }
}

public sealed class Widget : Entity
{
    internal Widget(int id) : base(id) { }
}

public sealed class Gadget : Entity
{
    internal Gadget(int id) : base(id) { }
}

public class EntityMap<T> where T : Entity
{
    private readonly Dictionary<int, T> _entities = new Dictionary<int, T>();
    private readonly object _getLock = new object();
    private readonly Func<int, T> _entityGenerator;

    public T Get(int id)
    {
        lock (_getLock)
        {
            T entity;

            if (!_entities.TryGetValue(id, out entity))
                _entities[id] = entity = _entityGenerator(id);

            return entity;
        }
    }

    internal EntityMap(Func<int, T> entityGenerator)
    {
        _entityGenerator = entityGenerator;
    }
}

public static class ApplicationMap
{
    public static readonly EntityMap<Widget> Widgets = new EntityMap<Widget>(id => new Widget(id));
    public static readonly EntityMap<Gadget> Gadgets = new EntityMap<Gadget>(id => new Gadget(id));
}
like image 37
Lobstrosity Avatar answered Oct 27 '22 19:10

Lobstrosity