1
\$\begingroup\$

I'm creating a factory design for math problems. The purpose of the factory is:

  • The problems must be generated with a certain difficulty (or level). To do this, I've got an abstract method called ConfigureLevels.
  • I set an abstract method called Generate, this one must be implemented in a concrete class.

This is the factory design which I'm talking to.

enter image description here

Here is the abstract ProblemFactory.

  • The abstract factory provide a Random variable.
  • Also contains a CanConfigureXLevel() which by default returns false, but if you want that will be available, just override it to true. ConfigureXLevel() is an abstract method which knows how to configure the level (returns IConfigure).
  • Contains a dictionary which contains the available levels (key:=Levels, value:=IConfiguration which works like a container of objects usefull to generate the problem (for example binary and times tables both needs two bound objects)).

_

public abstract class ProblemFactory
{
    private IDictionary<Levels, IConfiguration> Configurations = new Dictionary<Levels, IConfiguration>();
    protected Random Random = new Random();

    public ProblemFactory()
    {
        LoadLevels();
    }

    protected abstract Problem Generate();

    public virtual bool CanConfigureEasyLevel() { return false; }
    public virtual bool CanConfigureMediumLevel() {return false; }
    public virtual bool CanConfigureHardLevel() {return false; }

    protected abstract IConfiguration ConfigureEasyLevel();
    protected abstract IConfiguration ConfigureMediumLevel();
    protected abstract IConfiguration ConfigureHardLevel();

    private void LoadLevels()
    {
        if (CanConfigureEasyLevel()) {
            Configurations.Add(Levels.Easy, ConfigureEasyLevel());
        }

        if (CanConfigureMediumLevel()) {
            Configurations.Add(Levels.Medium, ConfigureMediumLevel());
        }

        if (CanConfigureHardLevel()) {
            Configurations.Add(Levels.Hard, ConfigureHardLevel());
        }
    }

    private void Configure(Levels level)
    {
        if (!Configurations.ContainsKey(level)) throw new InvalidOperationException("Level not available");

        // ..
    }
} 

And here is a concrete class, check how I'm overriding some ConfigureXLevel from the abstract ProblemFactory. The factory create additions problems, and it should know how to calculates.

public class AdditionProblemFactory : ProblemFactory
{
    public AdditionProblemFactory(Levels level)
        : base(level) { //.. }

    public override Problem Generate(IConfiguration configuration) 
    {
        int x = //.. x must receive a random number according to the configuration
        int y = //..
        Operators op = //..

        return ProblemA.CreateProblemA(x, y, op);
    }

    public override bool CanConfigureMediumLevel()
    {
        return true;
    }

    protected override IConfiguration ConfigureEasyLevel()
    {
        throw new NotImplementedException();
    }

    protected override IConfiguration ConfigureMediumLevel()
    {
        BinaryProblemConfiguration configuration = new BinaryProblemConfiguration();
        configuration.Bound1 = new Bound<int>(2, 10);
        configuration.Bound2 = new Bound<int>(2, 10);
        configuration.Operators = new List<Operators>() { Operators.Addition, Operators.Subtraction, Operators.Multiplication, Operators.Division };

        return configuration;
    }

    protected override IConfiguration ConfigureHardLevel()
    {
        throw new NotImplementedException();
    }
}

And this is a BinaryProblemConfiguration

public class BinaryProblemConfiguration : IConfiguration
{
    public Bound<int> Bound1 { get; set; }
    public Bound<int> Bound2 { get; set; }
    public List<Operators> Operators { get; set; }

