Anyone who sought for a job and been in several interviews would probably experience how aggravating cliché questions could be. Even though I perceive that these questions have to be asked, I still get the shameful feeling when I am on the answering seat.
These questions may vary according to industry. Today, I'll try to fill a blog post about one of the most popular software industry interview topics since 1990s: Object-Oriented Programming Concepts. Hopefully, I will start with the fundamental concepts, only scratching the surface first, and afterwards I will update with another posts containing more advanced topics.
Object-oriented programming is a programming paradigm of choice, which is based on communication of objects. A program that uses OO paradigm is basically a collection of objects. Objects instantiated by classes can be thought of as templates or blueprints.
In OO software, unlike the chicken and-egg dilemma, we do know what comes first—the class. An object cannot be instantiated without a class. You must design a class before you can create an object.
Since nothing in life is absolute good or bad, OOP has its good and bad sides. On one hand, we can tell OOP programs increase productivity in the long run by having reusable code, more readable and maintainable. On the other hand, it creates slower and larger software, steeper learning curve, and tough designing process.
Let's clarify and deal with the most basic concepts first, since these concepts with their fancy names may be vague for first-time encounterers (checking these concepts from their wiki pages may double the trouble), even though it's already clear for active developers who perpetually bump into these concepts.
Fundamental Concepts
Object-oriented programming languages are defined by the following principles: Abstraction, encapsulation, inheritance, and polymorphism. Thus, if a language or program does not use these concepts fully, it's generally considered not fully object-oriented.
In other words, if these features lack or poorly implemented in your program, you'd just have a procedural program written with classes and objects. Just because a programming language is suitable for developing object oriented programs, doesn't mean every program you implement by using those languages is going to be object-oriented.
What is worse is when a programmer use just enough object-oriented features, making it bewildering for both procedural and object-oriented programmers.
Abstraction
The main goal of abstraction is hiding the unnecessary parts, which is often complained by complexity as well, from the user. Abstraction is a very generic concept which is not limited to only object-oriented programming and can easily be found in the real world as well.
When a computer requires electricity, plugging the computer's power cord to the electric outlet is enough to reach electricity. You didn't need to know the details; how and where from electricity reaches your city, street, apartment, etc. These details are abstracted out.
When a native code (the code that can only be run on specific platform) is used in your program, that portion must be isolated in a different class and should be abstracted out from the primary classes of the program. For example, if you want to access a serial port of a particular hardware, a wrapper class may provide this solution, which is later injected into your primary classes, resulting in an elegant solution for the situation.
Similarly, abstraction in object-oriented world means that hiding the internal implementation details from the users of the class.
Abstraction is related to both encapsulation and data hiding.
import java.security.*;
..
byte[] bytesOfMessage = myString.getBytes("UTF-8");
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] thedigest = md.digest(bytesOfMessage);
Encapsulation
Encapsulation is restricting access to variables of an object while bundling the related data together. An object must be filled with related data and its state can only be changed by itself.
The important thing to note here is to close the variable usage and modification to other objects, we have to give sufficient information to object and make the object do the calculations for us through public methods; instead of stripping data out of the object and do calculations. In order to achieve this, one must spend good amount of time to OO design process while keeping OO design principles in mind.
When a data type or method is defined as public, other objects can directly access it. When a data type or method is defined as private, only that specific object can access it. Another access modifier, protected, allows access by related objects.
Inheritance and Composition
One of the main principles of software engineering is DRY (Don't Repeat Yourself) principle. This can be achieved with the help of OOP paradigm greatly; especially with inheritance concept. The inheritance is about defining relationships of classes by basing the related ones on top of each other and reuse the already written code instead of typing it again.
While some languages (e.g. Java, Objective C) restrict classes to have an only one super-class, some languages (like C++) permit multiple inheritance.
The inheritance relationship is in the form of an is-a relationship. When a subclass inherits from a superclass it basically means that the subclass will be able to do whatever the superclass can do.
Another type of class relationship, has-a, is achieved with composition concept which means objects containing other objects in its own essence. Thus, objects are built, or composed, from other objects.
Though, inheritance creates fragile base classes by weakening the encapsulation of base classes. Since the base class and its subclasses are tightly coupled, when the base class is changed, the first thing that should be checked for is, if the change made ripple effects through its subclasses. Additionally, tests of the derived classes must be done once again after the base class is changed.
Composition lets you build the system by combining the less sophisticated parts. However, the main thing, which creates the real advantage is, since the subsystems are independent, they can also be tested and maintained independently. When the system is divided into smaller subsystems, it provides a system for the developer that is easier to comprehend, maintain, and test.
Composition is one of the primary strategies that you, as a software designer, have in your arsenal to fight software complexity.
Polymorphism
Polymorphism, in Greek, means having many shapes. In programming jargon it is used for same piece of code, operations or objects works differently according to its definition.
Ad hoc Polymorphism
Ad hoc polymorphism refers to functions with the same name having different arguments. It is also known as operator overloading, and happens at compile time.
int sum (int a, int b) { // Sum of ints
return a + b;
}
double sum (double a, double b) { // Sum of doubles
return a + b;
}
int main() {
cout << "The sum is " << sum(1, 2); // Calculated by the first function.
cout << "The sum is " << sum(1.2, 2.1); // Calculated by the second function.
}
Coercion Polymorphism
Coercion polymorphism is conversion of a value into a different data type. This can happen either explicitly or implicitly. Also happens at compile time.
double x, y;
x = 3; // implicitly coercion (coercion)
y = (double) 5; // explicitly coercion (casting)
Sometimes combination of coercion and overloading may result in ambiguity.
int sum(int a, int b) { return a + b; }
double sum(double a, double b) { return a + b; }
int main() {
cout << "Sum = " << sum(1, 1.2);
}
Parametric Polymorphism
Parametric polymorphism allows a function or a data type specified generically. So the input data is handled uniformly, independent from its type. While retaining full static type-safety, it makes a language more expressive.
class LinkedList<T> {
class Node<T> {
T elem;
Node<T> next;
}
Node<T> head;
void insert() { ... }
}
Subtyping
Subtyping or subtype polymorphism happens in run time with virtual methods. Implementation of pointed or referred object (subtype) is called. Most of the time this type is the one which comes to mind first, when polymorphism is mentioned.
class Felid {
public:
virtual void meow() = 0;
};
class Cat : public Felid {
public:
void meow() { cout << "Cat meow!"; }
};
class Tiger : public Felid {
public:
void meow() { cout << "Tiger MEOW!"; }
};
int main() {
Felid* cat = new Cat();
Felid* tiger = new Tiger();
cat->meow(); // Outputs Cat meow!
tiger->meow(); // Outputs Tiger MEOW!
}
Comparison with Legacy Systems
When object-oriented concepts moved to mainstream, one of the issues faced was making it compatible with the legacy and currently working solidly code. Alter all the legacy code with object-oriented version, and expect it to work same as before, would be a trivial and risky (anyone who is quite intimate with software knows how even minor changes may result in disasters) job. So keeping the legacy code and making them work as complementary has always been the first choice of an experienced programmer.
Procedural vs Object Oriented Programming
An object holds both data and behaviour in its own essence, and the keyword both here makes the difference between procedural and object oriented programming. Data and behaviour are held in different structures in procedural programming.
In structured programming data is frequently global, which leads to uncontrollable data. The globally accessible data means that it is hardly noticeable and maintainable that which function is using the global data. Objects tend to solve these problems by bundling and in the same structure.