Without entering in academic definitions, let's say that the Strategy Pattern is used when you have a client code (Context) which will execute an operation, and this operation could be implemented in different ways (algorithms). For instance: https://www.dofactory.com/net/strategy-design-pattern
Which Strategy (or algorithm) will be used depend on many occasions of some input conditions. That is why Strategy Pattern sometimes is used in combination with Factory Pattern. The Client pass the input conditions to the Factory. Then the Factory knows which Strategy has to create. Then the Client execute the operation of the Strategy created.
However, I have come across in several occasions with a problem that seems to me the opposite. The operation to be execute is always the same, but it would be only executed depending on a family of input conditions. For example:
public interface IStrategy
{
string FileType { get; }
bool CanProcess(string text);
}
public class HomeStrategy : IStrategy
{
public string FileType => ".txt";
public bool CanProcess(string text)
{
return text.Contains("house") || text.Contains("flat");
}
}
public class OfficeStrategy : IStrategy
{
public string FileType => ".doc";
public bool CanProcess(string text)
{
return text.Contains("office") || text.Contains("work") || text.Contains("metting");
}
}
public class StragetyFactory
{
private List<IStrategy> _strategies = new List<IStrategy>{ new HomeStrategy(), new OfficeStrategy() };
public IStrategy CreateStrategy(string fileType)
{
return _strategies.Single(s => s.FileType == fileType);
}
}
Now the client code will get the files from some repository and will save the files in the database. This is the operation, store the files in the database, just depending on the type of the file and the specific conditions for each file.
public class Client
{
public void Execute()
{
var files = repository.GetFilesFromDisk();
var factory = new StragetyFactory();
foreach (var file in files)
{
var strategy = factory.CreateStrategy(file.Type);
if (strategy.CanProcess(file.ContentText))
{
service.SaveInDatabase(file);
}
}
}
}
Am I wrong to believe that this is a different pattern than the Strategy Pattern? (even though I have called Strategy in the code above because I have seem it like this in several occasions)
If this problem is different than the one the Strategy Pattern solves, then which pattern is it?.
Not really a strategy pattern, because as definition in the strategy pattern in Wikipedia says:
In computer programming, the strategy pattern (also known as the policy pattern) is a behavioral software design pattern that enables selecting an algorithm at runtime. Instead of implementing a single algorithm directly, code receives run-time instructions as to which in a family of algorithms to use.[1]
You're not selecting an algorithm to execute at runtime, you just check conditions to see if file type satisfies conditions and then execute the algorithm.
Do you expect this to change ever ? Do you need this to be extensible, so that in the future if you need to execute different code based on file type you can do it easily.
If answer to those questions is yes, then you can keep strategies and apply few changes.
First define base strategy class that defines the code to execute
public abstract class StrategyBase
{
public abstract bool CanProcess(string fileType);
public virtual void Execute(File file)
{
_service.SaveInDatabase(file);
}
}
Your strategies change to derive from base
public class HomeStrategy : StrategyBase
{
public string FileType => ".txt";
public override bool CanProcess(string text)
{
return text.Contains("house") || text.Contains("flat");
}
}
// implement the same for the rest of strategies...
As mentioned in the comment, it's not really a factory as it doesn't create a new strategy on every call. It's more like a provider which provides strategy to execute based on file type.
public class StragetyProvider
{
private List<StrategyBase> _strategies = new List<StrategyBase>{ new HomeStrategy(), new OfficeStrategy() };
public StrategyBase GetStrategy(string fileType)
{
return _strategies.FirstOrDefault(s => s.CanProcess(fileType));
}
}
As a result client code became much simpler:
public class Client
{
public void Execute()
{
var files = repository.GetFilesFromDisk();
var provider = new StragetyProvider();
foreach (var file in files)
{
var strategy = provider.GetStrategy(file.Type);
strategy?.Execute(file);
}
}
}
Notice, when you need to add new condition, you just implement a new class that derives from StrategyBase
and add it to the list of strategies in the provider and no other changes required. If you would need to execute different logic for some new file type, you will create new strategy and override
Execute method and that's it.
If this does really look like an overkill and you don't need to ever extend this solution with new behavior & the only thing you want is to be able to add new condition then go with another approach.
public interface ISatisfyFileType
{
bool Satisfies(string fileType);
}
public class HomeCondition : ISatisfyFileType
{
public string FileType => ".txt";
public bool Satisfies(string text)
{
return text.Contains("house") || text.Contains("flat");
}
}
// the rest of conditions
Compose all conditions into one
public class FileConditions
{
private List<ISatisfyFileType> _conditions = new List<ISatisfyFileType>{ new HomeStrategy(), new OfficeStrategy() };
public bool Satisfies(string fileType) =>
_conditions.Any(condition => condition.Satisfies(fileType));
}
And the client:
public class Client
{
public void Execute()
{
var files = repository.GetFilesFromDisk();
var fileTypeConditions = new FileConditions();
foreach (var file in files)
{
if (fileTypeConditions.Satisfies(file.ContentText))
{
service.SaveInDatabase(file);
}
}
}
}
This also has the benefit of implementing a new condition and adding it to FileConditions
class should you need a new condition without touching client code.