Search code examples
c#oopdesign-patternsstrategy-patterndecoupling

Avoiding coupling with Strategy pattern


I am attempting to apply the Strategy pattern to a particular situation, but am having an issue with how to avoid coupling each concrete strategy to the context object providing data for it. The following is a simplified case of a pattern that occurs a few different ways, but should be handled in a similar way.

We have an object Acquisition that provides data relevant to a specific frame of time - basically a bunch of external data collected using different pieces of hardware. It's already too large because of the amount of data it contains, so I don't want to give it any further responsibility. We now need to take some of this data, and based on some configuration send a corresponding voltage to a piece of hardware.

So, imagine the following (much simplified) classes:

class Acquisition
{
    public Int32 IntegrationTime { get; set; }
    public Double Battery { get; set; }
    public Double Signal { get; set; }
}

interface IAnalogOutputter
{
    double getVoltage(Acquisition acq);
}

class BatteryAnalogOutputter : IAnalogOutputter
{
    double getVoltage(Acquisition acq)
    {
        return acq.Battery;
    }
}

Now, every concrete strategy class has to be coupled to my Acquisition class, which is also one of the most likely classes to be modified since it's core to our application. This is still an improvement over the old design, which was a giant switch statement inside the Acquisition class. Each type of data may have a different conversion method (while Battery is a simple pass-through, others are not at all that simple), so I feel Strategy pattern or similar should be the way to go.

I will also note that in the final implementation, IAnalogOutputter would be an abstract class instead of an interface. These classes will be in a list that is configurable by the user and serialized to an XML file. The list must be editable at runtime and remembered, so Serializable must be part of our final solution. In case it makes a difference.

How can I ensure each implementation class gets the data it needs to work, without tying it to one of my most important classes? Or am I approaching this sort of problem in the completely wrong manner?


Solution

  • Ok, I hate to not give someone else the credit here, but I found a hybrid solution that works very well for my purposes. It serializes perfectly, and greatly simplifies the addition of new output types. The key was a single interface, IOutputValueProvider. Also note how easily this pattern handles the retrieval of varying ways of storing the data (such as a Dictionary instead of a parameter).

    interface IOutputValueProvider
    {
        Double GetBattery();
        Double GetSignal();
        Int32 GetIntegrationTime();
        Double GetDictionaryValue(String key);
    }
    
    interface IAnalogOutputter
    {
        double getVoltage(IOutputValueProvider provider);
    }
    
    class BatteryAnalogOutputter : IAnalogOutputter
    {
        double getVoltage(IOutputValueProvider provider)
        {
            return provider.GetBattery();
        }
    }
    
    class DictionaryValueOutputter : IAnalogOutputter
    {
        public String DictionaryKey { get; set; }
        public double getVoltage(IOutputValueProvider provider)
        {
            return provider.GetDictionaryValue(DictionaryKey);
        }
    }
    

    So then, I just need to ensure Acquisition implements the interface:

    class Acquisition : IOutputValueProvider
    {
        public Int32 IntegrationTime { get; set; }
        public Double Battery { get; set; }
        public Double Signal { get; set; }
        public Dictionary<String, Double> DictionaryValues;
    
        public double GetBattery() { return Battery;}
        public double GetSignal() { return Signal; }
        public int GetIntegrationTime() { return IntegrationTime; }
        public double GetDictionaryValue(String key) 
        {
            Double d = 0.0;
            return DictionaryValues.TryGetValue(key, out d) ? d : 0.0;
        }
    }
    

    This isn't perfect, since now there's a gigantic interface that must be maintained and some duplicate code in Acquisition, but there's a heck of a lot less risk of something being changed affecting the other parts of my application. It also allows me to start subclassing Acquisition without having to change some of these external pieces. I hope this will help some others in similar situations.