OO Relationships

In this small note we recap concepts of objects relationships in Object-Oriented programming.

Recently Allen Wirfs-Brock raised an interesting topic for distinguishing and abstracting a concept of “an open set of objects that share a common interface and implementation”.

A first possible definition that comes in mind can be a “type”, or a “class”. However, objects relationships in object-oriented world may be a bit more complicated.

Code reuse through objects relationships

As we know, practical implication for OOP in general, is a convenient code reuse: we form our systems mimicking a “real world” relationships between groups of objects. Those can be just abstract figures, people, countries, planets — anything that can be described as an encapsulated individual unit with custom properties and specifics of behavior.

Objects communicate with each other through their public interfaces. Usually these are set of methods to which other object may appeal (”Hey, Wiki, can you explain this topic, please?”, by executing the wiki.explain();).

[ wiki ] → public interface ← [ world ]

Encapsulation

And the question of how an object will react internally in order to respond for request, that came from a public interface, is its own private details, known also as an implementation details. This describes the concept of an encapsulation.

Note: in a well-designed system, at user-level, never should objects directly appeal to internal implementation details of another object. If you depend on a private state of some object, at some point your code may just stop working, since objects reserve a right to reimplement or even remove completely some implementation details, as long as the public interface is preserved.

Some objects though may still go deeper into another objects details, and analyze their private things. This is called ”going a level below in the abstraction barriers”, and usually is implemented via a process called ”introspection”. Usually it can be related to a debug process, to frameworks that need to work on a lower level, or automatic code generation. Introspection can be implemented at a language level, and also debuggers usually provide good introspection tools, allowing to analyze very low-level implementation details that are not directly exposed to user-level.

Forming a classification

During communication objects may start noticing similarities between each other, and form into groups, making a classification, or a typing. Combined with a process of reproduction (or generation) we get a technical concept of a “class”:

A class is a factory that is capable of constructing objects, that share the same initial state and behavior.

Note: in design patterns, concepts of a ”factory” and ”constructor function” precede concept of a “class”. With some assumptions, a “constructor” is a “sugar” on top of a factory, and a class is a “sugar” on top of a “constructor function”. Exactly this chain is used in implementation of some dynamic class-based systems: Python, Ruby, and recently ECMAScript.

Dynamic vs. static implementations

Some systems (Java, C++) provide static class-based organization. This means an object cannot change its initial behavior or set of properties once is created by its class.

Other systems (ECMAScript, Python) have fully dynamic nature, where objects may change their behavior and state. Methods resolution in this case is done via mechanism of delegation (aka dynamic dispatch): if a property or a method is not found in an object itself, there is an attempt to find it in the inheritance chain, which itself can be mutated and extended at runtime.

Main difference from the static system again, is that resolution is done at runtime, while in the static system it’s known at compile time (aka static dispatch), and hence it’s harder to change it.

Note: static dispatch (pointers known at compile time) vs. dynamic dispatch (pointers that may change at runtime) is a property of an implementation, it is just a technique, that can be used for implementation of a concept, e.g. inheritance.

First-class vs. Second-class

Another property related to statics and dynamics is a concept of a “first-class”, and “second-class” essence. A first-class object can be used as a normal data: e.g. stored in a variable, passed as a parameter, etc. In dynamic systems, functions and classes are usually first-class (examples again are Python, Ruby and ECMAScript).

class Point {}
class Point3D {}
Point.instances = 0;
Point3D.instances = 0;
function factoryByClass(classObject) {
classObject.instances++;
return new classObject();
}
factoryByClass(Point);
factoryByClass(Point3D);

And opposed, a second-class entity cannot be used as data — example is e.g. a C++ or Java class. There also could be variations, e.g. “first-class dynamic class”, or “second-class static class”, and other permutations. In some systems “second-class” entities still can be manipulated through introspection and reflection techniques.

One important take away from this: a concept of a “class” is not related to its implementation. It doesn’t matter whether a class is dynamic or static, it’s still a class. This is for the question, whether JavaScript has classes (or previously “whether JavaScript needs classes”): it always had classes — via the ”constructor function + prototype object” pair, which is got a bit sugared in ES2015 with the class keyword. Similarly how Python being a delegation-based (basically, a prototype-based) language, always had a sugar of classes out of the box.

You may refer to this small cheat sheet that describes “classification of classes” in various concrete implementations.

Prototype-based

In delegation-based inheritance, if objects decide not to form a classification, but deal individually by themselves, they may also just inherit from other objects. This approach is implemented e.g. in ECMAScript or Lua, and usually is known as prototype-based. It’s still an inheritance, just intermediate step (a class) can be omitted in this case, since objects may store all needed information (methods and properties) directly.

There are two sub-concepts here: delegation-based prototypes, and concatenative prototypes. Usually on practice (including in ECMAScript) exactly delegation-based prototypes are used.

Delegation-based prototypes use as we mentioned above dynamic dispatch for the implementation. In ECMAScript we may delegate using Object.create method (or explicit __proto__ link):

let base = {x: 10};
let child = Object.create(base);
child.y = 20;
console.log(child.x); // 10, inherited, via delegation
console.log(child.y); // 20, own
base.z = 30;
console.log(child.z); // 30, again via delegation

