My main complaint with OOP

Started by
18 comments, last by Green_Baron 4 years, 6 months ago

I think that object oriented programming has one major flaw. Consider the type below.


struct Circle {
  Vector2 center;
  float radius;
};

Now imagine we want to add functionality so the circle can

  • Calculate area/circumference of the shape
  • Draw it to a canvas
  • Add the shape to a spacial index
  • Serialize the shape to a file

A naive way to implement this with OOP is


class Circle : SpacialIndexLeaf {
  public Vector2 center;
  public float radius;
  
  public float CalculateArea();
  
  public void DrawToCanvas(Canvas canvas);
  
  // spacial index interface
  public BoundingBox GetBoundingBox();
  
  public void WriteToFile(File file);
}

This creates a hard dependency from Circle on Canvas, File, and a soft dependency in the spacial index by implementing an interface.

This makes it much harder organize dependencies in a meaningful manner. The shape code, which should be just a simple representation of geometry. Now depends on spacial index code, rendering code, and io code. How did we get into the mess? The circular dependency between the data of a class and the functions of a class inherent in object oriented programming.

Lets try to fix this problem using OOP approved methods. First lets try wrapper classes


class Circle {
  public Vector2 center;
  public float radius;

  // no dependencies, this one stays
  public float CalculateArea();
};

class CircleRenderer : Renderable {
  public Circle circle;

  public CircleRenderer(Circle circle);

  public DrawToCanvas(Canvas canvas);
};

public CircleSerializer : Serializable {
  public Circle circle;

  public CircleSerializer(Circle circle);

  public void WriteToFile(File file);
};

This definitely fixes the dependency problem. It has the unfortunate side effect of greatly multiplying the number of classes in the code. Adding new shapes would require having many parallel classes scattered throughout the code base. How about combining all the shape rendering code into a single class.


class Circle {
  public Vector2 origin;
  public float radius;

  public float GetArea();
};

class ShapeRenderer {
  public void RenderCircle(Canvas canvas, Circle circle);
  public void RenderRectangle(Canvas canvas, Rectangle rectangle);
  ...
};

class ShapeSerializer {
  public void SerializeCircle(File file, Circle circle);
  public void SerializeRectable(File file, Rectangle rectangle);
  ...
};

Less classes to manage, that is a plus. This has the downside of having a ShapeRenderer and ShapeSerializer that could grow to depend on a lot of different classes. If you only need to render a circle you still need to use a class that references all the other classes you don't care about. You also need to construct a class just to call a function. Moving where code is implemented changes what class may need to be constructed.

Lets solve this problem without OOP.


struct Circle {
  Vector2 origin;
  float radius;
};

float CalculateArea(Circle circle);

void Render(Canvas canvas, Circle circle);

void Serialize(File file, Circle circle);

No more circular dependencies between functions and data, or classes. Data depends on other data. You could define all the data structures in a completely different module from all the functions. Functions depend on data and other functions. The implementation of functions can be moved freely around in files without having to worry about which class the function should belong to. Moving where functions only changes where the functions are imported from.

Classes to solve polymorphism nicely, but I think there are other solutions that don't have the inherent circular depedency problem that OOP has. I personally think that golang has a good approach here. Any thoughts on this?

My current game project Platform RPG
Advertisement
13 minutes ago, HappyCoder said:

This creates a hard dependency from Circle on Canvas,

The problem here is that a circle should never know anything about drawing at all. It's fairly common for a circle to exist without any context of visual representation, so these methods just don't belong there.

If you have a separate class that knows both about circles and drawing contexts, this problem doesn't come up.

1 hour ago, Prototype said:

The problem here is that a circle should never know anything about drawing at all. It's fairly common for a circle to exist without any context of visual representation, so these methods just don't belong there.

If you have a separate class that knows both about circles and drawing contexts, this problem doesn't come up.

In an oop world, these refactors can be a big pain. Moving functions onto other classes is non trivial. Moving functions to different files is. 

My current game project Platform RPG
6 minutes ago, HappyCoder said:

