Object orientation: how to choose from a number of implementation options

I am a decent procedural programmer, but I am new to object orientation (I was trained as an engineer in good old Pascal and C). What I find particularly difficult is the choice of one of several ways to achieve the same. This is especially true for C ++, because its power allows you to do almost everything you like, even terrible things (I think the proverb of authority / responsibility is appropriate here).

I thought this could help me launch a specific case that I am struggling with the community to understand how people make these choices. What I am looking for is both advice related to my particular case, and more general pointers (no pun intended). Here:

As an exercise, I’m developing a simple simulator where the “geometric representation” can be of two types: “circle” or “polygon”. The other parts of the simulator will then have to accept these views and possibly deal with them differently. I came up with at least four different ways to do this. What are the advantages / disadvantages / compromises of each of them?

A: Function overload

Declare Circle and Polygon as unrelated classes, and then overload each external method that requires a geometric representation.

B: Casting

Declare enum GeometricRepresentationType {Circle, Polygon} . Declare an abstract GeometricRepresentation class and inherit Circle and Polygon from it. GeometricRepresentation has a virtual GetType() method, which is implemented by Circle and Polygon . The methods then use GetType() and the switch statement to cast a GeometricRepresentation to the appropriate type.

C: Unsure of a suitable name

Declare an enumeration type and an abstract class, as in B. In this class, also create the Circle* ToCircle() {return NULL;} and Polygon* ToPolygon() {return NULL;} functions. Each derived class then overloads the corresponding function, returning this . Is this just a reinvention of the dynamic cast?

D: combine them together

Implement them as a single class having an enumeration element indicating which type is an object. The class has members that can hold both views. Then, external methods should not call stupid functions (for example, GetRadius() on a polygon or GetOrder() on a circle).

+7
c ++ polymorphism casting oop
source share
4 answers

Here are a few design rules (thumb) that I teach my OO students:

1) at any time, when you are tempted to create an enumeration to track any mode in an object / class, you could (possibly better) create a derived class for each enumeration value.

2) whenever you write an if statement about an object (or its current state / mode / independently), you could (perhaps better) make a call to a virtual function to perform some (more abstract) operation, where the original is then- or else-sub-statement is the body of the virtual function of the derived object.

For example, instead:

 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; } 

Do it:

 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; } 

In the above example, it is pretty obvious what a more abstract idea is, the perimeter. It is not always so. Sometimes he will not even have a good name, which is one of the reasons why it is difficult to “see”. But you can convert any if statement to a virtual function call, where the "if" part is replaced by the virtuality of the function.

In your case, I definitely agree with the answer from Avi, you need a base / interface class and derived subclasses for Circle and Polygon.

+1
source share

Most likely, you will have common methods between Polygon and Circle . I would combine them as under an interface with the name Shape , for example (writing in java, because it is more recent in my syntactic form). But this is what I will use if I wrote a C ++ example. C ++):

 public interface Shape { public double getArea(); public double getCentroid(); public double getPerimiter(); } 

