Introduction
Object-Oriented Programming (OOP) has been a popular programming paradigm for as long as I could think of. Granted I started with C which isn’t really an OOP language but over the years I think I started favoring OOP more than structural code mainly due to trends and because I was in a journey of learning.
Today I will go over how this journey has taught me not to trust OOP so much and perhaps why I should have stuck with structural coding like C the whole time.
Many programmers find it confusing and I think this is for a very good reason. This tends to happen mostly when they are first starting out. Besides explaining why it’s confusing in the process I’ll go over some really basic stuff on the OOP concepts to clear out any misunderstandings in the process and help you out.
What is Object-Oriented Programming
Object-Oriented Programming is a programming paradigm that is based on the concept of objects. Objects are instances of classes and contain data and behavior. In OOP, objects interact with each other to accomplish tasks. Think of it as your body. Every cell serves a function and together they perform bigger functions. When put together they form an organ or a body part such as a hand etc. The subcomponents of each body part however each play a role to things and how they synthesize together.
Summary Of Why OOP Is Confusing
Using the above as a starting line I want to give you a quick summary in the next section as to why each concept of OOP is confusing in the hopes you get some quick answers if you are in a rush. So the areas I’m going to break down are basically what some will consider as the pillars of OOPS principles that a lot of OOP languages have used.
Reasons | |
---|---|
Polymorphism | If not careful can cause problems with naming |
Method Overriding | Similar naming |
Method Overloading | Similar naming, loss of code if one executes wrong |
Encapsulation | Overcomplicating code |
Inheritence | Multiple inheritance issues |
Abstraction | Overengineering |
As you can see above pretty much all core principles of OOP can be confusing. I know this was a really abstracted way of giving you the basic info but if you want to understand more read the details below that have examples associated with each.
Why is OOP Confusing
Below I’m going to start breaking down each principle of OOP and where I think it’s confusing. Enjoy the journey because this is going to be an intense one, I come from a deep background of using Java, C#/C++ and Python so I’m aware of some of the things I struggled and got confused about when using them.
Abstraction
One of the main reasons OOP can be confusing is the concept of abstraction. Abstraction is the idea of hiding complex implementation details behind a simple interface. This allows programmers to focus on the high-level behavior of objects without worrying about the underlying implementation details. However, abstraction can also make it difficult to understand how objects interact with each other.
It requires the designer to carefully balance the tradeoff between encapsulation and information hiding, on one hand, and the need to provide a clear and meaningful interface to the client code, on the other.
Consider the following code example in Python (which we will be re-using with alterations later to explain other confusing parts with OOP):
class Engine: def __init__(self): self._status = "stopped" def start(self): self._status = "running" def stop(self): self._status = "stopped" def get_status(self): return self._status class Car: def __init__(self): self._engine = Engine() def start_engine(self): self._engine.start() def stop_engine(self): self._engine.stop() def get_engine_status(self): return self._engine.get_status()
In this example, the Car
class encapsulates an instance of the Engine
class and provides methods for starting and stopping the engine. The abstraction is confusing because the client code only has access to the Car
class, and has no direct access to the Engine
class or its methods.
This abstraction can be confusing because it is not immediately clear what is happening when the client code calls the start_engine
or stop_engine
methods. The client code has no way of knowing what happens when these methods are called, or what the actual state of the engine is.
Moreover, this makes it difficult to change the implementation of the Engine
class without affecting the client code. If the implementation of the Engine
class is changed to use a different internal representation for the engine status, the client code may not be able to properly retrieve the engine status, because the get_engine_status
method may no longer work as expected.
Abstraction in object-oriented programming can be confusing because it can make it difficult to understand the behavior of the system and to maintain the code, especially when the implementation of a class is hidden behind an interface that provides limited information to the client code. To minimize the confusion, it is important to carefully design the interface of a class to provide a clear and meaningful API, while still providing sufficient encapsulation and information hiding to protect the implementation details of the class.
Inheritance
Inheritance is another OOP concept that can be confusing. Inheritance allows one class to inherit the properties and methods of another class. This allows programmers to create new classes that are based on existing classes, reducing the amount of code they need to write. However, it can be difficult to understand how inheritance works and how it affects the behavior of objects.
One of the most tricky things I’ve encountered is that some programming language such as Python allow multiple inheritence.
Multiple Inheritance in Object-Oriented Programming (OOP) refers to a feature that allows a class to inherit properties and behaviors from more than one parent class. This can become confusing because it can lead to issues such as ambiguity and the Diamond Problem.
Here is a code example in Python that demonstrates the ambiguity that can occur with multiple inheritance:
class Parent1: def method_one(self): print("Parent 1 Method One") class Parent2: def method_one(self): print("Parent 2 Method One") class Child(Parent1, Parent2): pass child = Child() child.method_one()
In this example, the Child class inherits from both the Parent1 and Parent2 classes. However, both Parent1 and Parent2 classes have a method with the same name method_one
. This leads to ambiguity in the Child class because it doesn’t know which method_one
to inherit.
This can become a complex problem and lead to unpredictable results. To avoid this, programmers often opt for composition over inheritance, where the Child class can delegate its methods to instances of Parent1 and Parent2 rather than inheriting from them.
It is important to note that some programming languages, such as Python, have ways to handle multiple inheritance in a more controlled manner to avoid ambiguity, but it is still considered a challenging and confusing concept in OOP. I know with Python you can always look at the method reverse order of the classes you are inheriting but this on it’s own adds more confusion and complexity in your code!
Polymorphism
Polymorphism is the ability of an object to take on multiple forms. In OOP, polymorphism allows objects to have different behavior based on the context in which they are used. This can be confusing because it can be difficult to understand how an object can have multiple forms and why it behaves differently in different situations. Particularly if you are a beginner and you are not familiar with most programming concepts this can really get to you.
Consider the following code example in Python:
class Animal: def make_sound(self): print("The animal makes a sound.") class Dog(Animal): def make_sound(self): print("The dog barks.") class Cat(Animal): def make_sound(self): print("The cat meows.") def make_animals_sound(animals): for animal in animals: animal.make_sound() animals = [Animal(), Dog(), Cat()] make_animals_sound(animals)
The output of this code is:
$ python ./test-polymorphism-confusion The animal makes a sound. The dog barks. The cat meows.
This example shows how polymorphism can be confusing because the make_sound
method is being overridden in the Dog
and Cat
classes, but when we call make_sound
on a list of animals, we expect it to behave differently for each type of animal, but the actual behavior is determined by the actual type of the object, not the declared type.
In this example, the make_animals_sound
function takes a list of animals
as an argument and calls the make_sound
method on each of them. The confusing part is that even though the list animals
is declared to contain Animal
objects, it actually contains objects of different types: Animal
, Dog
, and Cat
.
When we call make_sound
on each of these objects, the method that is actually executed is determined by the actual type of the object, not the declared type. When make_sound
is called on the first object in the list, which is of type Animal
, the Animal
class’s make_sound
method is executed. When it’s called on the second object, which is of type Dog
, the Dog
class’s make_sound
method is executed. This is polymorphism in action.
Basically OOP allows methods to behave differently based on the actual type of an object, rather than the declared type. This can lead to unexpected behavior, especially when dealing with inheritance and method overriding.
Polymorphism Combined With Inheritence
Polymorphism in combination with inheritance in object-oriented programming can be confusing because it can make it difficult to determine the correct type of an object at runtime, especially when objects are instances of subclasses that are derived from a common base class.
Consider the following code example in Python:
class Animal: def make_sound(self): return "Animal sound" class Dog(Animal): def make_sound(self): return "Bark" class Cat(Animal): def make_sound(self): return "Meow" def print_sound(animal): print(animal.make_sound()) a = Animal() d = Dog() c = Cat() print_sound(a) # Animal sound print_sound(d) # Bark print_sound(c) # Meow
In this example, the Animal
class defines a method called make_sound
, which returns the string “Animal sound”. The Dog
and Cat
classes both inherit from the Animal
class and override the make_sound
method to provide their own implementation. The print_sound
function takes an instance of the Animal
class as an argument and calls the make_sound
method on that instance.
This combination of polymorphism and inheritance can be confusing because it can be difficult to determine the correct type of an object at runtime. The print_sound
function is passed an instance of the Dog
class, it will call the make_sound
method of the Dog
class, even though the function is expecting an instance of the Animal
class. This can lead to unexpected behavior and make it difficult to understand the behavior of the system.
This can also lead to subtle bugs and errors, especially in situations where the behavior of a method is changed in a subclass in a way that is not expected. If a new subclass is added that overrides the make_sound
method in a way that is not anticipated by the client code, the behavior of the system may change in ways that are not expected.
Determining the correct type of an object at runtime, and can lead to subtle bugs and errors. To minimize the confusion, it is important to thoroughly test the behavior of the system, including the behavior of subclasses, to ensure that the behavior of the system is as expected. You should clearly document the expected behavior of each method, especially when the method can be overridden by subclasses, and to choose meaningful and descriptive names for classes and methods.
Method Overloading
Method overloading in object-oriented programming is the ability of a class to have multiple methods with the same name but different parameters. It can be confusing because it can make it difficult to understand which version of the method will be called, especially in situations where the method signatures are similar or the arguments are of similar types.
The following code example in Python demonstrates this:
class Shape: def area(self, width=None, height=None, radius=None): if width is not None and height is not None: return width * height elif radius is not None: return 3.14 * radius * radius else: return 0 s = Shape() print(s.area(width=10, height=20)) # 200 print(s.area(radius=5)) # 78.5
The Shape
class has a method called area
that can be used to calculate the area of different shapes, depending on the arguments passed to the method. This can be confusing because it is not immediately clear which version of the method will be called, especially when the arguments are of similar types.
This can also lead to ambiguity when the arguments passed to the method are of similar types, or when the arguments are derived from the same base class. In such cases, it can be difficult to determine which version of the method should be called.
It is also hard to know which version of a method will be called, especially in situations where the method signatures are similar or the arguments are of similar types. To minimize the confusion, it is important to choose descriptive and meaningful names for methods and to clearly document the expected parameters and behavior of each method.
Method Overriding
Method overriding in object-oriented programming is the ability of a subclass to provide a new implementation for a method that is already defined in its parent class. This can be confusing because it can make it difficult to understand the behavior of the system, especially when the subclass changes the behavior of the method in a way that is not expected.
Lets take a look at the code below:
class Animal: def make_sound(self): return "Animal sound" class Dog(Animal): def make_sound(self): return "Bark" class Cat(Animal): def make_sound(self): return "Meow" a = Animal() d = Dog() c = Cat() print(a.make_sound()) # Animal sound print(d.make_sound()) # Bark print(c.make_sound()) # Meow
Animal
class defines a method called make_sound
, which returns the string “Animal sound”. The Dog
and Cat
classes both override this method and provide their own implementation. This can be confusing because it can be difficult to understand what will happen when the make_sound
method is called on an instance of the Animal
class, especially if the client code is not aware that the behavior of the method can be overridden by subclasses.
If the subclass changes the behavior of the method in a way that is not expected by the client code. If a new subclass is added that overrides the make_sound
method in an unexpected way, the behavior of the system may change in ways that are not anticipated by the client code.
When the subclass changes the behavior of the method in a way that is not expected. To minimize the confusion, it is important to clearly document the expected behavior of each method, especially when the method can be overridden by subclasses. Thoroughly testing the behavior of the system, including the behavior of subclasses, to ensure that the behavior of the system is as expected.
Encapsulation
Encapsulation in OOP is a principle by which the implementation details of a class are hidden from the client code, and only the necessary and relevant information is exposed through a well-defined interface. It can be confusing because it requires the designer to balance the tradeoff between information hiding, on one hand, and the need to provide a clear and meaningful interface to the client code, on the other.
The code below will be a good example of this:
class Engine: def __init__(self): self._status = "stopped" self._fuel_level = 100 def start(self): if self._fuel_level > 0: self._status = "running" else: print("Unable to start engine: insufficient fuel") def stop(self): self._status = "stopped" def get_status(self): return self._status class Car: def __init__(self): self._engine = Engine() def start_engine(self): self._engine.start() def stop_engine(self): self._engine.stop() def get_engine_status(self): return self._engine.get_status()
Engine
class contains an instance variable _fuel_level
, which is not accessible from the client code. This is a good case of encapsulation, because the implementation details of the Engine
class are hidden from the client code, and only the necessary information is exposed through the start
, stop
, and get_status
methods.
However, the encapsulation can be confusing because it is not immediately clear what is happening when the client code calls the start_engine
method. The client code has no way of knowing what happens when this method is called, or whether the engine will start or not, because the _fuel_level
variable is hidden from the client code.
Moreover, the encapsulation can be confusing because it makes it difficult to change the implementation of the Engine
class without affecting the client code. The implementation of the Engine
class is changed to use a different internal representation for the fuel level, the client code may not be able to properly start the engine, because the start_engine
method may no longer work as expected.
Understanding the behavior of the system and to maintain the code, especially when the implementation details of a class are hidden behind an interface that provides limited information to the client code. To minimize the confusion, it is important to carefully design the interface of a class to provide a clear and meaningful API, while still providing sufficient information hiding to protect the implementation details of the class.