I've been teaching object orientation since the early 1990s when the craze first hit the mainstream. Since then I've seen it reduced to a sort of folk objects which misses the basis of what object orientation has always been about.
All object oriented languages, systems, designs and thinking are supported by three pillars:
These three are common to all object oriented systems of whatever kind.
I'm going to describe these in more detail and also explore some of the things that people tend to see as defining qualities of object orientation. What I'm not going to do though is give any examples of languages in any way¹ [1See if you can work out suitable example languages for the points that I make though. Feel free to discuss these in the forums.]. The reason is that I want to focus on the principles involved and not get bogged down in discussions about how to see the structure and features of differing languages.
In order to control its behaviour an object has to have two things: memory and code. The memory is there because it must know what has happened in the past so that knowledge can influence its future actions. The code is there so that the behaviour can actually do something. An object must have both of these things² [2Data on its own is called a data structure and code on its own is called a procedure or function.].
Normally these are called attributes for the data members and methods for the member functions³ [3Different languages often change this terminology either for historical reasons or simply to fit the terms used in the syntax.].
This part of the definition of object orientation can be looked on as taking encapsulation, as used within the structured programming paradigm, to its logical conclusion (a viewpoint I discuss more fully in Encapsulation is a Good Thing™).
Nearly every object oriented language is able to manipulate executable code as first order objects. The simple ramification of this is that functions can be passed as arguments and the languages also support closures⁴ [4This means that data used by the code is automatically transferred together with the code when it is passed around]. Many languages take this one step further and don't recognise any difference between an attribute and a method at all.
Because most object oriented languages treat data and code in much the same way it means that most support algorithmic programming in a way which is very close to the way it is done in functional languages. In many object oriented languages functional idioms can be used directly.
This is the most important and probably the least well understood aspect of object orientation. In essence it is very simple, but the ramifications and the different ways that languages deal with it has a huge impact on what is easy and what is hard to do in them.
When I explain this in a classroom I pick some poor unfortunate near the front of the room and thell them to stand up. I then turn to the person next to them and hand them a piece of paper with “stand up” written on it. I will then ask the second person their name and hand the first some paper with the question written on it. The idea here is to quickly show three things:
If I as an object want other objects to do things (like make people stand) then I have to communicate that command to them. Similarly if I want to ask them something (like their names) then I have to communicate that query to them and wait for their response. I can't look inside their heads and divine their names.
This disconnect between
is what the message passing in object orientation is all about.
All object oriented systems, at least conceptually, have this notion. At heart this is embodied into a message delivery mechanism (the implementation of which is normally called the message dispatcher) and it is the features of this mechanism that most strongly characterises different object oriented languages and systems.
In my classroom example the message dispatcher included the message medium (air or paper) as well as the recipients cognition of the command or question, but ends at the point where they start to do something (stand or answer).
Message dispatchers have two broad functions:
There are a huge number of different message dispatching systems implemented in all sorts of ways⁵ [5There are in fact so many ramifications to this that I'm not even going to attempt to describe them fully here. You'll have to wait for a more detailed explanation.]. Many languages have a single message dispatcher that is always used, and other languages let the programmer choose between one of several depending on what is being done. Nearly all object oriented systems also allow other message dispatchers to be created by the programmer as part of a system.
Some languages take this to an extreme and may run different objects in different threads using literal message passing as the communications medium. In large systems it is common to group objects into separate processes which can then be run individually, often on different physical (or virtual) machines.
In order for the message dispatcher to know how to match the message to an object method there is normally some notion of a message signature that is compared. Some languages use a single token (just a name), others use multiple tokens (typically one for each argument) and still others use a complex mechanism built from a name and the types of the arguments. As always other possibilities are possible (or example an identifying number).
By object specialisation we mean that objects are specialists (as opposed to generalists), not that they specialise over time (although this may also happen). This idea is very closely tied to the notion of cohesiveness and is the one pillar that isn't supported directly by object oriented languages other than through their support for being able to deal with multiple object definitions.
Normally the capabilities of an object (the form of its specialisation) is defined by its class (or type), but not all object oriented languages have a notion of type, in fact many are untyped.
In all cases though it is up to the programmer to determine the suitable level of specialisation and how this is to be implemented.
Good object oriented designs typically give each object a single, well defined responsibility. Where this responsibility can be broken down into sub-parts then these in turn are managed by separate sub-objects which are managed by their owner.
Another form of specialisation often seen in object oriented systems makes use of class hierarchies through inheritance. The super-class will implement the general rules for performing its task and then delegate the details to sub-classes.
There are a number of other things that are not defining themes of object orientation but do tend to be present.
For many people this is the defining aspect of object oriented systems. Historically inheritance has been an important aspect of implementing in object oriented languages. Later languages leveraged the concept in order to provide stronger type guarantees (most object oriented languages are untyped or very weakly typed) and this in turn has led to an increased focus on inheritance as the defining property of object orientation.
In practice the form of inheritance varies considerably between different object oriented languages, but whatever form it takes the simplest way of looking at the reasons for using inheritance involve one or more of:
Inclusional polymorphism can be seen as a formalism of operational polymorphism where the messages are defined as those that the super-class understands. This forms the basis of the type system within most strongly typed object oriented languages.
Inheritance is so convenient as an implementation tool and as a way of enforcing type constraints that nearly every object oriented programming language supports at least some notion of inheritance. It is however a much abused and much misunderstood feature. Because inheritance seems so simple and obvious it is often the main feature of object orientation that is taught (rather than the more difficult to grasp message passing).
As an analysis tool though it leaves much to be desired. It tends to promote a view of object oriented design which is based around classifying (or taxonomising) the problem domain, rather than the process oriented and dynamical analysis promoted by thinking in terms of message passing.
This seems so fundamental to object oriented systems that it seems nonsensical that it isn't required. There are however perfectly good object oriented systems that don't have classes.
The purpose of a class is to provide a definition for which attributes and methods all instances of that class posses. Object oriented languages that have classes normally fix this within the class definition and don't allow the objects to be extended during their lifetime. Systems without classes typically treat attributes and methods in a unified manner and allow these to be added to (and removed) during the object's lifetime. There are of course languages that allow the attributes and methods at construction to be specified for all objects of a given type, but still allow them to be changed later.
Clearly languages that don't have classes have a hard time with inheritance, but in practice this turns out not to be a problem.
For object oriented systems that have classes there must be a way of creating the objects. Some object oriented languages allow the objects to be created directly from the class definition, but in others there must be something that performs this build step.
Where systems have meta-classes then there will typically be a single meta-class instance for each class present in the system. These can be viewed as either object instance manifestations of the class or as singletons. These meta-class objects often serve two purposes:
If a language doesn't allow for meta-class objects then there will often be factory functions which perform the creation of the object instances. Of course some languages support meta-class instances, factory functions and direct creation of the objects from their class definitions.
It's not possible to leave this without taking a proper look at the issues of polymorphism. I've already mentioned two forms of polymorphism, operational and inclusional. There is also another form known as parametric polymorphism.
In a language which supports parametric polymorphism it is possible to state the features of a type that are acceptable. This form of polymorphism is rare in object oriented languages, but is quite common in functional programming languages.
Most object oriented languages use operational polymorphism. As mentioned, this means objects are considered compatible if they understand the same messages sent to them. This form of polymorphism is also common in functional programming languages (where the use of operator in the name is more obvious).
Inclusional polymorphism is used by strongly typed object oriented languages and involves the substitution of sub-classes for any of their super classes⁶ [6The Liskov Substition Principle, LSP, (described in the paper A Behavioral Notion of Subtyping by Barbara H. Liskov and Jeanette M. Wing) is widely seen as the prime formalism of this type of polymorphism. It is really talking about a wider notion to do with subtypes rather than sub-classes though.].
Whatever sort of polymorphism is employed it is a necessary consequence of treating the messages as an abstraction in their own right. It is impossible to think in terms of message passing and not have polymorphism. Once the message becomes something in its own right it is decoupled from the objects and can be re-routed, morphed, delayed or ignored. This makes operational polymorphism the most natural form of polymorphism in object oriented design, even when a given language doesn't support it directly.
Finally I think it's important to raise the issue of why use object orientation at all. The very first object oriented system was Simula⁷ [7Written by two fellow Norwegians, Kirsten Nygaard and Ole-Johan Dahl.] and the reason why it was created was a simple one: to help manage the complexity of the simulation software being worked on.
Most programmers today understand the difference between accidental difficulties and essential difficulties⁸ [8Most famously espoused by Frederick Brooks' No Silver Bullet: Essence and Accidents of Software Engineering.].
Object orientation, as a tool, is concerned with helping us to manage the essential difficulties. It is not, and never was, about removing accidental difficulties — that was left to individual languages and systems to deal with as they saw fit. A survey of object oriented languages will show a wide range of different techniques that deal with different types of accidental difficulty.
The very high encapsulation and the specialisation of objects makes then easier to reason about, implement and test because this implies high cohesion within any given object, or group of co-operating objects. The interstitial messages used by objects provides a very sharp knife that can be used to create highly decoupled systems.
We can all point to object oriented systems where the designs have failed to live up to these ideals, where the tools have not been used properly. Like any tool it takes time to learn how to use object orientation and many systems have been designed by those who misunderstood it, or were inexperienced. This doesn't really tell us anything about the effectiveness of the tool when used by those who know it better.
So long as people look to object orientation as a way to remove accidental difficulties they will always consider it a failure, and they'll be missing the point. The rest of us will continue to wield object orientation as our prime tool to manage the essential difficulties we face every day.