I spent some time playing around with this problem and wrote a semisolution in C to perhaps give you an idea of how I'd approach the problem (without doing it in an objectoriented fashion).
I decided it'd be most useful to store data in terms of what a mathematical expression can be in its most basic form; I called this an "Operation":
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25  enum {
OPERATION_null,
OPERATION_subtract,
OPERATION_add,
OPERATION_divide,
OPERATION_multiply,
OPERATION_exponentiate,
MAX_OPERATION
};
// an "Operation" can just be a single value, in
// which case a and b are both 0, and a_val contains
// the proper value.
// an "Operation" can point to other operations,
// allowing for arbitrary operation complexity
// (like what we need for a math expression parser)
typedef struct Operation {
struct Operation *a;
struct Operation *b;
int a_val;
int b_val;
int operation_type;
} Operation;

My reasoning is that there's no
real reason why a fraction is any different of an operation than an addition or subtraction. It is still fundamentally a binary arithmetic operation, so it's easy to generalize on that.
My goal was to effectively generate a tree that would be useful for parsing an expression. Take, for instance, the expression "1 + 2 * 3".
It can be observed that the best way to go about forming this tree is to find the lowest possible priority operator in a given expression and split the expression based on that to simpler components:
By picking the
lowest priority operator, we can break up the expression in such a way that is compliant with operator precedence. The leaf on the above tree that holds "2*3" is actually just a representation for a subtree, which can then be calculated recursively in a similar fashion:
Combining the two trees we've constructed thus far gives us a properly defined tree that will be helpful in calculating a result:
To calculate the result, we can just traverse the tree, starting at its root, and performing the correct operation on a given Operation's two branches according to its operator_type. If a particular branch is not an atomic value and is another subtree, we can operate on it recursively:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41  float solve_operation(Operation *root) {
float result = 0.f;
float a = root>a_val;
float b = root>b_val;
if(root>a) {
a = solve_operation(root>a);
}
if(root>b) {
b = solve_operation(root>b);
}
switch(root>operation_type) {
case OPERATION_null: {
result = a;
break;
}
case OPERATION_subtract: {
result = a  b;
break;
}
case OPERATION_add: {
result = a + b;
break;
}
case OPERATION_divide: {
result = b == 0.f ? 0.f : (a / b);
break;
}
case OPERATION_multiply: {
result = a * b;
break;
}
case OPERATION_exponentiate: {
result = pow(a, b);
break;
}
default: break;
}
return result;
}

Note that this doesn't actually
simplify the expression as you wanted, but rather solves for it. To simplify it algebraically, you'd just need to traverse it slightly differently and perform different operations, but the fundamental idea would be the same.
Now, this probably isn't the
most efficient way to go about doing this, but it gets the job done fine for this problem in particular. An iterative solution would probably lend itself better for efficiency, and perhaps the tree could be stored in a more cacheefficient way, but I was just trying to work through the problem conceptually.
I think what I would say regarding your solution is that it doesn't fundamentally matter what a human's understanding of what a number is and therefore it doesn't matter how a number is represented in terms of a class hierarchy. What matters is what is most useful to generalize
strictly in terms of solving the problem at hand (which, in this case, happens to be parsing a mathematical expression somehow).