I try to apply the strategy design pattern for parsing of some textual content where each result is represented in different class.
Minimal example.
So my interface looks like this:
public interface IParseStrategy
{
object Parse(string filePath);
}
The classes that implement the algorithm:
class ParseA : IParseStrategy
{
public object Parse(string filePath) => new ConcurrentQueue<ParseAData>();
}
class ParseB : IParseStrategy
{
public object Parse(string filePath) => new Dictionary<string, ParseBData>();
}
The specific "Data" classes:
class ParseAData
{
public int Id { get; set; }
public string Name { get; set; }
}
class ParseBData
{
private byte[] data;
public byte[] GetData()
{
return data;
}
public void SetData(byte[] value)
{
data = value;
}
}
The context class that defines the interface of interests for the clients:
class Context
{
private IParseStrategy _strategy;
private void SetParsingStrategy(IParseStrategy parseStrategy)
{
_strategy = parseStrategy;
}
public object TryParse(string filePath, TypeToParse typeToParse)
{
object parsedContent = new object();
try
{
switch (typeToParse)
{
case TypeToParse.A:
SetParsingStrategy(new ParseA());
parsedContent = _strategy.Parse(filePath);
break;
case TypeToParse.B:
SetParsingStrategy(new ParseB());
parsedContent = _strategy.Parse(filePath);
break;
throw new ArgumentOutOfRangeException(nameof(typeToParse), "Uknown type to parse has been provided!");
}
}
catch (Exception)
{
throw;
}
return parsedContent;
}
}
The enum where the client can choose the right algorithm
public enum TypeToParse { A, B }
and finally the main method:
static void Main(string[] args)
{
var context = new Context();
ConcurrentQueue<ParseAData> contentOfA = (ConcurrentQueue<ParseAData>)context.TryParse("file1.whatever", TypeToParse.A);
Dictionary<string, ParseBData>contentOfB = (Dictionary<string, ParseBData>)context.TryParse("file2.whatever", TypeToParse.B);
}
So, my problem here is that the client has to know the classes in order to cast the return type object
.
How to rewrite this to a more generic way, so that the compiler would infer the types automatically by using the var
keyword, so the call would look like:
var contentOfA = context.TryParse("file1.whatever", TypeToParse.A);
var contentOfB = context.TryParse("file2.whatever", TypeToParse.B);
with the yellow marked types infered:
You are implementing the strategy pattern in a wrong way. Please have a read again about the strategy patterns and try to understand how to use it. I will try to point the issues here. The idea is to inject a logic inside your codebase based on an input or state, the most important part it, the type of input or output does not change.
Problem 1, The context should never know there are multiple strategies available. It is only aware of the fact that it has an implementation of a strategy and it has to use the strategy to perform some action and return the result.
So, the context class
public object TryParse(string filePath, TypeToParse typeToParse)
{
object parsedContent = new object();
try
{
switch (typeToParse)
{
case TypeToParse.A:
SetParsingStrategy(new ParseA());...
break;
case TypeToParse.B:
SetParsingStrategy(new ParseB());...
break;
}
}
....
}
is a violation of this. It has a switch-case that knows about the types, which is not possible. A proper implementation would be something like -
public object TryParse(string filePath, TypeToParse typeToParse)
{
object parsedContent = _this._stategy.Parse(filePath); //it should never know which implementation is supplied, in other words wich strategy is applied. Only at runtime t will be decided.
}
Problem 2, The two class implementation of the strategy has implementations like this -
class ParseA : IParseStrategy
{
public object Parse(string filePath) => new ConcurrentQueue<ParseAData>();
}
class ParseB : IParseStrategy
{
public object Parse(string filePath) => new Dictionary<string, ParseBData>();
}
Which also a violation. Why you ask? Because the code calling the individual classes has to know what they return. The strategy pattern is a pattern, it does not care whether c# supports it or not. object
is a c# specific goodies that you can use to typecast any object. But that does not mean using object solves everything. Even if the return types are same (object
) the underlying actual objects are not and thus this cannot be an implementation of strategy pattern. The strategies are dynamically injected and thus no one should have hard coded dependencies on them. One possible implementation could be -
interface IData
{
}
class ParseAData : IData
{
public int Id { get; set; }
public string Name { get; set; }
}
class ParseBData : IData
{
private byte[] data;
public byte[] GetData()
{
return data;
}
public void SetData(byte[] value)
{
data = value;
}
}
public interface IParsedObject
{
void process(<Some other dependencies>);
}
public class ConcurrentQueue<T> : IParsedObject where T: ParseAData
{
}
public class ParsedDictionary<T> : IParsedObject where T: ParseBData
{
}
public interface IParseStrategy
{
IParsedObject Parse(string filePath);
}
//the method will be somesiliar to this
public IParsedObject TryParse(string filePath, TypeToParse typeToParse)
{
IParsedObject parsedContent = _this._stategy.Parse(filePath); //it should never know which implementation is supplied, in other words wich strategy is applied. Only at runtime t will be decided.
}
class ParseA : IParseStrategy
{
public IParsedObject Parse(string filePath) => new ConcurrentQueue<ParseAData>();
}
class ParseB : IParseStrategy
{
public IParsedObject Parse(string filePath) => new Dictionary<string, ParseBData>();
}
with these modifications you can now write -
static void Main(string[] args)
{
var contextA = new Context();
contentA.SetParsingStrategy(new ParseA());
var contextB = new Context();
contextB.SetParsingStrategy(new ParseB());
var contentOfA = contextA.TryParse("file1.whatever", TypeToParse.A);
var contentOfB = contextB.TryParse("file2.whatever", TypeToParse.B);
}
or
static void Main(string[] args)
{
var context = new Context();
contentA.SetParsingStrategy(new ParseA());
var contentOfA = context.TryParse("file1.whatever", TypeToParse.A);
context.SetParsingStrategy(new ParseB());
var contentOfB = context.TryParse("file2.whatever", TypeToParse.B);
}
Info The strategy pattern works only if your classes, who are using the strategy, does not hard code the dependencies, that is the whole idea of it.
The example you have is probably not a good one for strategy pattern. I tried to fix it as much as possible so that you get a good understanding of what was wrong in your implementation. Not all patterns supports all scenarios. Here is an example implementation of strategy pattern, which is much similar to yours https://refactoring.guru/design-patterns/strategy/csharp/example.
I hope this helps.
Note The code I supplied is not working versions. They probably won't even compile, they are merely there to express the idea behind strategy pattern. The proper implementation will have different codes for each of the classes ParseA
and ParseB
More Strategy patter and IoC, (Inversion of Cntrol) goes hand in hand. Try learning IoC and you will find strategy pattern much more easier to learn. https://en.wikipedia.org/wiki/Inversion_of_control
The simple answer is you can't, get around this problem:
So, my problem here is that the client has to know the classes in order to cast the return type object.
What ever answer that is given the user needs to know the type of object that is going to be returned, this will not be known until runtime or possibly before by the user of the context class only with knowledge of the internal implementation of the Context
class.
However this type of code would not be suitable as the only thing your interface guarantee is that an object is being returned.
If users of the Context
have to pass in the option to the function TypeToParse
users of the context class would be better having 2 functions, that return the correct type, e.g.
class Context
{
public ParseAData ParseAsA(string filePath)
{
...
}
public ParseBData ParseAsB(string filePath)
{
...
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With