Language extremists from the far right are spreading misinformation online on the essence of object-oriented programming (OOP). They say things like ‘… a language feature would not be worthy of the name “class” without supporting inheritance …’ insinuating that inheritance is an essential feature of OOP. The ministry of truth feels obliged to set the record straight here.
The essence of OOP are clearly objects.
And, philosophically, an object is ‘a thing observed’.
This particularly means that an object is determined by its properties because, in general, this is all one can observe from a thing in the end.
In other words, an object is something which is defined intensionally.
Hence, the essence of OOP are things defined intensionally.
Given that characterization of OOP, it makes sense to say that a programming language is object-oriented if and only if its essential language constructs model intensional definitions.
And, as algebraic data types with pattern matching are the currently best choice for extensional definitions, algebraic codata types with copattern matching [PDF] are the currently best choice for intensional ones.
So, regarding object-oriented programming languages, codata is the way to go.
A codata type is pretty much like an interface as you might know it, and a copattern match procedurally abstracts ambient variables, classifying the objects yielded by instantiating these ambient variables.
This entails that copattern matches are classes.
A leisurely introduction to codata is Codata in Action [PDF].
As you can see, classes do not need a notion of inheritance to deserve their name. After all, this is not surprising as inheritance is rather a code sharing mechanism on top of objects inspired by biological inheritance. Note that as such it is very powerful and language designers should take care of the impact on their language. In particular, installing it in an unrestricted manner allows to break the encapsulation originating from the intensionality of objects, and, generally, adding a feature to a language which changes properties of others should not done without justification.
Additional Information for Advanced Truth Seekers
Readers with some knowledge on academic programming language research might wonder how this relates to the two prevalent basic models of OOP in type theory. In Simple Type-Theoretic Foundations For Object-Oriented Programming [PDF], a model of OOP is introduced based on type abstraction with existential types
\[\begin{align*} \exists \texttt{Rep} .\, \texttt{Rep} \times \left( \texttt{Rep} \to \lbrace \ell_{1} \colon T_{1}[\texttt{Rep}], \ldots, \ell_{n} \colon T_{n}[\texttt{Rep}] \rbrace \right) \end{align*}\]This simplifies the other famous model based on procedural abstraction with recursive record types
\[\begin{align*} \mu \texttt{X} .\, \lbrace \ell_{1} \colon T_{1}[\texttt{X}], \ldots, \ell_{n} \colon T_{n}[\texttt{X}] \rbrace \end{align*}\](which is very close to codata, by the way). Alas, the simpler model based on type abstraction is strictly lower-level than the one based on procedural abstraction in the same sense as the special case of a typed closure
\[\begin{align*} \exists \texttt{Env} .\, \texttt{Env} \times \left( \texttt{Env} \to T_{1} \to T_{2} \right) \end{align*}\]is strictly lower-level than an inhabitant of type $T_{1} \to T_{2}$ which procedurally abstracts ambient variables (e.g. an open lambda-abstraction).
Interestingly, that level-change manifests itself in making the autognostic principle (which is essential to OOP) [PDF] invalid.
The principle says more or less that an object cannot make assumptions about the internals of other objects and the existential type clearly violates that as the methods have arbitrary access to the abstracted type.
But there is a situation when this not the case.
Namely, if the type abstraction $\texttt{X} \mapsto T_{1}[\texttt{X}] \times \cdots \times T_{n}[\texttt{X}]$ uses the abstracted parameter only in strictly positive positions.
Then the methods cannot make use of the access to the abstracted type since there is nothing to access.
The striking fact is that in this case the existential type is a Church-encoding [PDF] in System F of the greatest fixpoint the recursive record type defines.
And this is nothing but the coinductive type arising from that type abstraction, modelling the corresponding codata type.
Addendum
A notion related to OOP are type classes. This is because simple type classes behave very much like the existential type
\[\begin{align*} \exists \texttt{Rep} .\, \texttt{Rep} \to \lbrace \ell_{1} \colon T_{1}[\texttt{Rep}], \ldots, \ell_{n} \colon T_{n}[\texttt{Rep}] \rbrace \end{align*}\]which is very close to the existential types modelling OOP. This existential type generalizes the type of lambda-lifts
\[\begin{align*} \exists \texttt{Env} .\, \texttt{Env} \to T_{1} \to T_{2} \end{align*}\]suggesting that one difference between OOP and type classes is a generalization of the difference between closures and lambda-lifts.