我有一节课:
class A
{
public:
virtual void func() {...}
virtual void func2() {...}
};
Run Code Online (Sandbox Code Playgroud)
还有一些来自这个的派生类,比方说B,C,D ...在95%的情况下,我想要遍历所有对象并调用func或func2(),因此我将它们放在向量中,如:
std::vector<std::shared_ptr<A> > myVec;
...
for (auto it = myVec.begin(); it != myVec.end(); ++it)
(*it).func();
Run Code Online (Sandbox Code Playgroud)
但是,在剩下的5%的情况下,我想根据它们的子类做一些不同的类.我的意思完全不同,比如调用带有其他参数的函数或者根本不为某些子类调用函数.我想到了一些解决这个问题的方法,我都不喜欢这个方案:
还将类放在其他向量中,每个向量用于其特定任务.这也不觉得OO,但也许我错了.喜欢:
std::vector<std::shared_ptr<B> > vecForDoingSpecificOperation;
std::vector<std::shared_ptr<C> > vecForDoingAnotherSpecificOperation;
Run Code Online (Sandbox Code Playgroud)那么,有人可以建议一种能达到我想要的风格/模式吗?
sbi*_*sbi 35
有人聪明(不幸的是我忘了谁)曾经在C++中谈到过OOP:对于switch类型(这是你所有的建议提出的)的唯一原因是对虚函数的恐惧.(这是一种解释.)将虚函数添加到基类中,派生类可以覆盖这些函数,并进行设置.
现在,我知道有些情况下这很难或很笨拙.为此,我们有访客模式.
在某些情况下,一个更好,而另一个则更好.通常,经验法则是这样的:
如果您有一组相当固定的操作,但继续添加类型,请使用虚函数.
操作很难在大型继承层次结构中添加/删除,但只需让它们覆盖适当的虚函数即可轻松添加新类型.
如果您有一组相当固定的类型,但继续添加操作,请使用访问者模式.
向大量访问者添加新类型是一个严重的问题,但是为一组固定类型添加新访问者很容易.
(如果两者都改变了,那你就注定了.)
根据你的评论,正如菲利普·瓦德勒所表达的那样,你偶然发现的是(可疑地)已知的表达问题:
表达式问题是旧问题的新名称.目标是按案例定义数据类型,其中可以在数据类型上添加新案例,在数据类型上添加新函数,而无需重新编译现有代码,同时保留静态类型安全性(例如,无强制转换).
也就是说,对程序员来说,"垂直"(向层次结构添加类型)和"水平"(向函数添加要覆盖的函数)都很难.
在Reddit上有一个很长的(一如既往)关于它的讨论,我在其中提出了一个C++解决方案.
它是OO(非常适合添加新类型)和泛型编程(非常适合添加新功能)之间的桥梁.我们的想法是拥有纯粹的接口和一组非多态类型的层次结构.根据需要在具体类型上定义自由函数,并且具有纯接口的桥由每个接口的单个模板类引入(由用于自动演绎的模板函数补充).
到目前为止,我发现了一个限制:如果一个函数返回一个Base接口,它可能是按原样生成的,即使实际的包装类型现在支持更多的操作.这是典型的模块化设计(呼叫站点无法使用新功能).我认为它说明了一个干净的设计,但是我知道有人可能想要将它"重铸"到更详细的界面.Go可以,语言支持(基本上,可用方法的运行时内省).我不想编写这在C++中.
正如我已经在reddit上解释的那样......我只是重现并调整我已在那里提交的代码.
那么,让我们从2种类型和单个操作开始.
struct Square { double side; };
double area(Square const s);
struct Circle { double radius; };
double area(Circle const c);
Run Code Online (Sandbox Code Playgroud)
现在,让我们创建一个Shape界面:
class Shape {
public:
virtual ~Shape();
virtual double area() const = 0;
protected:
Shape(Shape const&) {}
Shape& operator=(Shape const&) { return *this; }
};
typedef std::unique_ptr<Shape> ShapePtr;
template <typename T>
class ShapeT: public Shape {
public:
explicit ShapeT(T const t): _shape(t) {}
virtual double area() const { return area(_shape); }
private:
T _shape;
};
template <typename T>
ShapePtr newShape(T t) { return ShapePtr(new ShapeT<T>(t)); }
Run Code Online (Sandbox Code Playgroud)
好的,C++很冗长.我们马上检查一下用法:
double totalArea(std::vector<ShapePtr> const& shapes) {
double total = 0.0;
for (ShapePtr const& s: shapes) { total += s->area(); }
return total;
}
int main() {
std::vector<ShapePtr> shapes{ new_shape<Square>({5.0}), new_shape<Circle>({3.0}) };
std::cout << totalArea(shapes) << "\n";
}
Run Code Online (Sandbox Code Playgroud)
所以,首先练习,让我们添加一个形状(是的,这就是全部):
struct Rectangle { double length, height; };
double area(Rectangle const r);
Run Code Online (Sandbox Code Playgroud)
好的,到目前为止一切顺利,让我们添加一个新功能.我们有两种选择.
第一个是修改Shape它是否在我们的权力.这是源兼容的,但不兼容二进制.
// 1. We need to extend Shape:
virtual double perimeter() const = 0
// 2. And its adapter: ShapeT
virtual double perimeter() const { return perimeter(_shape); }
// 3. And provide the method for each Shape (obviously)
double perimeter(Square const s);
double perimeter(Circle const c);
double perimeter(Rectangle const r);
Run Code Online (Sandbox Code Playgroud)
看起来我们可能会陷入表达问题,但我们却没有.我们需要为每个(已知的)类添加周长,因为无法自动推断它; 但是它也不需要编辑每个类!
因此,外部接口和自由函数的结合让我们整齐地(好吧,它是C++ ......)回避了这个问题.
sodraz 在评论中注意到功能的添加触及了原始界面,可能需要冻结(由第三方提供,或者用于二进制兼容性问题).
因此,第二种选择并不是侵入性的,代价是稍微冗长:
class ExtendedShape: public Shape {
public:
virtual double perimeter() const = 0;
protected:
ExtendedShape(ExtendedShape const&) {}
ExtendedShape& operator=(ExtendedShape const&) { return *this; }
};
typedef std::unique_ptr<ExtendedShape> ExtendedShapePtr;
template <typename T>
class ExtendedShapeT: public ExtendedShape {
public:
virtual double area() const { return area(_data); }
virtual double perimeter() const { return perimeter(_data); }
private:
T _data;
};
template <typename T>
ExtendedShapePtr newExtendedShape(T t) { return ExtendedShapePtr(new ExtendedShapeT<T>(t)); }
Run Code Online (Sandbox Code Playgroud)
然后,perimeter为Shape我们想要使用的所有人定义函数ExtendedShape.
编译为反对的旧代码Shape仍然有效.无论如何它不需要新功能.
新代码可以使用新功能,并且仍然可以轻松地与旧代码接口.(*)
只有一个小问题,如果旧代码返回a ShapePtr,我们不知道该形状是否实际上具有周边函数(注意:如果指针是在内部生成的,则它尚未使用该newExtendedShape机制生成).这是开头提到的设计的限制.哎呀:)
(*)注意:无痛地暗示您知道拥有者是谁.一个std::unique_ptr<Derived>&和std::unique_ptr<Base>&不兼容,但是一个std::unique_ptr<Base>可以从建立std::unique_ptr<Derived>和Base*从一Derived*所以一定要确保你的职责是干净的所有权,聪明,你是金色的.