Maybe the title has no make sense. I'm creating factories, one of them is abstract. Abstract contains a Random variable, and CanConfigureXLevel
. These one for default is false (I mean, is not available), but if you want to have it, just override it a change to true.
public abstract class ProblemFactory
{
protected Random Random = new Random();
public abstract IProblem Generate();
public virtual bool CanConfigureEasyLevel()
{
return false;
}
public virtual bool CanConfigureMediumLevel()
{
return false;
}
public virtual bool CanConfigureHardLevel()
{
return false;
}
protected abstract void ConfigureEasyLevel();
protected abstract void ConfigureMediumLevel();
protected abstract void ConfigureHardLevel();
}
A concrete class for Binary Problems (generate additions)
public class BinaryProblemFactory : ProblemFactory
{
private Bound<int> _bound1;
private Bound<int> _bound2;
public BinaryProblemFactory(Level level)
{
// ...
}
public override IProblem Generate()
{
int x = random.Next(_bound1.Min, _bound1.Max);
int y = random.Next(_bound2.Min, _bound2.Max);
Operators op = Operators.Addition;
return new BinaryProblem(x, y, operator, answer);
}
public override bool CanConfigureMediumLevel()
{
return true;
}
public override bool CanConfigureHardLevel()
{
return true;
}
protected override void ConfigureEasyLevel()
{
// ...
}
protected override void ConfigureMediumLevel()
{
this._bound1 = new Bound<int>(10, 100);
this._bound2 = new Bound<int>(10, 100);
}
protected override void ConfigureHardLevel()
{
this._bound1 = new Bound<int>(100, 1000);
this._bound2 = new Bound<int>(100, 1000);
}
}
Bound is just a class which contains Min and Max generic value.
Remember that BinaryProblemFactory contains a Random property. I'm creating several problems of mathematics, above is for addition problems, also I will create for times tables (very similar to BinaryProblem, but this is for multiplication and different bounds.
I mean, each concrete factory needs a container of utils or objects to setup the program. Binary and TimesTablesFactory need two bound properties.
My main problem is.. I need to show in a list which levels are available (above only two, medium and hard). I guess I can fix it overriding CanConfigureXLevel
if we maintain a dictionary, where the key will be a Level enum and the value will be the conditions (bound objects).
But I'm not sure what should I remove. I need a little of help.
I think that your ProblemFactory is potentially trying to do a bit too much, the factory should only have the responsibility of creating instances and knowing what types of instances to create, without having the added overhead of knowing about configurations.
With that in mind, here is how I would approach the problem:
/// <summary>
/// Each class that can generate a problem should accept a problem configuration
/// </summary>
public class BinaryProblem : IProblem
{
public BinaryProblem (ProblemConfiguration configuration)
{
// sample code, this is where you generate your problem, based on the configuration of the problem
X = new Random().Next(configuration.MaxValue + configuration.MinValue) - configuration.MinValue;
Y = new Random().Next(configuration.MaxValue + configuration.MinValue) - configuration.MinValue;
Answer = X + Y;
}
public int X { get; private set; }
public int Y { get; private set; }
public int Answer { get; private set; }
}
For this we will need a problem configuration class
/// <summary>
/// A problem configuration class
/// </summary>
public class ProblemConfiguration
{
public int MinValue { get; set; }
public int MaxValue { get; set; }
public Operator Operator { get; set; }
}
I would also a dedicated class for handling the configuration of levels and remove it from the factory class.
/// <summary>
/// The abstract level configuration allows descendent classes to configure themselves
/// </summary>
public abstract class LevelConfiguration
{
protected Random Random = new Random();
private Dictionary<Level, ProblemConfiguration> _configurableLevels = new Dictionary<Level, ProblemConfiguration>();
/// <summary>
/// Adds a configurable level.
/// </summary>
/// <param name="level">The level to add.</param>
/// <param name="problemConfiguration">The problem configuration.</param>
protected void AddConfigurableLevel(Level level, ProblemConfiguration problemConfiguration)
{
_configurableLevels.Add(level, problemConfiguration);
}
/// <summary>
/// Removes a configurable level.
/// </summary>
/// <param name="level">The level to remove.</param>
protected void RemoveConfigurableLevel(Level level)
{
_configurableLevels.Remove(level);
}
/// <summary>
/// Returns all the configurable levels.
/// </summary>
public IEnumerable<Level> GetConfigurableLevels()
{
return _configurableLevels.Keys;
}
/// <summary>
/// Gets the problem configuration for the specified level
/// </summary>
/// <param name="level">The level.</param>
public ProblemConfiguration GetProblemConfiguration(Level level)
{
return _configurableLevels[level];
}
}
This would allow the Binary Configuration to look something like this:
/// <summary>
/// Contains level configuration for Binary problems
/// </summary>
public class BinaryLevelConfiguration : LevelConfiguration
{
public BinaryLevelConfiguration()
{
AddConfigurableLevel(Level.Easy, GetEasyLevelConfiguration());
AddConfigurableLevel(Level.Medium, GetMediumLevelConfiguration());
AddConfigurableLevel(Level.Hard, GetHardLevelConfiguration());
}
/// <summary>
/// Gets the hard level configuration.
/// </summary>
/// <returns></returns>
private ProblemConfiguration GetHardLevelConfiguration()
{
return new ProblemConfiguration
{
MinValue = 100,
MaxValue = 1000,
Operator = Operator.Addition
};
}
/// <summary>
/// Gets the medium level configuration.
/// </summary>
/// <returns></returns>
private ProblemConfiguration GetMediumLevelConfiguration()
{
return new ProblemConfiguration
{
MinValue = 10,
MaxValue = 100,
Operator = Operator.Addition
};
}
/// <summary>
/// Gets the easy level configuration.
/// </summary>
/// <returns></returns>
private ProblemConfiguration GetEasyLevelConfiguration()
{
return new ProblemConfiguration
{
MinValue = 1,
MaxValue = 10,
Operator = Operator.Addition
};
}
}
Now the factory should only be responsible for creating new instances of problems and knowing what types of problems it can serve
/// <summary>
/// The only responsibility of the factory is to create instances of Problems and know what kind of problems it can create,
/// it should not know about configuration
/// </summary>
public class ProblemFactory
{
private Dictionary<Type, Func<Level, IProblem>> _registeredProblemTypes; // this associates each type with a factory function
/// <summary>
/// Initializes a new instance of the <see cref="ProblemFactory"/> class.
/// </summary>
public ProblemFactory()
{
_registeredProblemTypes = new Dictionary<Type, Func<Level, IProblem>>();
}
/// <summary>
/// Registers a problem factory function to it's associated type
/// </summary>
/// <typeparam name="T">The Type of problem to register</typeparam>
/// <param name="factoryFunction">The factory function.</param>
public void RegisterProblem<T>(Func<Level, IProblem> factoryFunction)
{
_registeredProblemTypes.Add(typeof(T), factoryFunction);
}
/// <summary>
/// Generates the problem based on the type parameter and invokes the associated factory function by providing some problem configuration
/// </summary>
/// <typeparam name="T">The type of problem to generate</typeparam>
/// <param name="problemConfiguration">The problem configuration.</param>
/// <returns></returns>
public IProblem GenerateProblem<T>(Level level) where T: IProblem
{
// some extra safety checks can go here, but this should be the essense of a factory,
// the only responsibility is to create instances of Problems and know what kind of problems it can create
return _registeredProblemTypes[typeof(T)](level);
}
}
Then here is how you could use all this
class Program
{
static void Main(string[] args)
{
ProblemFactory problemFactory = new ProblemFactory();
BinaryLevelConfiguration binaryLevelConfig = new BinaryLevelConfiguration();
// register your factory functions
problemFactory.RegisterProblem<BinaryProblem>((level) => new BinaryProblem(binaryLevelConfig.GetProblemConfiguration(level)));
// consume them
IProblem problem1 = problemFactory.GenerateProblem<BinaryProblem>(Level.Easy);
IProblem problem2 = problemFactory.GenerateProblem<BinaryProblem>(Level.Hard);
}
}
Of course, If you just need away to abstract your configurations then, you may not need the factory, it all depends on how you intend to use it.
IProblem problem3 = new BinaryProblem(binaryLevelConfig.GetProblemConfiguration(Level.Easy));
Possible improvements
Further to this, if one problem class always has a problem configuration this could further be improved to:
/// <summary>
/// Each class that can generate a problem should accept a level configuration
/// </summary>
public class BinaryProblem : IProblem
{
private static BinaryLevelConfiguration _levelConfiguration = new BinaryLevelConfiguration();
public BinaryProblem (Level level)
{
ProblemConfiguration configuration = _levelConfiguration.GetProblemConfiguration(level);
// sample code, this is where you generate your problem, based on the configuration of the problem
X = new Random().Next(configuration.MaxValue + configuration.MinValue) - configuration.MinValue;
Y = new Random().Next(configuration.MaxValue + configuration.MinValue) - configuration.MinValue;
Answer = X + Y;
}
public int X { get; private set; }
public int Y { get; private set; }
public int Answer { get; private set; }
}
Then all you would need to do is:
IProblem problem4 = new BinaryProblem(Level.Easy);
So it all boils down to how you need to consume all this. The moral of this post would be, there is no need to try and shoehorn configuration in an Abstract factory if all you need is config, all the factory should do is create instances and know what types to create, thats about it, but you may not reaaaally need it :)
Good luck!