2

我有一个带有纯虚方法的类 Feature。

class Feature {
public:
  virtual ~Feature() {}
  virtual const float getValue(const vector<int>& v) const = 0;
};

该类由多个类实现,例如 FeatureA 和 FeatureB。一个单独的类 Computer(简化)使用 getValue 方法进行一些计算。

class Computer {
public:
  const float compute(const vector<Feature*>& features, const vector<int>& v) {
    float res = 0;
    for (int i = 0; i < features.size(); ++i) {
      res += features[i]->getValue(v);
    }
    return res;
  }
};

现在,我想实现 FeatureC,但我意识到我需要 getValue 方法中的其他信息。FeatureC 中的方法看起来像

const float getValue(const vector<int>& v, const vector<int>& additionalInfo) const;

我当然可以修改Feature、FeatureA、FeatureB中getValue的签名,将additionalInfo作为参数,也可以在compute方法中添加additionalInfo作为参数。但是,如果我想实现需要更多附加信息的 FeatureD,我以后可能不得不再次修改所有这些签名。我想知道是否有更优雅的解决方案,或者是否有已知的设计模式,您可以指出我进一步阅读。

4

4 回答 4

2

你至少有两个选择:

  1. 不是将单个向量传递给getValue(),而是传递一个结构。在这个结构中,您可以在今天放置向量,并在明天放置更多数据。当然,如果您的程序的某些具体运行不需要额外的字段,那么计算它们的需要可能是浪费的。但是,如果您总是需要计算所有数据(即,如果总是有一个 FeatureC),它不会造成性能损失。
  2. 传递对getValue()具有获取必要数据的方法的对象的引用。该对象可以是计算机本身,也可以是一些更简单的代理。然后getValue()实现可以准确地请求他们需要的东西,并且可以延迟计算。在某些情况下,惰性会消除浪费的计算,但这样做的整体结构将由于必须调用(可能是虚拟的)函数来获取各种数据而产生一些小的恒定开销。
于 2013-07-04T10:55:38.720 回答
1

要求要素类层次结构的用户根据类调用不同的方法会破坏多态性。一旦你开始这样做dynamic_cast<>(),你就会知道你应该重新考虑你的设计。

如果子类需要只能从其调用者处获取的信息,则应更改 getValue() 方法以获取附加信息参数,并在无关紧要的类中忽略该信息。

如果 FeatureC 可以通过调用另一个类或函数来获取附加信息,那通常是更好的方法,因为它限制了需要了解它的类的数量。也许数据可以从 FeatureC 通过其构造函数访问的对象获得,或者从单例对象获得,或者可以通过调用函数来计算。找到最好的方法需要对案例有更多的了解。

于 2013-07-04T10:52:08.823 回答
1

这个问题在 C++ 编码标准 (Sutter, Alexandrescu) 的第 39 项中得到解决,标题为“考虑将虚拟函数设为非公共的,将公共函数设为非虚拟的”。

特别是,遵循非虚拟接口设计模式(这就是项目的全部内容)的动机之一被表述为

每个界面都可以采用其自然形式:当我们将公共界面与定制界面分开时,每个界面都可以轻松地采用其自然想要采用的形式,而不是试图找到一种折衷方案,迫使它们看起来相同。通常,两个接口需要不同数量的功能和/或不同的参数;[...]

这个特别有用

在更改成本高的基类中

在这种情况下,另一个非常有用的设计模式是访问者模式。至于 NVI,它适用于基类(以及整个层次结构)具有高更改成本的情况。你可以找到很多关于这种设计模式的讨论,我建议你阅读 Modern C++ (Alexandrescu) 中的相关章节,它(侧面)让你对如何使用(非常容易使用)访问者设施有很好的了解在洛基

我建议您阅读所有这些材料,然后编辑问题,以便我们为您提供更好的答案。我们可以提出各种可能不适合您的情况的解决方案(例如,如果需要,使用为类提供附加参数的附加方法)。

尝试解决以下问题:

  1. 基于模板的解决方案是否适合该问题?
  2. 在调用函数时添加一个新的间接层是否可行?
  3. “推送参数”-“推送参数”-...-“推送参数”-“调用函数”方法会有帮助吗?(起初这可能看起来很奇怪,但想想类似“cout << arg << arg << arg << endl”,其中“endl”是“调用函数”)
  4. 您打算如何区分如何调用 Computer::compute 中的函数?

现在我们有了一些“理论”,让我们瞄准使用访客模式的实践:

#include <iostream>

using namespace std;

class FeatureA;
class FeatureB;

class Computer{
    public:
    int visitA(FeatureA& f);

    int visitB(FeatureB& f);
};

class Feature {
public:
  virtual ~Feature() {}
  virtual int accept(Computer&) = 0;
};

class FeatureA{
    public:
    int accept(Computer& c){
        return c.visitA(*this);
    }
    int compute(int a){
        return a+1;
    }
};

class FeatureB{
    public:
    int accept(Computer& c){
        return c.visitB(*this);
    }
    int compute(int a, int b){
        return a+b;
    }
};

int Computer::visitA(FeatureA& f){
        return f.compute(1);
}

int Computer::visitB(FeatureB& f){
        return f.compute(1, 2);
}

int main()
{
    FeatureA a;
    FeatureB b;
    Computer c;
    cout << a.accept(c) << '\t' << b.accept(c) << endl;
}

您可以在此处尝试此代码。这是访问者模式的粗略实现,如您所见,它解决了您的问题。我强烈建议您不要尝试以这种方式实现它,存在明显的依赖问题,可以通过称为非循环访问者的改进来解决。它已经在 Loki 中实现,因此无需担心实现它。

除了实现之外,您可以看到您不依赖类型开关(正如其他人指出的那样,您应该尽可能避免)并且您不要求类具有任何特定接口(例如,计算函数的一个参数)。此外,如果访问者类是一个层次结构(在示例中将 Computer 设为基类),那么当您想要添加此类功能时,您不需要向该层次结构添加任何新功能。

如果您不喜欢 visitA、visitB、...“模式”,请不要担心:这只是一个简单的实现,您不需要它。基本上,在实际实现中,您使用访问函数的模板特化。

希望这会有所帮助,我已经付出了很多努力:)

于 2013-07-04T11:29:35.490 回答
0

为了正常工作,虚函数需要具有完全相同的“签名”(相同的参数和相同的返回类型)。否则,你只会得到一个“新成员函数”,这不是你想要的。

这里真正的问题是“调用代码如何知道它需要额外的信息”。

您可以通过几种不同的方式解决这个问题 - 第一个是始终传入const vector <int>& additionalInfo,无论是否需要。

如果这是不可能的,因为additionalInfo除了 之外没有任何FeatureC参数,你可以有一个“可选”参数——这意味着使用指向向量 ( vector<int>* additionalInfo) 的指针,当值不可用时它为 NULL。

当然,如果additionalInfo是一个可以存储在 FeatureC 类中的值,那么它也可以工作。

另一种选择是扩展基类Feature以具有另外两个选项:

class Feature {
public:
  virtual ~Feature() {}
  virtual const float getValue(const vector<int>& v) const = 0;
  virtual const float getValue(const vector<int>& v, const vector<int>& additionalInfo) { return -1.0; };   
  virtual bool useAdditionalInfo() { return false; }
};

然后让你的循环是这样的:

for (int i = 0; i < features.size(); ++i) {
  if (features[i]->useAdditionalInfo())
  {
       res += features[i]->getValue(v, additionalInfo);
  }
  else
  {
     res += features[i]->getValue(v);
  }
}
于 2013-07-04T10:54:57.890 回答