After studying memory management, let us now look at the abstractions offered by programming languages to represent data, which is eventually stored in memory. Java or C ++ are said to be object-oriented languages. But what does this imply? The answer to this question will go a long way in understanding the modern tools that help developers write complex programs.
Objects, instances and classes
For those unfamiliar with the concept, object-oriented programming consists of grouping data into objects: fields and methods. To understand this abstraction, let’s again use an analogy - a coffee machine. In the object representing the coffee machine, there are two fields: “powdered coffee reserve” and “liquid coffee produced”. What are the methods? “Make coffee” and “put coffee powder” of course. When conceptualising objects, we distinguish between a class and instances of a class. In our example, the class is the description of the coffee machine, its fields and methods, while the instances are all the physical coffee machines that fit the description of the class.
What makes object-oriented programming interesting is the fact that the methods of an object act on its fields. The method “making coffee” of the coffee object decreases the supply of coffee powder and increases the level of liquid coffee in the coffee maker. Thus, a method called on different instances will produce a different result: making coffee with an empty coffee powder supply raises an error. Thanks to this, one can organize the data of this program - ultimately only numbers in the memory - in logical entities, the objects, which in turn are matched with what the program wants to achieve.
For example, if you want to program a game in three dimensions, each character will be represented by an object whose fields will contain the coordinates \(x\), \(y\) and \(z\). The object “character” will be endowed with various methods with explicit names such as “jump”, “run”, etc. From then on, the code of a program becomes readable and intelligible! Indeed, even if the C language had popularized the notion of
struct which is a grouping of fields, the complementarity between fields and methods allows for the first time to make a strong link between what the program seeks to achieve (characters moving in three-dimensional space) and the implementation of such a program (the object “character” ). The concept of and object is so all-encompassing that most of the time, it is very easy to model what the program does in terms of objects endowed with its fields, and methods acting on these fields.
Objects in memory
Le concept d’objet est une abstraction offerte par les langages de programmation, qui doit être traduite par le compilateur ou l’interpréteur. Comment un objet est-il traduit vers l’assembleur ? Expliquons ce qui se passe lorsque l’on déclare une
struct en C:
The concept of an object is an abstraction offered by many programming languages nowadays. This abstract notion of an object must be translated by the compiler or the interpreter into valid assembly instructions. How is an object translated to the assembler? Let’s look at what happens when we declare a
struct in C:
For those who aren’t aware of C, the
struct declared above is an abstraction for the
Character object. This object has three fields: the coordinates
y(which are floats) and an array of 5 integers
stats which describes some characteristics of the character (life remaining, coins collected etc). The first thing the compiler does when it encounters this object is to calculate its layout in memory. Indeed, the compiler knows the memory required for storing an integer(4 bytes) and float(4 also). It can therefore calculate that an instance of the
Character class will take a total of \(4 + 4 + 5 \ times4 = 28 \) bytes in memory. To be completely precise, it will associate each field of the object to an offset from the beginning of the object in memory.
As seen in the previous post, memory is simply a large array of bytes, each entry having an address. Thus, if an instance
pof the object
Character is in memory at address \(a\), the object occupies all memory locations between \(a \) and \(a 28 \). But the compiler can also deduce where the different fields are located:
xis between \(a\) et \(a+4\) (excluded) ;
yis between \(a+4\) et \(a+8\) (excluded) ;
statsis between \(a+8\) et \(a+28\).
Thus, the assembler’s way of accessing the
y field of the object is to read the bytes in memory between \(a + 4\) and \(a + 8 \). This is how the compiler will translate the instruction
p.y to access the
y field of the object. Similarly,
p.stats which notes access to the second element of the
stats array (actually third entry since the indexes start at zero) will be translated as: “read memory between \(a +20\) and \(a + 24\)”. The compiler completely hides these address calculations, allowing the developer to focus on the logic of her program. Even though this small example is relatively straightforward to explain, to completely translate an object-oriented language to assembler is a project of industrial scale. The details of such a project are beyond the scope of this modest blog.
After this little detour into the deep underbelly of low-level programming, lets get back to sea-level. Object-oriented programming allows us to introduce a concept with a barbaric but nevertheless essential name, polymorphism. Let’s go back to our example of a coffee machine. Suppose we have two classes “powder coffee machine” (you add powdered coffee) and “capsule coffee machine” (machines using nespresso capsules). Both these classes have a method “make coffee”, how do we formalize this commonality between these classes? The object-oriented solution to this problem is the notion of abstract class or interface. An interface is a contract that requires how a class should be implemented. The interface is expressed in terms of methods that will be owned by the class; for instance, here the method “make coffee”. So, if you want to be able to make coffee at home, you can simply ask to have an object honoring the interface “make coffee”.
But the object “make coffee” may very well be a kit to grind our own coffee beans and a wood fire to boil the water, which is not very practical. To ensure that the object is a machine, we define an abstract class “coffee machine” that has a field “power supply”. This class is called abstract because no instance will match this class; the real classes will be either “coffee powder machine” or “capsule coffee machine”. These last two classes will therefore be implementations of the abstract class.
Interfaces and abstract classes are concepts whose purpose is to formalize the sharing of fields or methods by different object classes. But these strategies are only one possible way to achieve polymorphism. The common theme lying behind polymorphism in the notion of encapsulation: we hide the way a set of data is represented, while offering a set of means of interact with it. In our example, the difference capsule/powder for our machines is encapsulated and disappears in the object “coffee machine”.
Polymorphism is an extremely powerful tool for the developer since it allows to modularize her programs, i.e. to precisely define the exchange of information between the different parts of her program. Indeed, hiding the representation of the data makes it possible to expose only what is necessary at the moment when one needs it, making it possible to avoid numerous bugs while guaranteeing security. To illustrate this, suppose we want to transfer money between two bank accounts. We can define a “transfer” method that takes as argument the two bank accounts and the amount to be transferred from one to the other. This “transfer” method is implemented in the bank’s software and this implementation is trusted. With polymorphism, we can declare this method “public” and make the field “account balance” private, i.e. hidden outside the implementation of the object. Other external programs will then be able to call the “transfer” method when they handle bank accounts, without being able to directly modify the “account balance” field.
Without polymorphism, it’s all or nothing: the bank can not let external software handle the “bank account” object without allowing for possible changes to the field “account balance”, which is a huge security breach. This issue is still relevant and the polymorphism is difficult to implement correctly. Indeed, recently, a computer attack against the Ethereum virtual currency was due to the fact that it was possible to call a certain function in a context where it should not have been possible.
Limitations of object-oriented programming
Becoming extremely popular in the late 90s, object-oriented programming has become a kind of cult in the small world of programming languages, some even going so far as to imagining it as the ultimate form of data organization and abstraction in computer programs. It is true that object-oriented programming, enriched by its static or dynamic inheritance mechanisms, I will not talk about it here, is a formidable tool that has brought programming into a new era. Code earlier obscured filled with references to the machine has been rendered clear and legible making it possible to reveal the reasoning underlying the program.
However, object-oriented programming occupies one extreme on the spectrum of programming language paradigms; at the other end we find functional programming, the subject of a future article. Indeed, there are situations for which the representation as objects is heavy: for example, an object containing only a single field, or single method. This over-objectification leads to extremely verbose source code, with a plethora of class declarations making code bloaty. Examples of this include the rise of IDEs (Integrated Development Environments), a sort word-processing software on steroids specialized for writing code; a Java IDE ends up writing half of the code via pre-defined models, in place of the developer.
But let’s take a step back. The representation as objects corresponds to the situation where one wants to group data together. However, how does one express the fact that data can take multiple forms, being something now and something else later? The object-programming solution that uses abstract classes is verbose and clumsy; we will see that functional programming takes a more balanced approach to data organization, where grouping and differentiating data are two complementary concepts.
Java Wikibook describes the main inheritance mechanisms mentioned in the article.
An explanation of engineering treasures deployed by Java to translate the language to the assembler, using a mixture of static compilation and dynamic interpretation.
A rather colorful article that highlights this asymmetry in the structuring of data introduced by object-oriented programming.