The first object oriented language was Simula-67, an Algol-like language, specifically designed to run simulations, for example of cars waiting for traffic lights. Traffic lights, cars, crossings and roads could each be represented by objects, each with their own functional behaviour. You could design the functional behaviour of each of your objects and as a whole, they would simulate the traffic situations you wanted to analyse. It would also be extremely convenient to add special kinds of cars (or crossings), that could reuse most of the behaviour of their regular counterparts. This would later be known as inheritance.
Smalltalk would be the next object oriented language. Objects were modelled to literally send messages to other objects, Objects were modelled as little servers that could receive commands from anyone and then execute those commands and return the corresponding results to the sender of the original command. Method calls were not seen as a special type of function call, bound to an object, but as messages representing commands.
Everything in Smalltalk is an object, including numbers. A number would receive a ‘+’ command (followed by another number as a parameter) and then do the addition and return the sum as yet another number object. Smalltalk was integrated with a GUI and GUI objects turned out to be very suitable for the object-oriented model, including inheritance.
C++ came in the 1980s. In the 1990s, no programming language would be taken seriously if it did not embrace the object oriented philosophy. Languages like Pascal, Ada, Perl and Visual Basic would get object oriented extensions. New languages like Python and Java would be designed as object oriented languages from the start.
Data Structures and Methods
Early programming language like FORTRAN or Algol had only a very limited set of data types: scalar values like integers or reals, plus arrays of these scalar values. A date, consisting of a day, month and year needed three separate variables to store it. If you needed an array of dates, you would need three separate arrays (or maybe a multidimensional one). .
COBOL introduced grouping of data items into records(one of the few good things that we owe to COBOL). This way we would group the name, address and data of birth of a person into a single data structure. Niklaus Wirth proposed an Algol extension in the 1960s, called Algol-W. This Algol extension contained a record type, containing named fields, each with its own type. This feature was of course carried over to Pascal (designed by the same person).
In order to have an object oriented language, you need a way to group related data items into a single structure. These records are called “struct” in C and related languages.
Languages like Zig and Rust allow you to add methods to data structures. In essence, a method is just syntactic sugar to associate a function call with an object. For example we can now write my_object.my_method(); instead of my_method(my_object);.
Ada-95 has object-oriented features, but no method call syntax. Zig has method call syntax, but no inheritance (or even interface types). So method call syntax is neither required nor sufficient to make a language object oriented.
Inheritance
Sometimes there are different variants of an object type. In a university database, a person can either be a student or a staff member. Students and staff members share some data members and methods, but some data members or methods are different. Pascal allows variant records. Part of a record can contain different fields, depending on a ‘tag’ field. In C you can achieve the same effect by defining one of the fields in a ‘struct’ as a union. Each of the union variants can be its own ‘struct’ with the per-subtype fields. Each method has to select the appropriate subfields, depending on a tag field, which is included in the main (non-variant) part of the record or ‘struct’..
The problem with this approach is that all variants have to be baked into the record or ‘struct’ type from the start. All variant behaviour in methods is inside ‘switch’ or ‘if’ statements inside these methods. Adding more variants to the record type requires you to edit the source code of the data type definition and all the relevant methods. Variant records or tagged unions are a viable alternative to inheritance in many situations.
True object oriented languages however, support inheritance. You can define a type as a subclass of an already existing object type. The subclass type inherits the member variables (data fields) and methods (associated functions) of the base class. Then the subclass can add more member variables and more methods to it. Plus it can replace existing methods with its own, where an overridden method of a subclass can call the corresponding method of the base class as well. So in the university data base we can have a base class “Person”, with subclasses “Student” and “StaffMember”. The Student subclass gets additional information about completed courses and methods to manipulate these, the StaffMember class gets additional information about salary. The definitions of StaffMember and Student can be in separate files, without requiring any modifications to the definition of the Person class.
If you implement this type of inheritance in a compiled language, variables of the base object type and each of the subclass types have different sizes. Therefore you cannot have a variable that can hold an object type and each of its subclasses. You can only have variables that contain pointers to such objects, where each object was allocated separately, for example on the heap. Each object variable has to contain a pointer to a table of function pointers (often called ‘vtable’). Those function pointers correspond to all the methods a specific subclass has. Although the overhead is nothing compared to that of interpreted languages like Python, there is overhead nevertheless, caused by these levels of indirection.
Although inheritance can bring code reuse, it is often the case that behaviour of a single object gets spread across many source files, each corresponding to a different level in the class hierarchy.
Inheritance can be added to languages like C or Zig, but it requires manually implementing the vtable mechanism and careful casting of object pointers between the base class type and the relevant subclass type. The data structure of a subclass contains the complete data structure of the base class as its first member, so the pointer to the subclass type can be cast to a pointer to the base class type and vice versa. The first implementation of C++ was a preprocessor that converted C++ code to regular C and the generated C code would implement inheritance in exactly this way.
Multiple Inheritance
In some case, an object can be a special case of multiple things. In the category of 2D-shapes, a square can be either a special type of rectangle (with equal width and height) or a special type of rhombus (with right angles). In those cases you might want to inherit from two or more base classes. But if the classes Rectangle and Rhombus each have a draw method, which one to pick for the Square? This is sometimes called the “diamond problem”, due to the diamond-shaped class hierarchy (base class at the top, two subclasses at the sides and multiply-inherited sub-subclass at the bottom).
C++ and Python both have multiple inheritance. This adds much complexity to the language and it makes it harder to understand what the resulting object really is and how it behaves. In C++ the multiply inherited class gets two instances of the top base class, It is strange for a 2D-shape to have two distinct area member variables.
There are reasons why many object oriented languages like Java and Smalltalk restrict themselves to single inheritance
Abstract Base Classes
In the world of 2D-shapes, you can have triangles, rectangles, circles and hexagons, which can all be subclasses of a Shape base class. But you would never have ‘just’ a Shape object without specifying what kind of shape it is. The Shape base class could have methods like ‘draw’ or ‘compute_area’ and a function can take as parameter a reference to s Shape (which can then be any of the subclasses). But we would never instantiate a variable of just a Shape, only of the many subclasses this base class has. The Shape base class would specify the existence of the ‘draw’ and ‘compute_area’ methods but it would not implement them, only the subclasses would.
This pattern is called an abstract base class and it occurs many times in object oriented designs..
Interfaces
If we have an abstract base class without any member variables or implemented methods, we have in fact an interface.
An interface is basically just a bunch of methods that any object implementing this interface must have. We can have a pointer to any object that implements the interface and then we can call methods on that object.Under the hood we need vtable pointers along with each reference to an interface object.
In case of an abstract base class, we can inherit from this base class and implement all the required methods. This way we implemented the interface and we can have pointer to an object of the abstract base class that can later be used to call the methods.
But interfaces can also exist without proper inheritance. This is what many modern programming languages have. What inheritance does not provide, can often be implemented by composition. Suppose we have a database of all persons at a university. We can implement an interface called Person that defines all common methods. We can them implement a BasePerson structure with all fields that should be associated with any person, like name, address and date of birth, along with common functions. We can have the data types Student and StaffMember, each of which contains BasePerson as one of its fields and implements the Person interface.
The nice thing with interfaces is that a data type can implement several interfaces without too many problems.
Modern Languages
Most modern languages do not implement inheritance, let alone multiple inheritance. Go and Rust rely on interfaces (called ‘traits’ in Rust). Using interfaces and composition, you can get basically all the stuff done that object oriented programming could do, without many of the drawbacks.
Of the C-replacement languages, C3 does implement interfaces, while Zig and Odin require you to do everything by hand.
Object oriented programming seems to have been past its peak. Some of the good things will remain and where it is useful, it has its place. But making everything an object just for the sake of it, is no longer considered good practice.