我知道这类问题已经回答了几次,但我提出问题的背景,期待其他一些架构选择。
考虑一个CExpression类:
class CExpression
{
public:
...
private:
vector<CComponent*> components_;
string expression_;
}
CExpression必须将字符串分解为向量中的数学表达式(例如&#34; y = x + 5&#34;)(&#34; y&#34;,&#39; =& #39;,&#34; x&#34;,&#39; +&#39;,5)。为此,向量由CComponent指针组成,它可以指向类CVariable,COperator和CConstant的对象。 显然,CComponent是一个抽象类,也是上面提到的三个类的基类。因此,在解析字符串之后,向量应按顺序包含以下内容(进程的半伪代码):
components_.push_back(new CVariable("y"));
components_.push_back(new COperator('='));
components_.push_back(new CVariable("x"));
components_.push_back(new COperator('+'));
components_.push_back(new CConstant( 5 ));
这里使用多态性是将表达式分解为单个向量(这将有助于将来的解析过程)。但是,某些派生类具有其他人不具备的独特功能,因此无法在基类(CComponent)中实现这些功能。
例如,考虑COperator类:
class COperator : public CComponent
{
public:
int GetPriority() const { return prority_; }
...
private:
int priority_;
...
}
优先级,表示操作符必须从向量中解析的优先级,对于此类是唯一的(因此基类中没有虚函数)。现在让我们来解决问题。
考虑CComponent类(基类):
enum Type { VARIABLE, OPERATOR, CONSTANT };
class CComponent
{
public:
Type GetType() const { return type_; }
...
private:
Type type_;
...
}
类型,对于表达式的任何组件都是通用的,表示组件的类型(例如,如果它是CVariable,则在构造时类型将设置为VARIABLE)。
最后,考虑一下这种CExpression方法(虚构):
void CExpression::Process()
{
for (int i = 0; i < components_.size(); i++)
{
if (components_[i] -> GetType() == OPERATOR)
{
cout << components_[i] -> GetPriority(); // won't work
}
}
}
事实上,由于我只能使用指针类型类的方法(除非我认为动态广播不是最美妙的方式),我有两个问题:
顺便说一下,我知道解释起来可能更简单,但我认为上下文是抓住问题的好帮手。
谢谢!
答案 0 :(得分:3)
我想,您的架构无法满足您的需求 - 尤其是当您开始扩展它时。
我在处理数学表达式方面有一些经验,我说,存储表达式最自然的方式是树。每个终端项(例如数字或变量)是树的叶子,并且每个非终端项(例如运算符或函数调用)是具有子节点的节点。例如:
y = x + 5
应翻译成树:
=
/ \
y +
/ \
x 5
这种结构有什么好处?首先,它比令牌矢量更容易评估。其次,运营商优先级或关联方向等问题仅在构建此结构时起作用 - 在构建结构并准备评估时不使用它们。然后,每个节点都不关心作为子节点附加到它上面的内容,它只是让它们自己进行评估,当它完成时,它最终会得到一个可以工作的终端项列表。甚至赋值运算符也可以执行它的工作(当然,如果你传递某种包含变量列表的上下文)。
如果使用众所周知的反向波兰表示法算法,那么创建这样的结构非常容易。
在你的情况下,我会对你的数据结构的完全重新安排进行投票,这对于存储表达式来说要好得多。
还有一件事。另外根据我的经验,我强烈建议您为这三件事创建不同的类:
这可能会使您的架构变得复杂,但实际上会简化您的工作并使您的架构更加灵活。
结构的草稿:
class BaseNode
{
public:
virtual EvalObject Eval() = 0;
// This method is handy when working with assignment operator.
// For instance, Eval() called on variable will return its value
// but EvalLHS() will return a reference to variable.
virtual EvalObject EvalLHS() = 0;
};
class Operator : BaseNode
{
};
class BinaryOperator : Operator
{
private:
BaseNode * leftChild;
BaseNode * rightChild;
};
class Add : BinaryOperator
{
public:
void Eval()
{
auto left = leftChild->Eval(); // Eval RHS,
auto right = rightChild->Eval(); // Eval RHS
// Now perform calculations on left and right
// depending on their types
}
void EvalLHS()
{
throw InvalidOperationException("Cannot perform LHS evaluation on adding operator");
}
}
class Assign : BinaryOperator
{
public:
void Eval()
{
auto left = leftChild->EvalLHS();
auto right = rightChild->Eval();
// Perform assignment
// This is required such that operations
// like a = b = 7 will also work
return right;
}
void EvalLHS()
{
// Assignment cannot be on the LHS of operation, eg.
// (a = 5) = 8 is wrong
throw InvalidOperationException("Assignment cannot be LHS");
}
}
答案 1 :(得分:0)
我觉得设计很差,在这里我会解释原因:
在您输入enum Type
的那一刻,您实际上承认,虽然您希望在任何派生类上拥有一个纯粹的界面同样 - 但您无法做到。在基类中实现的算法确实需要 来知道派生的确切类型才能发挥作用。在这方面,enum Type
和dynamic_cast
提供相同的习惯用法:依赖于确切类型的实现。
这不是面向对象的方式
面向对象的方式声称你的算法,你的代码,与一些基类一起工作的函数作为输入,不假设接口背后的真实对象 - 只有它的界面很重要。
至于你的具体问题,正如上面提到的,我也认为树状结构最适合这个问题。有几种方法可以做到这一点。在我看来,使用它有两个阶段:1。建立结构; 2.对结构进行一些评估
我将尝试仅绘制(不会编译)我的想法,细节和微调留给你:
class Expression
{
};
class Constant : public Expression
{
public:
// 'int' can be easily changed to generic type 'T'
Constant( int value ) : _value( value ) {}
private:
int _value;
};
class Operator : public Expression
{
public:
Operator( Expression left, Expression right )
: _left( left ), _right( right ) {}
protected:
Expression _left;
Expression _right;
};
class OperatorPlus : public Expression
{
public:
OperatorPlus( Expression left, Expression right )
: Operator(_left( left ), _right( right )) {}
};
// few more operators, the same
class OperatorMinus : public Expression { /* ... */ }
class OperatorMul : public Expression { /* ... */ }
class OperatorDiv : public Expression { /* ... */ }
class Variable : public Expression
{
public:
// string can be easily changed to generic type 'T'
Constant( string value ) : _value( value ) {}
private:
string _value;
}
void f()
{
// y = x + 5
OperatorEqual s1( Variable( "y" ), OperatrPlus( Variable( "x" ), Constant( 5 ) ) );
}
至于实际用它做什么。我认为最好的方法是在Expression
类中添加所需的功能:
class Expression
{
public:
virtual Expression eval() = 0;
}
和派生可以轻松实现它:
class OperatorPlus : public Expression
{
public:
OperatorPlus( Expression left, Expression right )
: Operator(_left( left ), _right( right )) {}
virtual Expression eval()
{
return _left.eval() + _right.eval();
}
}
Expression
的其他接口可能是print
或shorten
或combineEqual
,也可能适合您的域名。