The three characteristics of OOP

 

One of popular interview questions is: “please describe the four characteristics of object-oriented programming”. In my observation, not many candidates — even the senior ones — can explain well Abstraction, Encapsulation, Polymorphism, and Inheritance. Worse, little did they know the drawback of Inheritance.

Procedural programming: data structures are not hidden

Procedural programming is actually not bad. It works! A lot of great software were written in this paradigm. Problem is that, if we aren’t careful, a data structure will be reused in too many different places, so a small change in that structure will lead to changes in all of those places.

Of course, if that structure is primitive (e.g. int, char, float …) then no problem, because it is stable. But if it’s a user-defined structure, chance is that it is unstable. Consider this example:

// Language: C

struct Person {
  char firstName[128];
  char lastName[128];
};

void printToConsole(Person* ps) {
  char fullName[256];
  sprintf(fullName, "%s %s", ps->firstName, ps->lastName);
  printf(fullName);
}

void saveToFile(Person* ps) {
  char fullName[256];
  sprintf(fullName, "%s %s", ps->firstName, ps->lastName);
  write(fullName);
}

void saveToDatabase(Person* ps) {
  char fullName[256];
  sprintf(fullName, "%s %s", ps->firstName, ps->lastName);
  insert(fullName);
}

// .. and many more functions using the Person struct

As we see, the Person‘s structure is very likely to change, e.g. adding a middleName field. That change is quite annoying since all the dependent functions printToConsole, saveToFile, saveToDatabase … must change also.

The only way to solve this problem is to find the abstraction of the Person concept and let all the dependent functions depend on this abstraction instead.

Abstraction: the most stable thing of a concept

Obviously, abstraction of a concept should be stable and unlikely to change. But sometimes finding the correct abstraction is not an easy task. It depends totally on how the concept is used and interacted in our domain. It is NOT about the corresponding concept in the real world. Do you think you can find the correct abstraction of the Person concept in the real world? No! Only when considering the Person in the particular context of the software under development, we are able to find its right abstraction.

Nevertheless, in any case, abstraction of a concept should be its behavior (a set of functions), rather than its data structure (a set of attributes).


Using behaviors as the basis of abstraction is the key of OOP.


This leads to the so-called Encapsulation.

Encapsulation: data structures must be hidden

A data structure is hardly the abstraction of a concept, simply because a behavior is always much more (or at least equal) abstract. A behavior can return data, so basically it could be a replacement of a data structure. But more important, every behavior has two aspects: prototype and implementation. The clients of a behavior, in order to use it, only require knowing about its prototype. Thus, a behavior can hide a lot of details — including a data structure — in its implementation. What is abstraction if not the hiding of details?!

Encapsulation is very closed to abstraction. While abstraction says that details should be hidden, encapsulation insists that the major part of those details is data structures.

Back to our example, now we are clear that the correct abstraction of Person is a behavior containing a function that returns the full name of Person (in other examples, this behavior may contain more than one function). This implies that, passing a Person around is basically passing functions around. We can easily achieve this by using function pointers which is a cutting-edge feature of the C programming language:

// Language: C

struct Person {
  char* (*fullName)();
};

void printToConsole(Person* ps) {
  char* fullName = (*(ps->fullName))();
  printf(fullName);
}

void saveToFile(Person* ps) {
  char* fullName = (*(ps->fullName))();
  write(fullName);
}

void saveToDatabase(Person* ps) {
  char* fullName = (*(ps->fullName))();
  insert(fullName);
}

The use of function pointers suggests a very powerful characteristics of behaviors: Polymorphism.

Polymorphism: different behaviors with the same prototype can replace each other

Consider a client using a behavior through a set of function pointers. As we know, a function pointer is just a normal pointer, it can point to any function as long as that function has the expected prototype. Consequently, we are able to pass other behaviors to that client. This makes the client quiet flexible since it can interact with many different behaviors without changing its source code.


When a concept is abstracted and encapsulated by a polymorphic behavior, we call it an object.

When we program by defining objects, creating objects and letting them interact in order to achieve dedicated tasks, we call it object-oriented programming.


Inheritance is not a characteristic of OOP

By inheritance, I mean implementation inheritance: the mechanism of creating an object (child) that inherits not only prototype but also implementation from the behavior of another object (parent) which is automatically created right before.

At first glance, this mechanism seems to be convenient. However it causes a tight-coupling between the child object and the parent object. Firstly, we cannot remove some functions that the child doesn’t want to inherit. Secondly, we don’t have the option of specifying a different parent object at runtime, because the parent is always created internally when the child is created.

Inheritance also introduced a mechanism so-called method overriding which is a very bad practice. By overriding, I mean implementation overriding: the mechanism of replacing existing implementation of a method by another implementation. This is so dangerous for the parent since the new implementation may not satisfy the contract of this method. Even if we care about the contract, sometimes satisfying it is impossible because it requires access to private state of the parent.

All of the problems above do not happen with aggregation. Prefer aggregation over inheritance and we may never need inheritance.

Conclusion

OOP is all about behaviors. Every object is abstracted and encapsulated by its behavior which is a set of polymorphic functions. While Abstraction, Encapsulation, and Polymorphism are essential to OOP and are beneficial, Inheritance is not and therefore should be less preferred than Aggregation.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s