And let both Polygon and Circle implement this interface:

 public class Circle implements Shape { // Implement the methods } public class Polygon implements Shape { // Implement the methods } 

What are you getting:

  • You can always consider Shape as a generalized object with certain properties. In the future, you will be able to add different implementations of Shape without changing the code that does something with Shape (unless you have something specific for the new Shape )

  • If you have exactly the same methods, you can replace the interface with an abstract class and implement them (in a C ++ interface, it's just an abstract class in which nothing is implemented)

The most important thing (I'm emphesizing bullet # 1) is that you will like the power of polymorphism. If you use enumerations to declare your types, you will have to change many places in the code if you want to add a new form. While you do not need to change anything for the new class, it implements the form.

+1
source share

Go through the C ++ tutorial for the basics and read something like Stroustrup's "C ++ Programming Language" to learn how to use the language idiomatically.

Do not think that people tell you that you will have to study OOP regardless of language. The dirty secret is that what each language understands, since OOP in some cases is in no way vaguely similar, therefore it has a solid foundation, for example, Java, in fact, C ++ does not really help; it is so far away that go simply does not have classes at all. In addition, C ++ is clearly a multi-paradigm language, including procedural, object-oriented, and general programming in a single package. You need to learn how to combine this effectively. It was designed for maximum performance, which means that some of the lower-bit elements are demonstrated, leaving many performance-related decisions in the hands of a programmer, where other languages ​​simply don't provide options. C ++ has a very extensive library of common algorithms, training on the use of which is part of the curriculum.

Start small, so after a couple of years you can giggle at the naivety of your first attempts, instead of pulling out your hair.

Don't worry about "efficiency"; use virtual member functions everywhere if there is no good reason. Get a good grip on recommendations and const . Obtaining the right to design an object is very difficult; do not expect the first (or fifth) attempt to be the last.

+1
source share

First, a little about OOP and how C ++ and other languages, such as Java, differ.


People tend to use object-oriented programming for several different purposes:

  • General programming: creating common code; that is, it works on any object or data that provides the specified interface without worrying about implementation details.

  • Modularity and encapsulation: prevention of too strong connection of different parts of the code with each other (the so-called "modularity"), hiding irrelevant implementation details from its users.
    This is another way to think about sharing issues.

  • Static polymorphism: the default setting to implement any behavior for a particular class of objects while maintaining modular code when a set of possible settings is already known when writing your program.
    (Note: if you do not need to keep the code modular, the choice of behavior will be as simple as if or switch , but then the source code will have to take into account all the possibilities.)

  • Dynamic polymorphism: like static polymorphism, with the exception of the many possible settings, it is not yet known - perhaps because you expect the library user to be able to implement specific behavior later, for example. to create a plugin for your program.

In Java, the same tools (inheritance and redefinition) are used to solve almost all of these problems.
The good thing is that there is only one way to solve all the problems, so it is easier to learn.
The disadvantage is sometimes - but not always negligible: a solution that solves problem No. 4 is more expensive than a solution that needs to be resolved only for # 3.

Now enter C ++.

C ++ has different tools to solve all these problems, and even when they use the same tool (e.g. inheritance) for the same problem, they are used in different ways, that they are actually completely different solutions than The classic "inherit + override" you see in Java:

  • For this, common programs are created: C ++ template . They are similar to Java generators, but in fact Java generics often require inheritance to be useful, while C ++ templates have nothing to do with inheritance at all.

  • Modularity and encapsulation: C ++ classes have public and private access modifiers, as in Java. In this regard, the two languages ​​are very similar.

  • Static polymorphism: Java has no way to solve this particular problem, and instead you are forced to use the solution for # 4, paying a fine that you don't have to pay. On the other hand, C ++ uses a combination of template class es and CRTP inheritance to solve this problem. This type of inheritance is very different from the type for # 4.

  • Dynamic polymorphism: C ++ and Java allow overriding inheritance and function and are similar in this regard.


Now back to your question. How can I solve this problem?
From the discussion above, it follows that inheritance is not the only hammer designed for all nails.

Probably the best way (although perhaps the most difficult way) is to use # 3 for this task.

If necessary, you can implement # 4 on top of it for classes that need it without affecting other classes.

You declare a class called Shape and define the basic functionality:

 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; } }; 

Then you define the overall functionality:

 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; } }; 

Once you do this, you can define new shapes:

 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! }; 

This is probably the best method, since you most likely already know all the possible shapes at compile time (i.e. you do not expect the user to write a plugin to define his own form), and therefore is not needed any deal with virtual . However, it keeps the code modular and separates the problems of different forms, effectively providing you with the same advantages as the dynamic polymorphism approach.
(This is also the most efficient option at runtime, at the cost of a bit more complicated at compile time.)

Hope this helps.

0
source share

All Articles