4

经过长时间的 C 风格程序编码,我才刚刚开始“获得”OOP。所以我怀疑可能有标准的方式来处理我所面临的情况。我有一个应用程序,其类层次结构如下所示:

#include <iostream>
using namespace std;

class A {
public:
  virtual int intf() { return 0;} // Only needed by B
  virtual double df() {return 0.0;} // Only needed by C
};    
class B : public A {
  int intf() {return 2;}
  // B objects have no use for df()
};    
class C : public B {
  double df() {return 3.14;}
  // C objects have no use for intf()
};    
int main(){
  // Main needs to instantiate both B and C.
  B b;
  C c;
  A* pa2b = &b;
  A* pa2c = &c;

  cout << pa2b->intf() << endl;
  cout << pa2b->df() << endl;
  cout << pa2c->intf() << endl;
  cout << pa2c->df() << endl;

  return 0;
}

现在这个程序编译并运行良好。但是,我对它的设计有疑问。A类是通用接口,不需要实例化。B类和C类需要。关于函数:B需要intf()但C不需要,C需要df()但B不需要B {C} 的 df() {intf()}。

编辑B 和 C 共享一些数据成员以及 f() 以外的一些成员函数。我没有向它展示我的精简代码。

最后,按照标准,我的应用程序需要通过指向 A 的指针访问 B 和 C。所以我的问题是:有没有办法“清理”这个设计,以便不需要/空的成员函数定义(比如我有A)的声明/定义中所做的可以消除吗?类之间有明确的“IS-A”关系。因此,尽管我分享了每个新手对继承的兴奋,但我不觉得我已经扩展了我的设计,只是为了可以使用继承。

背景以防万一:我正在实施一个回归套件。A 类实现了每个回归通用的函数和矩阵(例如因变量和自变量)。B 类是具有两个类(“0”和“1”)的逻辑回归,并定义了成本函数,以及用于两类逻辑回归的训练算法。C类是多类逻辑回归。它通过使用“one-vs-all”算法对多个类进行训练来扩展 B 类。因此,从某种意义上说,如果您将您感兴趣的类别视为正面示例,而将所有其他类别视为负面示例,则 C 是二元逻辑回归。然后为每个类执行多类回归。有问题的函数(intf 和 df)返回输出。在逻辑回归的情况下,返回值是一个向量,而对于多类回归,它是一个矩阵。而且,如上所述,B 和 C 对彼此的返回函数没有任何用处。除了我似乎无法消除 A(回归类)中的冗余定义。

谢谢你的帮助。

4

2 回答 2

5

查看 Liskov 替换原则 (http://en.wikipedia.org/wiki/Liskov_substitution_principle)。它指出子类必须履行与超类相同的契约。在您的示例中,两个子类都没有这样做。“Is-A”关系不足以证明继承是合理的。

一种选择是使用单个模板方法,如下所示:

template <typename T>
class A<T> {
    T getValue();
}

class B : A<int> {
    int getValue();
}

class C: A<double> {
    double getValue();
}

这将允许两个子类都履行合同,同时允许方法的返回类型根据子类定义而变化。

如果你想了解更多面向对象编程的“最佳实践”,google“Robert Martin SOLID”

于 2012-10-26T19:50:52.113 回答
3

你触及了 OOP 中最具争议的一点:is-a == 派生模式,导致了“上帝对象”反模式,因为一切最终都是上帝的孩子,上帝知道每个人的每一种方法,并且对所有内容都有一个“答案”(阅读“默认实现”)。

“Is-a”不足以证明继承的合理性,其中不存在替换能力,但在现实世界中,没有对象真的可以完全替换为另一个对象,否则不会有所不同。

您处于替代原则无法很好地工作的“无名之地”,但是 - 同时 - 虚拟函数看起来是实现动态调度的最佳工具。

你唯一能做的就是妥协,牺牲两者中的一个。

就情况而言,由于 B 和 C 没有任何共同点(没有共享有用的方法),所以不要让这些方法源自 A。如果你有一些东西要“共享”,可能是一种运行时机制来发现在输入 B 相关特定代码或 C 相关特定代码之前输入 B 或 C 的类型。

这通常是通过一个公共基础来完成的,该基础具有一个运行时类型指示器来切换,或者只是一个虚拟函数(通常是析构函数)来让 dynamic_cast 能够工作。

class A
{
public:
    virtual ~A() {}

    template<class T>
    T* is() { return dynamic_cast<T*>(this); }
};

class B: public A
{
public:
    int intf() { return 2; }
};

class C: public A
{
public:
    double df() { return 3.14; }
};

int main()
{
    using namespace std;

    B b;
    C c;

    A* ba = &b;
    A* ca = &c;

    B* pb = ba->is<B>();
    if(pb) cout << pb->intf() << endl;

    C* pc = ca->is<C>();
    if(pc) cout << pc->df() << endl;
}
于 2012-10-26T20:12:18.030 回答