Why is OOP criticized?

We have previously talked about Object-Oriented Programming when discussing about why so many programming languages exist in this article:
Why are there so many programming languages?
That it was originally introduced in CPP and gradually other languages included it as well, as a feature. We have briefly gone through what it means - OOP is a design where you write code similar to how you view real world. Let us go into a bit more detail here before going to why people criticize it.
Object-Oriented Programming
OOP is a way to mimic the real-world entities, their state and their behavior.
If you want to code about a car, instead of writing like this:
car1 = {
color: 'red',
brand: 'Toyota',
speed: 40
}
car2 = {
color: 'blue',
brand: 'Toyota',
speed: 60
}
car3 = {
color: 'white',
brand: 'Tata',
speed: 50
}
def change_color(car, target_color):
car['color'] = target_color
change_color(car1, 'white')
change_color(car2, 'red')
def increase_speed(car, increment):
car['speed'] = car['speed'] + increment
increase_speed(car1, 1)
increase_speed(car2, 2)
def brake(car):
car['speed'] = 0
brake(car1)
brake(car2)
You can make a base class called Car, which is like a blueprint, which will take color and brand as the states when you define it and give you an object i.e., an instance of the class (or blueprint) and you can access that object's properties when required.
class Car:
def init(self, color, brand):
self.color = color
self.brand = brand
self.__speed = 0
def change_color(self, color):
self.color = color
def increase_speed(self, increment):
self.__speed = self.__speed + increment
def decrease_speed(self, decrement):
self.__speed = self.__speed - decrement
def brake(self):
self.__speed = 0
car1 = Car('red', 'Toyota')
car2 = Car('blue', 'Toyota')
car3 = Car('white', 'Tata')
# when you need to access a property, say, car1's color, you just do car1.color
# when you need to access a behavior, you can just do:
car1.change_color('white')
car1.increase_speed(2)
car1.brake()
car2.brake()
This makes the code more readable and much more intuitive. Now OOP also has some concepts you will need to know to use it properly.
Abstraction
Abstraction means providing a simplified, high-level view of an object, exposing only what's relevant to the user and while hiding the unnecessary internal details and complex implementations. It's closely related to another concept of OOP, called encapsulation.
Encapsulation
Encapsulation also means hiding the internal details of how an object works and only exposing what's necessary for other parts of the program to interact with it. This protects the data from accidental external modification. What is the difference between abstraction and encapsulation?
Abstraction is a design, and encapsulation is its implementation. Encapsulation tells us how exactly you can implement abstraction in the program. Abstraction is a concept while encapsulation is a mechanism.
In the above example, __speed is encapsulated. It is a private attribute and other parts of the code can't access it or modify it directly, outside of the Car class. We only provide them with increase_speed, decrease_speed and break methods to modify the __speed property in a controlled manner.
Inheritance
Imagine you have a basic Vehicle blueprint. It has wheels, an engine, and can move. Now, you want to create a Car and a Truck. Instead of starting from scratch, you can say, "A Car is a type of Vehicle," and "A Truck is also a type of Vehicle." They can inherit the common features of a Vehicle and then add their own specific characteristics and behaviors.
Inheritance allows a new class (called the child or derived class) to inherit attributes and methods from an existing class (called the parent or base class). This promotes code reusability and establishes a "is-a" relationship (e.g., a "Car is a Vehicle").
Example:
# Parent class
class Vehicle:
def init(self, make, model):
self.make = make
self.model = model
def start_engine(self):
print(f"The {self.make} {self.model}'s engine is starting.")
def stop_engine(self):
print(f"The {self.make} {self.model}'s engine is stopping.")
# Car is a child class inheriting from Vehicle
class Car(Vehicle):
def init(self, make, model, num_doors):
# Call the parent class's init method to handle make and model
super().__init__(make, model)
# Property specific to the child class
self.num_doors = num_doors
def drive(self):
print(f"The {self.make} {self.model} with {self.num_doors} doors is driving.")
# Truck is another child class inheriting from Vehicle
class Truck(Vehicle):
def init(self, make, model, bed_capacity):
super().__init__(make, model)
self.bed_capacity = bed_capacity
def haul_cargo(self):
print(f"The {self.make} {self.model} with {self.bed_capacity} capacity is hauling cargo.")
my_car = Car("Tesla", "Model 3", 4)
my_truck = Truck("Ford", "F-150", "1000 lbs")
my_car.start_engine() # Inherited from Vehicle
my_car.drive() # Specific to Car
my_truck.start_engine() # Inherited from Vehicle
my_truck.haul_cargo() # Specific to Truck
Polymorphism
The word "polymorphism" comes from Greek. It means "many forms".
Polymorphism means objects of different classes can be treated as objects of a common type. Or, put simply, you can have a single way of doing something, and different objects will respond in their own specific way.
Think of a "play" button on different media players. The "play" button on a music player makes music, while the "play" button on a video player plays a video. The action, pressing "play", is the same, but the outcome is different depending on the device.
Example:
from abc import ABC, abstractmethod
# An abstract base class for any kind of Media Player
class MediaPlayer(ABC):
def init(self, title):
self.title = title
self._is_playing = False
@abstractmethod
def play(self):
pass
@abstractmethod
def pause(self):
pass
def get_status(self):
return f"{self.title} is {'playing' if self._is_playing else 'paused'}."
class AudioPlayer(MediaPlayer):
def init(self, title, artist):
super().__init__(title) # Initialize the MediaPlayer part
self.artist = artist
def play(self):
self._is_playing = True
print(f"Playing audio: '{self.title}' by {self.artist}.")
# Can have its own implementation here
def pause(self):
self._is_playing = False
print(f"Pausing audio: '{self.title}'.")
# Can have its own implementation here
class VideoPlayer(MediaPlayer):
def init(self, title, resolution):
super().__init__(title) # Initialize the MediaPlayer part
self.resolution = resolution
def play(self):
self._is_playing = True
print(f"Playing video in {self.resolution} resolution.")
# Can have its own implementation here
def pause(self):
self._is_playing = False
print(f"Pausing video: '{self.title}'.")
# Can have its own implementation here
Composition
Take a look at the below example code.
class Bird:
def fly(self):
print("This bird can fly.")
def lay_eggs(self):
print("This bird lays eggs.")
class Eagle(Bird):
def hunt(self):
print("Eagle is hunting.")
class Penguin(Bird):
# Penguins can't fly, but they inherit the fly() method
def fly(self):
print("Penguins cannot fly! This is awkward.")
def swim(self):
print("Penguin is swimming.")
class Ostrich(Bird):
# Ostriches can't fly either
def fly(self):
print("Ostrich cannot fly! I'm a runner.")
def run_fast(self):
print("Ostrich is running very fast.")
eagle = Eagle()
penguin = Penguin()
ostrich = Ostrich()
eagle.fly()
eagle.hunt()
penguin.fly() # We had to override 'fly' just to say it can't fly.
penguin.swim()
ostrich.fly() # Same problem here.
ostrich.run_fast()
What's the problem here?
The Bird class has a fly() method because most birds fly. But then we have Penguin and Ostrich which are birds, but they cannot fly. We're forced to override the fly() method in their classes just to state that they can't do what their parent class implies they can. It indicates a design flaw: not all Bird objects can fly(), so fly() shouldn't be a core behavior of Bird.
Now with composition we do the following. Instead of saying "A Penguin is a Bird that can fly (but actually can't)", let's think about what capabilities an animal has.
We can define different "behaviors" as separate, smaller objects.
# Define behaviors as separate classes
class CanFly:
def fly(self):
print("This animal can fly by flapping wings.")
class CannotFly:
def fly(self):
print("This animal cannot fly.")
class CanSwim:
def swim(self):
print("This animal is swimming.")
class CanLayEggs:
def lay_eggs(self):
print("This animal lays eggs.")
class CanHunt:
def hunt(self):
print("This animal is hunting prey.")
class CanRunFast:
def run_fast(self):
print("This animal can run very fast.")
# Now, let's build our birds by "composing" these behaviors
class Eagle:
def init(self):
self.flying_behavior = CanFly()
self.hunting_behavior = CanHunt()
self.egg_laying_behavior = CanLayEggs()
def fly(self):
self.flying_behavior.fly()
def hunt(self):
self.hunting_behavior.hunt()
def lay_eggs(self):
self.egg_laying_behavior.lay_eggs()
class Penguin:
def init(self):
self.flying_behavior = CannotFly()
self.swimming_behavior = CanSwim()
self.egg_laying_behavior = CanLayEggs()
def fly(self):
self.flying_behavior.fly()
def swim(self):
self.swimming_behavior.swim()
def lay_eggs(self):
self.egg_laying_behavior.lay_eggs()
class Ostrich:
def init(self):
self.flying_behavior = CannotFly()
self.running_behavior = CanRunFast()
self.egg_laying_behavior = CanLayEggs()
def fly(self):
self.flying_behavior.fly()
def run_fast(self):
self.running_behavior.run_fast()
def lay_eggs(self):
self.egg_laying_behavior.lay_eggs()
eagle = Eagle()
penguin = Penguin()
ostrich = Ostrich()
eagle.fly()
eagle.hunt()
eagle.lay_eggs()
penguin.fly() # This correctly says it cannot fly
penguin.swim()
penguin.lay_eggs()
ostrich.fly() # This correctly says it cannot fly
ostrich.run_fast()
ostrich.lay_eggs()
Why is this better?
We can easily create new types of birds by mixing and matching these behavior objects. Want a flying penguin? Just give it CanFly behavior! You don't have to redefine an entire inheritance tree. Penguin and Ostrich no longer "pretend" to fly and then awkwardly tell you they can't. Their "flying" behavior is explicitly defined as CannotFly. And if we ever change how CanFly works, it only affects objects that have a CanFly object, not every class in a deep inheritance hierarchy.
This is the essence of composition: instead of inheriting features from a parent class, you build your objects by including instances of other objects that provide the desired behaviors. You're giving the object "parts" that define what it can do, rather than inheriting a whole "blueprint" that might contain unwanted features.
Things to be careful about
While there is a concept of static methods, which are like utility methods in a class, which can't access an object's state directly, you will notice that a vast majority of methods you write in a class often directly deal with some state of an object. After all, the fundamental idea of OOP is to encapsulate the state and behaviors into a self-contained unit, an object. This core philosophy by itself is where functional programming differs. But that's a topic for another time.
Meanwhile, you will notice that I have said that OOP lets you write a more readable and intuitive code. But that doesn't mean if you are using OOP, you are writing great code. You will find many people criticizing OOP.
One of the key reasons highlighted by many is exposure to bad usage of OOP in the code bases which made them hate it. Endless inheritance trees making it difficult to understand the code (ironic when OOP is made so code can be intuitive), fragile base class problem (meaning same base class being used by too many different classes - so when you need to make a change to base class in the future, you will have to go through all the child classes to make sure nothing breaks. Though this is what unit testing is for), uncontrolled state changes (meaning different parts of your program can affect your state making it hard to track how it will change), over engineering (using classes and objects where not necessary) along with some other reasons lead to the increased disillusionment with OOP.
So you can avoid that by properly understanding when and how to use OOP:
Try to model your classes based on intuitive entities.
Use it only when necessary. Not everything needs to be a class. Sometimes a simple function will do the job.
Proper encapsulation.
Use small classes with single responsibility instead of a single class with too many methods.
Try to use composition over inheritance when useful
Conclusion
You don't need to use every design principle of OOP in your code just because you can. Based on the problem you are dealing with, if only a few features of OOP can do the job, then use just those. Always look for simplest, cleanest and efficient answer. When you don't need composition, don't use it. When you don't need inheritance, don't use it. Sometimes you may not even need OOP, just a regular function might do. Then don't use it.




