3

I wish to create an online quiz that can ask any question from thousands of programmed questions. Each question is created via a function that is given an array of int whose values determine the exact question displayed. I have each question as a class:

public class AddingTwoDigitNumbers : IQuestion
{
    public string QName() { return "Adding Two-Digit Numbers" };
    public int[] QParams() { return int[]() {Random(10, 99), Random(10, 99) };
    public void Question(int[] values) {
        Console.WriteLine(string.Format("What is {1} + {2}?", values[0], values[1]);
    }
    public void Answer(int[] values) {
        Console.WriteLine(values[0] + values[1]).ToString());
    }
}

QParams creates the array of int (to determine exactly the question created), that is given to both Question and Answer to create the question and answer.

I want a List of questions searchable by QName but would rather not have to create (and name) thousands of classes all implementing IQuestion.

So here is my second solution:

public class Question
{
    public string QName { get; set; }
    public Func<int[]> QParams { get; set; }
    public Action<int[]> Question { get; set; }
    public Action<int[]> Answer { get; set; }
}

public class QuestionRepository
{
    public static Dictionary<string, Question> Questions = new Dictionary<string, Question>();
    public static void AddQuestions(Question[] qs) {
        foreach (Question q in qs) Questions.Add(q.QName, q);
    }
}

public class FirstSetOfQuestions
{
    static void AddQuestions()
    {
        QuestionRepository.AddQuestions(new Question[]
        {
            new Question()
            {
                QName = "Adding Two-Digit Numbers",
                QParams = () => int[]() {Random(10, 99), Random(10, 99) },
                Question = (v) => {Console.WriteLine(string.Format("What is {1} + {2}?", v[0], v[1]);},
                Answer = (v) => {Console.WriteLine(values[0] + values[1]).ToString());}
            },
            new Question()
            {
                QName = "Subtracting Three-Digit Numbers",
                QParams = () => int[]() {Random(100, 999), Random(100, 999) },
                Question = (v) => {Console.WriteLine(string.Format("What is {1} - {2}?", v[0], v[1]);},
                Answer = (v) => {Console.WriteLine(values[0] - values[1]).ToString());}
            }
        }
    }
}

So my question is which is better? Do I create thousands of classes, having to provide a name for each one, or do I create thousands of anonymous functions and a class that stores these using (I assume) delegates? Is there a problem with the second solution if I have thousands of questions, or even a better way to do this?

(Obviously the questions I wish to create are much more complicated than shown here, and involve fractions, algebra etc.)

Soner Gönül
  • 97,193
  • 102
  • 206
  • 364
CapIsland
  • 129
  • 1
  • 9
  • 4
    Why not store the question data in DB ? – Alex Feb 11 '15 at 12:22
  • I can see why you need an anonymous function for the answer part, but the question part sounds like it could be done just with a formatting string, and the parameters could be an enum of common types, e.g. TwoDigitInteger. – Jon Skeet Feb 11 '15 at 12:27
  • Each generated int[] that represents a particular question will be stored in a DB or file so that I know what questions have been set. But that's not my problem. You can't store in a DB the functions that create each question, so they have to be hardcoded. – CapIsland Feb 11 '15 at 12:28
  • 3
    You can store an expression, and you can even store C# code and compile it on the fly. – SimpleVar Feb 11 '15 at 12:29
  • Skeet, sometimes the question part is very complicated, such as creating a y = mx + c line using fractions for m and c. – CapIsland Feb 11 '15 at 12:29
  • Nathan, point me in the direction of how to store an expression or code, please. – CapIsland Feb 11 '15 at 12:30
  • 1
    I'm not familiar with any good expression serialization library, but storing code is extremely simple. You literally store the text of the code (e.g `"x + y = Math.PI"`). To compile it later, you use the `CSharpCodeProvider`, adding required namespaces and variable declarations as needed. – SimpleVar Feb 11 '15 at 12:34
  • @CapIsland: who (how, when) will populate the questions list? I think, it's a primary question. Without answering it, it is hard to say, what storage format will be suitable, and how the model should look like. – Dennis Feb 11 '15 at 12:35
  • I am the only one who creates the questions. At the moment, I am hard-coding them. – CapIsland Feb 11 '15 at 12:35
  • @YoryeNathan: store C# code in DB, that's a very bad idea. But an expression could be stored like `{x} + {y}`. Then you can parse it at runtime. There's even a [`DataTable.Compute` trick](http://stackoverflow.com/a/18796518/284240) to calculate (simple) expressions. – Tim Schmelter Feb 11 '15 at 12:37
  • It's pretty damn crazy to hardcode all those questions at once. I imagine you'd want to have some management interface to add questions to the database. – SimpleVar Feb 11 '15 at 12:37
  • Nathan, thanks for the suggestion, and I am now reading http://stackoverflow.com/questions/28226271/compile-funct1-tresult-from-string-on-the-fly – CapIsland Feb 11 '15 at 12:38
  • @TimSch I agree that storing the expression is a lot better, just pointed out a different approach which can also be viable. Note that I doubt the `Compute` technique can be used for `Sum[x..y](root(sin(x^2) + y*root(log(x)) * Pi))`, or whatever complex math he plans on having. – SimpleVar Feb 11 '15 at 12:39
  • A lot of the functions were becoming repetitive, and I was considering some kind of macro system/pseudo code system for representing them. I have written about 500 so far, so you can imagine the code is bloating. – CapIsland Feb 11 '15 at 12:40
  • @YoryeNathan: yes, it's limited. I just wanted to mention it anyway. He probably needs a different approach. – Tim Schmelter Feb 11 '15 at 12:41
  • Some of the questions ask for the solutions of quadratics or the point where two lines cross, so they are complicated. – CapIsland Feb 11 '15 at 12:43
  • 1
    @CapIsland: if so, I think, the better way is `IQuestion` implementations, bundled together by some area of application into assemblies (e.g., "Trigonometry questions"). Load them as plugins at run-time, and leave the quiz "core" unchanged, when you'll need to add another question. DB with C# code + some kind of admin tool won't help you, because then you'll need good code and/or expression editor (while now you already have it - Visual Studio). – Dennis Feb 11 '15 at 12:44
  • Dennis, I'll have to look into plugins as well. But if I need a list of ALL available questions so that a question type can be chosen, won't I need to load all the plugins anyway? – CapIsland Feb 11 '15 at 12:50
  • 1
    @CapIsland "Skeet"? That's Mr Skeet to you! –  Feb 11 '15 at 12:52
  • You can certainly shorten the coding by wrapping it all with something that'll allow fluent syntax. Off the top of my head, it'll be looking like [this](http://pastebin.com/w2f95Jbr) – SimpleVar Feb 11 '15 at 12:53
  • @CapIsland: not really. Assemblies - yes, you'll need to load them, but you don't need to instantiate questions. Actually, you have to refactor `IQuestion` to make `QName` a part of type declaration (this should be an attribute). – Dennis Feb 11 '15 at 12:53
  • @Poldie It's just different naming conventions, chill – SimpleVar Feb 11 '15 at 12:53
  • @YoryeNathan Well, it's incorrect, rather than different, given that many people would be offended by being referred to in that way! –  Feb 11 '15 at 12:56
  • Apologies to Mr Skeet if any offence was caused. :) And feel free to call me Cap or Island. – CapIsland Feb 11 '15 at 13:01
  • Dennis, surely instantiating the questions (which I probably don't need to do anyway - I just need static functions) won't change much - the compiled code is still hardcoded once the modules are in memory. – CapIsland Feb 11 '15 at 13:03
  • Oh and thanks for two ideas: firstly, I need to split off the int[] that represents the question from the functions that display the question and answer - this stops repeating the same display functions. I also need to split off the names from the code, so that the names can be searched without necessarily loading the function that creates the int[] or the functions that display the question and answer. – CapIsland Feb 11 '15 at 13:05

2 Answers2

3

Just to get you started with fluent syntax, throwing in some stubs and ideas in there as well.

class Question
{
    public string Name { get; set; }
    public string QuestionFormat { get; set; }
    public List<Range> Args { get; set; }
    public Expression<Func<int[], int>>  ValExp { get; set; }

    public Question(string name, string questionFormat)
    {
        this.Name = name;
        this.QuestionFormat = questionFormat;
        this.Args = new List<Range>();
    }

    public Question Rand(int min, int max)
    {
        this.Args.Add(new Range(min, max));
        return this;
    }

    public void Val(Expression<Func<int[], int>> exp)
    {
        this.ValExp = exp;
    }

    public CompiledQuestion Compile()
    {
        // Generate args in the appropriate ranges
        // Evaluate the result with the ValExp
        // Return a new CompiledQuestion with the information -
        //    basically just replacing Args, ValExp with RealArgs, Val
    }

    public ICoolDataObject Save()
    {
    }

    public static Question Load(ICoolDataObject hmm)
    {
    }
}

class Range
{
    public int Min { get; set; }
    public int Max { get; set; }

    public Range(int min, int max)
    {
        this.Min = min;
        this.Max = max;
    }
}

It's almost fun, creating questions now:

new Question("simple addition",
             "whats {0} + {1}?")
    .Rand(10, 99)
    .Rand(10, 99)
    .Val(v => v[0] + v[1]);

You can obviously add some validation checks to avoid bad number of arguments due to late hours of work, and use double or decimal instead of int wherever.

SimpleVar
  • 14,044
  • 4
  • 38
  • 60
2

Both approaches are wrong. I presume you are not going to have thousands of different types of calculations. You are only going to have a dozen or a few dozen different types of calculations, operating on a huge variety of data.

So, you need to normalize your data so as to end up with about a dozen or a few dozen different well defined calculations on a database of well defined data, end then write about a dozen or a few dozen classes, one for each kind of calculation, only.

You might think that this is too complicated, and you might think that writing thousands of classes (or delegates, it does not really matter) might be a lot of work but each piece is small and easy, but trust me, you will bitterly regret doing it this way as soon as something needs to change on the interface or the implementation of all of these classes, and most chances are that something will need to change at some point in time.

Mike Nakis
  • 56,297
  • 11
  • 110
  • 142
  • I do have a lot of the calculations in classes. I have created classes for points, straight lines, fractions, equations, etc. This reduces the `Question` and `Answer` functions substantially. – CapIsland Feb 11 '15 at 13:16