Object-oriented – Struct + Functions that operate on the struct vs. OOP objects

object-oriented

I recently started learning about OOP, and I have read that OOP is a different programming paradigm than procedural programming. But I didn't find any examples that shows in what way they are different.

I mean say that I am using OOP and I created a Car class with a moveForward() function and then I called this function:

Car car = new Car();    // Car is a class here
car.moveForward();

Now say that I am using procedural programming and I created a Car struct and a moveForward() function that takes a Car struct as an argument, and then I called this function:

Car car = new Car();    // Car is a struct here
moveForward(car);

So it seems to me that OOP and procedural programming are only different in the way you call a function of an object, but the way you think about a problem and implement its solution is almost the same!

Best Answer

As you've observed, for much so-called object-oriented code, the difference to procedural programming is purely syntactic.

There are many different opinions what Object Oriented Programming actually means. One viewpoint is: objects communicate by sending messages to each other. So instead saying “hey procedure, execute with this data”, we say “hey object, handle this message”. It is the object's responsibility of figuring out which code should be executed in response to a message, and the caller is not guaranteed that any specific procedure will be executed.

Making the function dispatch the responsibility of the receiving object opens up some interesting opportunities: we have to make a difference between the public interface promised by the objects to its users, and the internal implementation of this interface. This also allows inheritance and dependency injection: we can create a new object that conforms to the same interface (answers the messages in a compatible way), and replace the original object – without having to change the calling code. Since messages are (supposed to be) values, they can also be sent over network, so object-oriented thinking lends itself to distributed problems such as microservices.

In practice, most languages don't actually use messages, and instead use a technique called dynamic dispatch: the object (or the class of an object) contain a table of function pointers. The layout of this table forms the interface of the object. So I can have many different objects that conform to the same interface, but implement it differently.

For example, let's think about a traffic simulation with cars and bikes. Cars and bikes have very different characteristics but for our simulation code, they are both traffic participants. A procedural approach would be to check with ifs and elses what kind of traffic participant we have:

Car_moveForward(Car*) { ... }
Bike_moveForward(Bike*) { ... }

// in using code:
for (int i = 0; i < len; i++) {
  if (participants[i]->type == TP_CAR)
    Car_moveForward((Car*) participants[i]);
  else if (participants[i]->type == TP_BIKE)
    Bike_moveForward((Bike*) participants[i]);
  else
    error("unexpected traffic participant type");
}

That's how a lot of code in C has to be written.

With OOP, we instead define an interface that describes which operations we need. All objects implement this interface:

Car::moveForward() { ... }
Bike::moveForward() { ... }

// using code:
for (int i = 0; i < len; i++) {
  // doesn't need to know which implementation to call
  participants[i]->moveForward();
}

So this interface allows us to remove explicit if/else checks for the concrete type. This also gives us a lot of extensibility: If we want to add another type of traffic participant (e.g. pedestrians), we would have to update every if-else type check in procedural code. But with an interface, we only have to implement that interface and our new type can already be used everywhere: code using an interface doesn't have to change when the interface is implemented by another type.

Many problems don't need these interfaces. Quite a lot of code can happily be procedural with OOP-ish syntax. One of the biggest use cases for interfaces is decoupling and dependency injection, especially for unit testing. For example, an application might make database requests. How can I test this without setting up a database? First, introduce an interface that describes what I need from the DB, then implement that interface for the DB. But my tests can use a mock implementation of this interface that just returns static data instead of the real DB implementation.

The Design Patterns book contains a collection of problems where object-oriented techniques can provide an elegant solution. They are all based on the idea of using interfaces so that calling code doesn't need to know the concrete type in advance.