In an oop world, these refactors can be a big pain. Moving functions onto other classes is non trivial. Moving functions to different files is. 

Sometimes you'll have to reconsider your design, that's just the way it is. I think it's one of the strenghts of OOP to allow you to do that relatively easy, if you follow the single responsibility principle. Dependencies between unrelated classes should always be a red flag.

OOP is a great tool, but it surely can lure you into building unnecessary complex structures that eventually turn against you. I think that's the 'blow whole foot off' part of C++.

3 hours ago, HappyCoder said:

Any thoughts on this?

I sort of agree with Prototype that your example seems a bit contrived, in that even in the context of OOP, loading up a 'shape' class with a large amount of disparate functionality (especially drawing) would likely be contraindicated. So it may be less an issue with shortcomings of OOP and more just a matter of using it effectively.

Also, many languages either don't enforce object-orientation, or if they do, there are ways to work around it (e.g. static fields and functions). In a multi-paradigm language like C++, for example, OOP is just a tool that's available - you're not forced to use it. You can absolutely do things like in the example later in your post (with free functions or whatever).

Furthermore, I see a lot of discussion of techniques like ECS these days, some of which (there may be some debate about this) arguably aren't really 'OOP' in the traditional sense. So it would seem that even in the mainstream, there's not some big push to only use OOP (in general at least).

So I guess my thoughts are that you may be tilting at windmills a bit ? Many languages are multi-paradigm, no one's forcing you to use OOP (presumably ?), and there are mainstream architectural approaches that arguably aren't strictly OOP in the traditional sense. In summary, I think it's probably pretty much as it's always been, which is that one can and should use whatever tools and techniques are most appropriate for the task at hand, whatever they may be.

My beef with OOP is the "OP" part. Objects are fine, but they are just one of many tools.

When I was programming in C in the 90s, I often found myself making a struct like this:


typename struct {
  int width;
  int height;
  Pixel *data;
} Image;

Then I would write a bunch of functions (FillWithColor, DrawPixel...) that would have a first argument of type `Image *'. Then there would be a function to allocate memory in the `data' field, and a function to deallocate it.

In essence, that and a bit of syntactic sugar (so instead of `DrawPixel(my_image, 40, 50, red)' you can call `my_image.drawPixel(40, 50, red)') is all an object is. It's tidy to think of the struct and these functions as part of the same thing, and the fact that C++ will call destructors for you automatically and at easy-to-predict times is wonderful.

But to approach program design by trying to shoehorn everything into this paradigm is a terrible idea. Use the tools that are appropriate for the problem at hand.

Lately I've become a fan of the signal-slot paradigm to try to keep dependencies down to a minimum.

 

 

7 hours ago, HappyCoder said:

A naive way to implement this with OOP is

That's (ab)using OOP tools, but breaking OOD rules, so it's "OOP"....

7 hours ago, HappyCoder said:

Lets solve this problem without OOP.

That approach is famously identified as C++ OOP best practice in 1992 (or in the Effective C++ 3rd edition from 2005: "Item 23: Prefer non-member non-friend functions to member functions.").

Free functions can improve encapsulation and are perfectly valid in OOP designs.

I definitely agree that OOP can be easily abused and I posted examples that were contrived to make OOP look bad. I guess my major problem is the way OOP is taught. I feel like most new programmings come into programming trying to solve everything with OOP and do it poorly.

My current game project Platform RPG
14 minutes ago, HappyCoder said:

I guess my major problem is the way OOP is taught. I feel like most new programmings come into programming trying to solve everything with OOP and do it poorly.

This is a consequence of the fact that few people truly understand what OOP is, including those who teach it. I can only refer to @Hodgman's article OOP is dead, long live OOP. That text is the best explanation of proper OOP I've ever read.

 

5 hours ago, HappyCoder said:

I guess my major problem is the way OOP is taught

Yeah - its basically "here are these tools that we call OOP" but not "here's why you should and shouldn't use them in different situations". OOP makes for a horrible mess if you just use all its tools all the time with no guidance :)

This topic is closed to new replies.

Advertisement