    public BinaryProblemConfiguration() { }
    public BinaryProblemConfiguration(Bound<int> bound1, Bound<int> bound2, List<Operators> operators)
    {
        this.Bound1 = bound1;
        this.Bound2 = bound2;
        this.Operators = operators;
    }
}

The matter is in the Generate method from AdditionProblemFactory and TimesTablesProblemFactory, x, y and operator variables should receive random numbers according to the Level IConfiguration.

Here is the part I don't know what I should modify of the design. Maybe is better move the random variable to IConfiguration and generate numbers there.

Here is the current project, don't worry it's so compact (Factories => Infrastructure.FactoryCore | Configurations => Infrastructure.ConfigurationCore | BinaryProblem => ExerciseA):

http://www.mediafire.com/?rf3m1x9b1vdha1k

\$\endgroup\$
4
\$\begingroup\$

If I keep giving feedback on this project, I'll have to start charging consulting fees. :P

protected Random Random = new Random();

You should be sharing your Random object between all your ProblemFactories. Basically, you should only have one new Random in your entire program, and you should pass it around.

Here is the part I don't know what I should modify of the design. Maybe is better move the random variable to IConfiguration and generate numbers there.

The problem you are facing is that you've split BinaryProblemConfiguration and AdditionProblemFactory in an awkward way. The AdditionProblemFactory manages a collection of configuration as well as generating problems. That's two different things, and a class is best when it only does one thing. Move Generate into BinaryProblemConfiguration.

As for Random, pass the reference to Generate() function on IConfiguration.

protected override IConfiguration ConfigureMediumLevel()
{
    BinaryProblemConfiguration configuration = new BinaryProblemConfiguration();
    configuration.Bound1 = new Bound<int>(2, 10);
    configuration.Bound2 = new Bound<int>(2, 10);
    configuration.Operators = new List<Operators>() { Operators.Addition, Operators.Subtraction, Operators.Multiplication, Operators.Division };

    return configuration;
}

Since BinaryProblemConfiguration is mostly concerned with holding configurations, this piece of code now seems out of place. Move it outside of the class like so:

    BinaryProblemConfiguration configuration = new BinaryProblemConfiguration();
    configuration.Bound1 = new Bound<int>(2, 10);
    configuration.Bound2 = new Bound<int>(2, 10);
    configuration.Operators = new List<Operators>() { Operators.Addition, Operators.Subtraction, Operators.Multiplication, Operators.Division };
    problemFactory.AddConfiguration(Levels.Medium, configuration);

Now there is no addition specific logic in AdditionProblemFactory and you should be able to get rid of any subclasses and just have a ProblemFactory class.

I'd then extend the problem factory so that it handles multiple types of problems. I.E. it would take a ProblemType and a Level rather then just a Level.

interface IConfiguration
{
    IProblem Generate(Random random);
}

class ProblemFactory
{
    void AddConfiguration(ProblemType,Level,IConfiguration);
    IProblem Generate(ProblemType, Level);
    bool ProblemAvailable(ProblemType, Level);
}

class AdditionProblemConfiguration : IConfiguration
{
...
}

class SubtractionProblemConfiguration : IConfiguration
{
...
}

class ProblemConfiguration
{
     ProblemFactory StandardConfiguration()
     {
          ProblemFactory problemFactory = new ProblemFactory();
          // add configuration to problem factory
          // consider reading file to determine ranges of numbers for problems
          return problemFactory;
     }
}
\$\endgroup\$
  • \$\begingroup\$ thanks for take a look Winston, but I have my doubth about the design, maybe because is midnight, but I can't understand how would I use the last three classes? And this one is off topic, I'm still learning about how create good designs, I really have difficulty creating these designs.. what books would you recomend me? \$\endgroup\$ – Darf Zon Jan 29 '12 at 5:00
  • \$\begingroup\$ I agree with Winston about having just one Random instance. Without looking very closely, I don't see why you can't just have it as static protected in the most relevant (abstract?) base class. (As I proposed in my example the other day) \$\endgroup\$ – Lars-Erik Jan 30 '12 at 23:47
  • \$\begingroup\$ With the design that you propose. Supposing we need in addition problems the three levels (Easy, Medium and Hard), should I have three AdditionXProblemConfiguration? \$\endgroup\$ – Darf Zon Jan 31 '12 at 0:15
  • \$\begingroup\$ @DarfZon, no. Like your BinaryProblemConfiguration the range of the values should be set by manipulating the properties of the object. No need for extra classes. You should set those properties in the ProblemConfiguration class. There you should create three versions of each and add them all to the problemFactory \$\endgroup\$ – Winston Ewert Jan 31 '12 at 0:52

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.