In concatenative model objects are just “blueprint copies” of their prototypes. After such objects are created, they can extend their own properties, but changing prototypes do not affect created children. In ECMAScript concatenative model can be seen on Object.assign behavior:

let base = {x: 10};
let child = Object.assign({}, base);
child.y = 20;
console.log(child.x); // 10, copy-"inherited" at creation time, own
console.log(child.y); // 20, own
base.z = 30;
console.log(child.z); // undefined, no delegation
Note: prototype-based concept is not related to its implementation. On practice that’s said usually delegation-based model with dynamic dispatch is used to implement prototypes (hence, sometimes “delegation-based”, and “prototype-based” definitions are used interchangeably). However, as we see, delegation is again just a technique to implement a concept.
It is also possible to implement prototypes with static dispatch as well, both: with inheritance and “blueprint copies”. From this perspective and C++ can easily become a “prototype-based” language. As an example, take a look at this delegation-based prototypes implemented in Python (although Python is itself already delegation based)

Vertical inheritance: a “Tower”

Now we also know that classes or objects themselves may inherit other classes or objects, forming a ”vertical inheritance chain”. On terminological jargon it’s called sometimes a “tower”:

        null
^
|
|
[ Root ]
^
|
|
[ Extended ]
^
|
|
[Even more specific]
Notice, the “vertical inheritance” exists irregardless of an implementation: it can be a static class-based system, a dynamic class-based, or a prototype-based, that operates only with objects.

Usually, every link in this vertical chain defines specifics that are directly related to this classification. This is a very important thing: directly related.

However, designing more and more convenient code reuse systems, we can go to reusing other objects functionality, that is not directly related to our classification. This is what is known as a “horizontal inheritance”.

Horizontal inheritance

There could be a generic functionality, that some of the links from our vertical inheritance chain may want to have, while this functionality may not be directly related to our classification. Hence it makes it not a good candidate to put it at the very beginning of our vertical chain, although often this design anti-pattern may be seen in practice.

For example, any object in a UI system can be ”draggable”.

Note: usually interfaces are named with “-able” suffix, underlying that they bring in some extra capabilities to the main vertical inheritance.

So we can extend our objects or classes with this extra functionality by attaching it from a side, hence the “horizontal” term:

        null
^
|
|
[ Root ]
^
|
|
[ Extended ] ← [ Draggable ] ← [ Serializable ]
^
|
|
[Even more specific]
Notice, how the horizontal chain may itself have a “vertical” inheritance chain.

Horizontal implementations: interfaces, protocols, mixins, traits

As for implementations of the horizontal inheritances, on practice they are usually can be heard as “interfaces”, or “protocols”, or even “mixins” and “traits”. From an abstract perspective, we choose here a concept of an “interface” as an umbrella term for all of them.

Usually an interface may only define a set of needed functionality not providing its actual implementation. Thus, the implementation is left to the classes that implement this interface (or “conform to this protocol”).

From this perspective, a “mixin” or a “trait” can be just a technique for an interface implementation, but not an interface itself. Although, the difference is so smooth here, that even a mixin can be an interface: it may or may not implement all the methods. E.g. you may choose to throw an exception from some method in a mixin, proposing to implement it, while still implementing all the other methods. In this case they may look similar to ”abstract classes”.

Duck typing

Going further we can even take one link out from our vertical chain, and make it independent:

[ Extended ] ← [ Draggable ] ← [ Serializable ]

In this case it becomes again just a vertical chain:

[ Serializable ]      
^
|
|
[ Draggable ]
^
|
|
[ Extended ]

However one important thing is happened: we went out from the borders of our classification. This means, that our [Extended] object(s) will correctly work in any system that expects a “draggable” or “serializable” functionalities, but its initial classification (from the [Root]) is not essential anymore.

In jargon it’s called a Duck typing: ”When I see a bird that walks like a duck and swims like a duck and quacks like a duck, I call that bird a duck”.

Composition

In case if we combine several independent sets of behaviors, e.g. via mixing several mixins or traits, we get a composed objects. A composition is often used as a better alternative for the “vertical inheritance” since allows picking only needed functionality from independent blocks.

┌────────┐
│ │ ← [ Draggable ]
│composed│ ← [ Logger ]
│ │ ← [ Serializable ]
└────────┘

Notice how in this case it’s not a “vertical tower” anymore, but a composition of more primitive parts. A composition may include implementation of interfaces, as well as other sub-objects, like Logger in this case. Combined with the “duck typing”, composition can be the most flexible and powerful code reuse technique.

Conclusion

Coming back to Allen’s poll of how can we name with one single word ”an open set of objects that share a common interface and implementation”, we see that the definition borders may still be very smooth in case. It might perfectly be named as a “class” or a “type”. However once we assume that objects don’t belong to this classification, it can be described as a “composition”, or even “interface implementation”.

Maybe you have an appropriate single word term for this? :) Let me know.

Main thing to remember though — the main purpose of all this, is still a very practical reason of code reuse. As long as we are able to design a predictable and maintainable system reusing needed functionality efficiently, we may smooth the definition borders, adjusting to the concrete cases.

Further reading

- ECMA-262 in detail: Chapter 7.1 OOP: The general theory
- Using Prototypical Objects to Implement Shared Behavior in Object Oriented Systems by Henry Lieberman

Show your support

Clapping shows how much you appreciated Dmitry Soshnikov’s story.