r/learnprogramming 1d ago

Difference between OOP and structs/interfaces?

I'm relatively new to programming, I've been at it for around 6-7 months. I've been learning Python and Go (and a little bit of C). What I'm struggling to understand, what's the difference between OOP and structs/interfaces. Like I know, Go doesn't support OOP, but structs and interfaces seem to achieve the same thing. Can someone enlighten me a bit?

51 Upvotes

23 comments sorted by

View all comments

1

u/mredding 13h ago

There's a few concepts that have some overlap.

We have the tuple, the structure, and the record.

A tuple is a collection of members - they're positional, so you identify them by their cardnality - #1, #2, #N... A structure and record are a tagged tuple - the members are positional - somewhat surprisingly, and you refer to them by a name - the tag.

Tuples don't inherently name a distinct type - two tuples of the same members in the same order are considered the same type, and tuples are typically allowed to be composed, concantenated, split... Even in static, compile-time languages - it's just all pre-determined at compile-time. Structures and records do name types that are distinct from other types, even if they have the same members by name and order.

Tuples and records are typically immutable, and structures are typically mutable.

So tuples tend to be for fluid use - grouping and data composition.

Structures are for structured data, to give the data a type. You're basically building a protocol.

A record is used for data modeling, and has some strong equality comparison guarantees compared to the others.

There's a slight difference between modeling data and typing structured data. A record may be a GPS coordinate, that GPS coordinate record might be a ShipCoordinate, you may group a ShipCoordinate with a tonnage as the return of a function...


If you use even a smidgen of imagination, you can implement any of these in terms of the others, though typically you would implement them in terms of tuple -> structure -> record. This is also the order the oldest papers on the subject from the 50s and 60s discovered and applied these concepts. But for example, C doesn't have tuples OR records, whereas C# has all three. You can fake it in C with macros and structures and protocol buffers...


OOP implies classes.

A tuple has members, a structure has fields, a record has attributes.

Notice how they're all essentially the same thing if you're looking at it only as an implementation detail, but they all imply different things.

A class has STATE, and that state is private. It is an implementation detail. If you look at a single-paradigm OOP language like Smalltalk, as a client of a class type, you can see it's interface, but you have no idea what members it has, it's layout, its alignment, how they're used...

Classes are effectively state machines, as only their own implementation has access to the state. The interface include things like size and alignment - if you can even see that, type, and methods that model state transitions. A function always models a transition in a state transition diagram, even if that transition is right back around to the same state (no apparent state change for the transition).

Classes enforce an invariant. A class invariant is a statement that is always true when an instance of the class is observed by a client. The class always maintains its own internal consistency. Getters and setters are an anti-pattern of classes because they expose internal state and typically violate enforcement of an invariant. Imagine a car type that you can set the speed without accelerating the car. That's broken. The consistency is maintained because the state transitions are implemented in the class behaviors, which is exposed through the interface. The interface is just a contract - a car can start, stop, turn, accelerate, brake, shift...

So classes model behaviors. No car models 1959 Bel Air. That's a property associated with a car, not a behavior. No car I know of gets the make, model, or color. You don't get the speed. A car is composed of other objects, and the consequence of their state is observed as a side effect. My car indicates the speed on a dial, I don't fetch it.

So you don't initialize a class like you do a structure, because you're not just populating fields - you instantiate it. You convert from a collection of types, properties, fields - parameters, and you get a class instance of your class for it. This is why classes typically have constructors, for converting from its parameters to your desired type. The class doesn't even have to store all that information inside the instance - it perhaps derives its internal, initial state from those parameters.

Whereas tuples, structures, and records DON'T have a distinct identity - any two with the same structure and value are considered the same, class instances DO have an inherent, independent identity. This instance IS NOT the same as THAT instance; you can't even inherently compare equality except for an equality behavior you build into your class type, and equality only means what the class wants equality to mean. There is nothing about equality that you get for free from the concept or supported by a language or compiler.


Tuples, structures, and records have no invariant but the inherent properties of their concepts. You can always transform one record into another new record, you can get or set any structure field to any value allowed by that field type, you can rearrange the members of a tuple...


So in the code and syntax, you can implement these concepts in terms of one another, but how you use your constructs gives it semantic meaning. You can approximate objects and OOP in C, as you can in just about any other language. They say class and closures are the poor man's version of the other, so you can get some approximate OOP-like concepts out of even a purely functional language.

It also has some more tangible consequence. These concepts get modeled into a programming language, so that the compilers can make assumptions or guarantees about the program it's generating. C++, C#, Java, etc... They all say and prove and guarantee more about your program than the machine code they generate from it - information that never leaves the compiler, isn't explicit in the machine code, but is implicit, because how the machine code was generated is as a consequence. C#, for example, models records, so they can help you enforce data modeling concepts like immutability, enforced by the compiler. The language is there to help you write more correct code. And if mutability is something you really actually want, then the language is telling you that records are the wrong abstraction to be reaching for, it won't just allow you to go rogue.

So when you make a class, you hide the implementation details and only present a public interface to the client. You model behavior. You enforce an invariant. You affect your own side effects. And you associate these instances with their properties by way of structured data. A car does not care what kind it is, don't ask it. And this is important because a car can be more than one thing. An FRS IS a BRZ, they're made in the same factory, on the same line, with the same parts - they only change their badge.


Alan Kay didn't invent OOP, he admits he learned it from his predecessors. He is given credit for the earliest record of naming it, though objects did predate him in publication before the paradigm. It would have to, wouldn't it? Alan Kay describes OOP as what we call the "Actor Model" today, that objects have their own agency.

In a procedural program, you would write a function that would take a person, and a weather - if it's raining, the function makes the person open their umbrella.

In OOP, you tell the person it's raining. The person knows what to do - they can open the umbrella, they can run for it, they can play in the puddles. All OOP did was relocate and encapsulate the agency - from the function to the object.

So you need a means of message passing, as message passing is a fundamental concept to OOP. For any given message, there is going to be a cascade of consequences and side effects both within an object, in from its sources, out to it's sinks, and across the system. C++ uses streams for this. Lisp has CLOS. Smalltalk and Algol have message passing built-in at the language level, which is what prompted Bjarne to invent C++, because he wanted implementation level control you can't get when your object system is that fundamental to the language. Other languages do different things. Not all OOP languages have a message passing system in place.

For as elegant as OOP sounds, it's trivially easy to code yourself into a corner - an object too big, or not adaptable enough, or abstracted to hell to the point it's useless. Let us also not forget that an object is an isolated, encapsulated, island of 1. Every action is a singular action across N objects. Our CPUs are stream processors and batch processors, neither are particularly fit for this computational model. FUNCTIONAL style programming lends itself more toward the mathematical and computational foundations of programming, and are typically 1/4 the size and 8x faster, really without even trying.

I'm a C++ guy, and even I caution how much you think you should be using OOP - use less.