我是一个不错的程序程序员,但我是面向对象的新手(我曾经在好老Pascal和C上接受过工程师培训)。我觉得特别棘手的是选择其中一种方法来实现同样的目的。对于C ++来说尤其如此,因为它的功能可以让你做任何你喜欢的事情,甚至可怕的事情(我想这里的权力/责任格言是合适的)。
我认为这可能有助于我运行一个我正在与社区斗争的特定案例,以了解人们如何做出这些选择。我正在寻找的是与我的具体案例相关的建议,以及更一般的指针(没有双关语意)。这是:
作为练习,我正在开发一个简单的模拟器,其中“几何表示”可以有两种类型:“圆”或“多边形”。然后,模拟器的其他部分需要接受这些表示,并可能以不同方式处理它们。我已经提出了至少四种不同的方法来做到这一点。每个的优点/缺点/权衡取舍是什么?
A:功能重载
将Circle
和Polygon
声明为不相关的类,然后重载需要几何表示的每个外部方法。
B:施法
宣布enum GeometricRepresentationType {Circle, Polygon}
。声明一个抽象的GeometricRepresentation
类,并从中继承Circle
和Polygon
。 GeometricRepresentation
具有由GetType()
和Circle
实施的虚拟Polygon
方法。然后,方法使用GetType()
和switch语句将GeometricRepresentation
强制转换为适当的类型。
C:不确定名称
声明枚举类型和抽象类,如 B 。在此课程中,还会创建函数Circle* ToCircle() {return NULL;}
和Polygon* ToPolygon() {return NULL;}
。然后,每个派生类重载相应的函数,返回this
。这仅仅是动态铸造的重新发明吗?
D:将他们聚集在一起
将它们实现为具有枚举成员的单个类,该成员指示对象的类型。该类具有可以存储两种表示的成员。然后由外部方法决定不调用愚蠢的函数(例如,多边形上的GetRadius()
或圆上的GetOrder()
。
答案 0 :(得分:1)
很可能您会在Polygon
和Circle
之间使用常用方法。我将它们组合在一个名为Shape
的接口下,例如(在java中编写,因为它在语法方面更新鲜。但是如果我编写c ++示例,那就是我会使用的。这只是一段时间以来我写了c ++):
public interface Shape {
public double getArea();
public double getCentroid();
public double getPerimiter();
}
让Polygon
和Circle
都实现此接口:
public class Circle implements Shape {
// Implement the methods
}
public class Polygon implements Shape {
// Implement the methods
}
你得到了什么:
您始终可以将Shape
视为具有特定属性的generelized对象。您将来可以添加不同的Shape
实现,而无需更改使用Shape
执行某些操作的代码(除非您具有针对新Shape
的特定内容)
如果你有完全相同的方法,你可以用抽象类替换接口并实现它们(在C ++接口中只是一个没有实现的抽象类)
最重要的是(我正在强化子弹#1) - 你将享受多态的力量。如果你使用枚举声明你的类型,如果你想添加新的形状,你有一天必须改变代码中的很多地方。然而,您不必为实现形状的新类别更改任何内容。
答案 1 :(得分:1)
阅读基础知识的C ++教程,阅读Stroustrup的“The C ++编程语言”,了解如何习惯使用该语言。
不要相信人们告诉你,你必须独立于语言学习OOP。肮脏的秘密是,每种语言都理解为OOP在某些情况下甚至不是模糊地相似,因此具有坚实的基础,例如Java,对C ++来说并不是一个很大的帮助;到目前为止,语言go根本就没有类。此外,C ++显然是一种多范式语言,包括程序,面向对象和通用编程。您需要学习如何有效地组合它。它被设计用于最大性能,这意味着一些较低位的东西显示出来,在程序员手中留下许多与性能相关的决定,其他语言只是不提供选项。 C ++有一个非常广泛的泛型算法库,学习使用它们是课程的必要部分。
从小处开始,所以在几年的时间里,你可以在第一次尝试的天真地笑一笑,而不是把头发拉出来。
不要担心“效率”,在任何地方使用虚拟成员函数,除非有令人信服的理由不这样做。掌握参考资料和const
。正确设计对象是非常难,不要指望第一次(或第五次)尝试成为最后一次。
答案 2 :(得分:1)
以下是我教我的OO学生的一些设计规则(
)1)每当你想要创建一个枚举以跟踪对象/类中的某些模式时,你可以(可能更好)为每个枚举值创建一个派生类。
2)任何时候你写一个关于一个对象的if语句(或者它当前的状态/模式/无论什么),你可以(可能更好)进行虚函数调用来执行一些(更抽象的)操作,其中原始then-或else-sub-statement是派生对象的虚函数的主体。
例如,而不是这样做:
if (obj->type() == CIRCLE) {
// do something circle-ish
double circum = M_PI * 2 * obj->getRadius();
cout << circum;
}
else if (obj->type() == POLY) {
// do something polygon-ish
double perim = 0;
for (int i=0; i<obj->segments(); i++)
perm += obj->getSegLength(i);
cout << perim;
}
这样做:
cout << obj->getPerimeter();
...
double Circle::getPerimeter() {
return M_PI * 2 * obj->getRadius();
}
double Poly::getPerimeter() {
double perim = 0;
for (int i=0; i<segments(); i++)
perm += getSegLength(i);
return perim;
}
在上面的案例中,非常明显的是“更抽象”的想法是什么,外围。情况并非总是如此。有时它甚至没有一个好名字,这是很难“看到”的原因之一。但是,您可以将任何if语句转换为虚函数调用,其中“if”部分被函数的虚拟替换。
在你的情况下,我绝对同意Avi的答案,你需要一个基类/接口类以及Circle和Polygon的派生子类。
答案 3 :(得分:0)
首先,关于OOP的一些背景知识以及C ++和Java等其他语言的区别。
人们倾向于使用面向对象的编程来实现几个目的:
通用编程:编写通用代码;即适用于提供指定接口的任何对象或数据,而无需关心实现细节。
模块化和封装:通过向用户隐藏不相关的实现细节,防止不同的代码片段彼此过于紧密耦合(称为“模块化”)。 这是考虑关注点分离的另一种方式。
静态多态:为特定类对象定制某些行为的“默认”实现,同时保持代码模块化,其中可能的自定义集已经已知< / em>当你编写程序时。
(注意:如果您不需要保持代码模块化,那么选择行为就像if
或switch
一样简单,但原始代码需要考虑所有可能性。)
动态多态:类似于静态多态,但可能的自定义集是不已知 - 可能是因为您希望库的用户实现特定行为以后,例如为你的程序制作一个插件。
在Java中,使用相同的工具(继承和覆盖)来解决这些问题的基本所有。
好处是,只有一种方法可以解决所有问题,因此更容易学习
缺点是有时但并不总是微不足道的效率损失:解决问题#4的解决方案比仅需要解决#3的解决方案更昂贵。
现在,输入C ++。
C ++有不同的工具来处理所有这些,即使他们对同一个问题使用相同的工具(例如继承),它们也会以不同的方式使用,它们实际上是完全不同的解决方案< / em>比你在Java中看到的经典“继承+覆盖”:
通用编程:为此制作了C ++ template
。它们类似于Java的泛型,但实际上Java的泛型通常需要继承才有用,而C ++模板通常与继承无关。
模块化和封装:C ++类具有public
和private
访问修饰符,就像在Java中一样。在这方面,这两种语言非常相似。
静态多态:Java无法解决此特定的问题,而是强迫您使用#4的解决方案,支付给您的罚款不一定需要付钱。另一方面,C ++使用template
class
es和称为CRTP的继承的组合来解决此问题。 这种类型的继承与#4的继承非常不同。
动态多态:C ++和Java都允许继承和函数覆盖,并且在这方面类似。
现在,回到你的问题。我该如何解决这个问题呢? 从上面的讨论可以得出,遗传不是针对所有指甲的单锤。
可能最好的方法(尽管可能是最复杂的方法)是使用#3来完成这项任务。
如果需要,你可以在它上面为需要它的类实现#4,而不会影响其他类。
您声明了一个名为Shape
的类并定义了基本功能:
class Graphics; // Assume already declared
template<class Derived = void>
class Shape; // Declare the shape class
template<>
class Shape<> // Specialize Shape<void> as base functionality
{
Color _color;
public:
// Data and functionality for all shapes goes here
// if it does NOT depend on the particular shape
Color color() const { return this->_color; }
void color(Color value) { this->_color = value; }
};
然后定义通用功能:
template<class Derived>
class Shape : public Shape<> // Inherit base functionality
{
public:
// You're not required to actually declare these,
// but do it for the sake of documentation.
// The subclasses are expected to define these.
size_t vertices() const;
Point vertex(size_t vertex_index) const;
void draw_center(Graphics &g) const { g.draw_pixel(shape.center()); }
void draw_outline()
{
Derived &me = static_cast<Derived &>(*this); // My subclass type
Point p1 = me.vertex(0);
for (size_t i = 1; i < me.vertices(); ++i)
{
Point p2 = me.vertex(1);
g.draw_line(p1, p2);
p1 = p2;
}
}
Point center() const // Uses the methods above from the subclass
{
Derived &me = static_cast<Derived &>(*this); // My subclass type
Point center = Point();
for (size_t i = 0; i < me.vertices(); ++i)
{ center += (center * i + me.vertex(i)) / (i + 1); }
return center;
}
};
一旦这样做,您就可以定义新的形状:
template<>
class Square : public Shape<Square>
{
Point _top_left, _bottom_right;
public:
size_t vertices() const { return 4; }
Point vertex(size_t vertex_index) const
{
switch (vertex_index)
{
case 0: return this->_top_left;
case 1: return Point(this->_bottom_right.x, this->_top_left.y);
case 2: return this->_bottom_right;
case 3: return Point(this->_top_left.x, this->_bottom_right.y);
default: throw std::out_of_range("invalid vertex");
}
}
// No need to define center() -- it is already available!
};
这可能是最好的方法,因为您很可能在编译时已经知道所有可能的形状(即您不希望用户编写插件来定义自己的形状) ,因此不需要与virtual
完全交易。然而,它保持代码模块化并分离不同形状的关注点,有效地为您提供与动态多态方法相同的好处。
(它在运行时也是最有效的选项,代价是在编译时更复杂。)
希望这有帮助。