views:

144

answers:

3

I have a system which I've been wrestling with for a while. Essentially, it uses a lot of abstraction to deal with the fact that later extension is not just expected, but necessary. One place this is required is data access. The system generally deals with managing objects encapsulating some observation (in the sense of an observed value or set of values) and using them. To this end, I have something to the effect of:

public interface Observation 
{
    /** UniqueKey is used to access/identify an observation */
    UniqueKey Key
    {
        get;
    }
}

public interface ObservationDataSource
{
    /** 
      * Retrieves an Observation from the actual data source.
      * performs any necessary operations to encapsulate values in object
      */
    Observation GetObservationByUniqueKey(UniqueKey key);
}

The problem arises for specific implementations of these interfaces. Eventually, the Observation and ObservationDataSource class are implemented with specific runtime classes. However, UniqueKey may also be extended to deal with whatever the uniquely identifying set of values are for an observation in the data source (maybe an id, maybe a time, etc). So any implementation of GetObservationByUniqueKey will expose a UniqueKey argument, but expect a specific subclass. I expect the UniqueKey to be casted to a specific type once passed in.

This seems like a poor design choice, since the implementation is lying about the argument requirements -- but I can't see another way of doing this. I expect other people to be using these interfaces, so I can't just say that I'll remember this convention.

Any ideas to fix it or handle it more elegantly?

A: 

I'm not sure if this is what you want, but it seems to me you could try doing it with generics:

public interface Observation<T> where T : UniqueKey
{
    T Key { get; }
}

public interface ObservationDataSource<T> where T : UniqueKey
{
    Observation<T> GetObservationByUniqueKey(T key);
}

Now the interface is strongly typed with the specific subclass of UniqueKey that your class requires.

Neil Williams
A: 

I do not see this as a problem, simply by amending this one statement of yours:

So any implementation of GetObservationByUniqueKey will expose a UniqueKey argument, but expect a specific subclass if and only if that UniqueKey was generated by this ObservationDataSource.

If the UniqueKey is not of the expected type, that's just a trivial rejection case that can be handled one of two ways:

(1) By exposing a UniqueKeyType property in your ObservationDataSource interface, the caller of GetObservationByUniqueKey can check the UniqueKey's instance type a priori.

(2) It becomes the contractual responsibility of each GetObservationByUniqueKey implementor to handle the case where the UniqueKey was not generated by it. (This seems perfectly reasonable to me)

However, your entire issue begs the question - why are you allowing UniqueKey to be polymorphic in the first place, when you already have a defined lookup function to get at your data objects?

Not Sure
Are you saying make the UniqueKey a class with some sort of collection in it that holds all the key values (in the case of a composite key)? Or am I missing the point?
harley.holt
I'm saying the opposite - UniqueKey should be as simple a class as possible, I don't see what any real benefit of allowing it to be inherited from.
Not Sure
A: 

You say

So any implementation of GetObservationByUniqueKey will expose a UniqueKey argument,

Howerver, this is incorrect. It will not expose the argument - it will receive one. As you say, the caller might pass arbitrary unique keys, and there is nothing wrong with that.

For a specific data source, only specific unique keys will have associated observation - not just specific wrt. type, but also specific wrt. to actual value. If the value has no observation associated, you return null (I suppose). Do the same if the type of the uniqueid is incorrect - if somebody passes a time when an ID is expect, no observation is associated with that key, in that data source.

OTOH, if the caller is free to pick unique ids, and the data source is supposed to return an empty observation to be filled by the caller, then you have two choices:

  1. Don't use interfaces in the first place. Apparently, you cannot substitute one implementation for the other, and the caller must be aware what implementation they use.
  2. Alternatively, make sure that all implementations support all kinds of unique keys.
Martin v. Löwis