Object Oriented Programming: The 4 Flimsy Pillars
This will be a quick dive into how everything these ivory tower professors taught you is false about OOP. Most professors in CS departments beat it into your head that these 4 pillars of OOP opened the third eye of programmers and allowed for programming to change and become the way to design systems and solve problems.
Let’s go over to 4 Pillars:
- Encapsulation
- Inheritance
- Polymorphism
- Abstraction
Encapsulation
The formal definition according to wikipedia is
encapsulation refers to the bundling of data with the mechanisms or methods that operate on the data. It may also refer to the limiting of direct access to some of that data, such as an object’s components. Essentially, encapsulation prevents external code from being concerned with the internal workings of an object.
Boiled down encapsulation is how you bundle your data or operations to a system to a concise interface and hide away parts you do not want exposed. Below is a code example in python which is probably the worse language to represent this.
class BankAccount:
def __init__(self, owner, initial_balance=0):
self.owner = owner
self.__balance = initial_balance # Private attribute (name mangling)
self._account_number = "ACC123456" # Protected attribute (convention)
def deposit(self, amount):
if amount > 0:
self.__balance += amount
return f"Deposited ${amount}. New balance: ${self.__balance}"
return "Invalid deposit amount"
def withdraw(self, amount):
if 0 < amount <= self.__balance:
self.__balance -= amount
return f"Withdrew ${amount}. New balance: ${self.__balance}"
return "Insufficient funds or invalid amount"
def get_balance(self): # Public method to access private data
return self.__balance
# Usage
account = BankAccount("Alice", 1000)
print(account.deposit(500))
print(account.withdraw(200))
print(f"Balance: ${account.get_balance()}")
Here is the same example but in an actual language meant for adults
class BankAccount {
private:
std::string owner;
double balance; // Private data member
std::string accountNumber; // Private data member
public:
// Constructor
BankAccount(const std::string& ownerName, double initialBalance = 0.0)
: owner(ownerName), balance(initialBalance), accountNumber("ACC123456") {}
// Public interface methods
std::string deposit(double amount) {
if (amount > 0) {
balance += amount;
return "Deposited $" + std::to_string(amount) +
". New balance: $" + std::to_string(balance);
}
return "Invalid deposit amount";
}
std::string withdraw(double amount) {
if (amount > 0 && amount <= balance) {
balance -= amount;
return "Withdrew $" + std::to_string(amount) +
". New balance: $" + std::to_string(balance);
}
return "Insufficient funds or invalid amount";
}
// Getter method for private data
double getBalance() const {
return balance;
}
std::string getOwner() const {
return owner;
}
};
The problem with being taught this in school is most professors champion this as something unique to OOP or that OOP made encapsulation better.
OOP really just moves the encapsulation around. One of the most annoying things is the private/public member with all the getters and setters. If most of your members or all of your members are private and you have getters/setters
class Person:
def __init__(self):
self.__name = ""
def get_name(self): return self.__name
def set_name(self, name): self.__name = name
vs
class Person:
def __init__(self):
self.name = ""
This is crazy to me. This creates extra memory and stupid abstractions just to modify or retrieve a member that should be public. This happens all the time in the wild.
Here are some non OOP ways to handle encapsulation.
Use Yo Struct
Python does not have structs in the traditional sense but in a real language it may look like
// Instead of hiding data in objects
struct Player {
float x, y, z;
float health;
int weapon_id;
};
// Functions operate on the Struct
void update_player_position(struct Player* players, int count, float dt);
void apply_damage(struct Player* player, float damage);
python has dataclasses and with slotting them you can kinda have a struct and save a bit of memory.
@dataclass(slots=True)
class Point2D:
x: float
y: float
z: float
health: float
weapon_id: int
You could also used NamedTuples if you want immutable data. This way you don’t have to deal with a whole object and the public/private issues that can arise. You also can take a more functional approach of operating on the data if you want.
C Did it Better
Before the days of OOP, The C language had stronger encapsulation than a lot of OOP languages.
// header file
#ifndef FILE_H
#define FILE_H
#include <stddef.h>
// Forward declaration - compiler knows File exists but not its contents
typedef struct File File;
// Public interface - users can ONLY use these functions
File* file_create(const char* filename);
int file_write(File* file, const char* data);
char* file_get_filename(File* file); // Controlled access to filename
void file_destroy(File* file);
#endif
// implementation file
#include "file.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// The ACTUAL struct definition - ONLY visible in this file
struct File {
FILE* handle;
char* filename;
int is_open;
size_t bytes_written;
int magic_number;
};
File* file_create(const char* filename) {
File* file = malloc(sizeof(struct File));
if (!file) return NULL;
file->filename = strdup(filename);
file->handle = fopen(filename, "w+");
file->is_open = (file->handle != NULL);
file->bytes_written = 0;
file->magic_number = 0xDEADBEEF;
return file;
}
int file_write(File* file, const char* data) {
if (!file || !file->is_open) return -1;
size_t len = strlen(data);
size_t written = fwrite(data, 1, len, file->handle);
file->bytes_written += written;
return (written == len) ? 0 : -1;
}
// Controlled access - we decide what users can see
char* file_get_filename(File* file) {
if (!file) return NULL;
return file->filename;
}
void file_destroy(File* file) {
if (file) {
if (file->handle) fclose(file->handle);
free(file->filename);
free(file);
}
}
The user can only use what is in the header file and the program would crash if the user tried to reference internal File
members.
In my mind this is a much stronger enforcement of the encapsulation pillar and it’s in C. There ain’t no objects here
Inheritance
This was just a bad idea. Every time people reach for this tool it bites them back two fold. I would say that many people try to not use inheritance or if forced to they try not to go more than one layer deep. Honestly, this deserves its own rant but lets just hit on why this sucks.
The Abusive Parent
If you need to change the base class which happens more than it should in the real world then prepare to get beaten. You force all the other implementation classes to carry whatever bloat you made in the base class.
You also end up in confused pretty quickly. You have to keep hopping down the trail into the nested class structure and most people when they get 3+ layers deep their brain gives up trying to solve the murder mystery.
class Rectangle:
def __init__(self, width: float, height: float):
self._width = width
self._height = height
def set_width(self, width: float):
self._width = width
def set_height(self, height: float):
self._height = height
def get_width(self) -> float:
return self._width
def get_height(self) -> float:
return self._height
def area(self) -> float:
return self._width * self._height
class Square(Rectangle):
def __init__(self, side: float):
super().__init__(side, side)
def set_width(self, width: float):
self._width = width
self._height = width
def set_height(self, height: float):
self._width = height
self._height = height
In this stupid example the square class is forced to hold onto the height and width attribute of the parent. It has to manage these resources. A square does not need a width and a height to maintain. It can be the shared value since width and height are equal in a square but now you get to make sure they are always equal.
No more Abuse
Other ways to handle this situation could be composition, traits, entity componenet system, etc…
@dataclass
class Position:
x: float = 0.0
y: float = 0.0
@dataclass
class Velocity:
dx: float = 0.0
dy: float = 0.0
@dataclass
class Health:
current: int = 100
maximum: int = 100
@dataclass
class Render:
sprite: str = "?"
color: str = "white"
@dataclass
class Player:
name: str = "Unknown"
@dataclass
class Enemy:
damage: int = 10
@dataclass
class Powerup:
effect: str = "heal"
value: int = 20
# Entity is just a container for components
class Entity:
def __init__(self, entity_id: int):
self.id = entity_id
self.components: Dict[Type, Any] = {}
def add_component(self, component: Any) -> 'Entity':
self.components[type(component)] = component
return self
def get_component(self, component_type: Type):
return self.components.get(component_type)
def has_component(self, component_type: Type) -> bool:
return component_type in self.components
def remove_component(self, component_type: Type):
if component_type in self.components:
del self.components[component_type]
# World manages all entities
class World:
def __init__(self):
self.entities: Dict[int, Entity] = {}
self.next_id = 1
def create_entity(self) -> Entity:
entity = Entity(self.next_id)
self.entities[self.next_id] = entity
self.next_id += 1
return entity
def get_entities_with_components(self, *component_types: Type) -> List[Entity]:
result = []
for entity in self.entities.values():
if all(entity.has_component(comp_type) for comp_type in component_types):
result.append(entity)
return result
def remove_entity(self, entity_id: int):
if entity_id in self.entities:
del self.entities[entity_id]
# Systems operate on entities with specific components
class MovementSystem:
@staticmethod
def update(world: World, delta_time: float):
# Find all entities with both Position and Velocity
moving_entities = world.get_entities_with_components(Position, Velocity)
for entity in moving_entities:
pos = entity.get_component(Position)
vel = entity.get_component(Velocity)
# Update position based on velocity
pos.x += vel.dx * delta_time
pos.y += vel.dy * delta_time
In this pattern you have a container that holds all the entities and then you have systems that query and operate on the entities held.
You could use composition
class KiBlast:
def fire(self):
return "💥 KAMEHAMEHA! Powerful ki blast fired!"
def charge_up(self):
return "⚡ Charging ki energy..."
class FlightAbility:
def fly(self):
return "🚀 Flying at super speed!"
def land(self):
return "Landing gracefully"
class Transformation:
def __init__(self, form_name, power_multiplier):
self.form_name = form_name
self.power_multiplier = power_multiplier
self.active = False
def transform(self):
if not self.active:
self.active = True
return f"⚡ TRANSFORMING TO {self.form_name.upper()}! Power x{self.power_multiplier}!"
return f"Already in {self.form_name} form"
def power_down(self):
if self.active:
self.active = False
return f"Powering down from {self.form_name}"
return "Not transformed"
class Scouter:
def scan(self, target_name):
power_level = 9000 # It's over 9000!
return f"{target_name}'s power level is {power_level}!"
# Fighter uses composition - they HAVE these abilities
class Fighter:
def __init__(self, name):
self.name = name
self.power_level = 1000
# Optional abilities - start as None
self.ki_blast = None
self.flight = None
self.transformation = None
self.scouter = None
def learn_ki_blast(self):
self.ki_blast = KiBlast()
return f"{self.name} learned ki blast techniques!"
def learn_flight(self):
self.flight = FlightAbility()
return f"{self.name} learned how to fly!"
def gain_transformation(self, form_name, multiplier):
self.transformation = Transformation(form_name, multiplier)
return f"{self.name} gained {form_name} transformation!"
def get_scouter(self):
self.scouter = Scouter()
return f"{self.name} equipped a scouter!"
def attack(self):
if self.ki_blast:
return self.ki_blast.fire()
return f"{self.name} throws a punch!"
def fly_around(self):
if self.flight:
return self.flight.fly()
return f"{self.name} can't fly yet - running instead!"
def power_up(self):
if self.transformation:
return self.transformation.transform()
return f"{self.name} powers up normally"
def scan_enemy(self, enemy_name):
if self.scouter:
return self.scouter.scan(enemy_name)
return f"{self.name} can't scan power levels without a scouter"
# Create different fighters with different abilities
print("Creating Dragon Ball Z fighters:")
# Weak fighter - just basic abilities
krillin = Fighter("Krillin")
print(f"🧑 {krillin.name} created")
print(f"{krillin.name}: {krillin.attack()}") # Just punches
print(f"{krillin.name}: {krillin.fly_around()}") # Can't fly yet
print()
# Train Krillin
print(f"💪 {krillin.name} training:")
print(krillin.learn_ki_blast())
print(krillin.learn_flight())
print(f"{krillin.name}: {krillin.attack()}") # Now has ki blast!
print(f"{krillin.name}: {krillin.fly_around()}") # Now can fly!
print()
# Powerful fighter - Goku with everything
goku = Fighter("Goku")
print(f"🧑 {goku.name} created")
print(goku.learn_ki_blast())
print(goku.learn_flight())
print(goku.gain_transformation("Super Saiyan", 50))
print(f"\n{goku.name} in action:")
print(f"{goku.name}: {goku.attack()}")
print(f"{goku.name}: {goku.fly_around()}")
print(f"{goku.name}: {goku.power_up()}")
This can be nice because not every object needs to have all the abilities or components. This does not force you into maintaining uneeded attributes.
PolyMorphing Power Rangers
This is like the Todd Howard of programming. No matter the object just call this function and it “just works”. Polymorphism is a way for one method or one symbol to represent or perform different types. I just dont understand why this gets taught as if OOP made this unique. Here is a caveman example of this in python
class Car:
def __init__(self, brand, model):
self.brand = brand
self.model = model
def move(self):
print("Drive!")
class Boat:
def __init__(self, brand, model):
self.brand = brand
self.model = model
def move(self):
print("Sail!")
class Plane:
def __init__(self, brand, model):
self.brand = brand
self.model = model
def move(self):
print("Fly!")
car1 = Car("Ford", "Mustang") #Create a Car object
boat1 = Boat("Ibiza", "Touring 20") #Create a Boat object
plane1 = Plane("Boeing", "747") #Create a Plane object
for x in (car1, boat1, plane1):
x.move()
Now the move
will function for any of the class instances. OOP did not invent this or strengthen it. It just packaged it differently.
Old Timers be Morphing
Get ready for this throwback. ALGOL 68 (1968) had polymorphism.
# Simple Union type example - ALGOL 68's form of polymorphism
BEGIN
# Define union type for numbers
MODE NUMBER = UNION(INT, REAL);
# Polymorphic procedure using CASE to handle different types
PROC print number = (NUMBER n) VOID:
CASE n IN
(INT i): print(("Integer: ", whole(i, 0))),
(REAL r): print(("Real: ", fixed(r, 8, 2)))
ESAC;
# Create different number types
NUMBER int num = 42;
NUMBER real num = 3.14159;
# Polymorphic usage - same procedure works with both types
print number(int num);
print(newline);
print number(real num);
print(newline)
END
This code is old as dirt and it handles both INT
and REAL
. This is essentially what OOP is doing so. In C you can get polymorphism via void pointers, VTables, etc…
Polymorphism is very powerful but OOP being some special harbinger is not true.
Abstraction
I have a lot to say on this area but for this post we will keep it brief and expand upon it at a later date. Abstraction involves hiding details from the user and only exposing the necessary features of an object. This doesn’t even make sense to be a pillar of OOP. This has been done since the dawn of programming. It’s like saying food fed by a spoon is a pillar of humanity. Food is for sure but I don’t know about the spoon.
So in the ideal world you get a nice clean object that you pray never crashes because if you have to untangle all the abstractions you are screwed my friend. You end up with all these strange hierarchical class structures that make no sense.
Sometimes you need to know or have access to the important parts of the program. If your database ORM library is having issues with optimized queries you need to be able to know why this is happening and how to correct it.
Regardless this just is not a pillar of OOP. If you want to try and claim it for OOP be my guest but it is a huge turnoff for me.
Here’s an example of “hiding” a filter function
def map_function(func, items):
return [func(item) for item in items]
You only expose the nice function to the user. This is a bad example and a stupid example but I see this exact thing all the time. It would be better to directly use the code that does the real operations.
For now know that this is not unique to OOP at all.
Conclusion: OOP Repackaged These Concepts
I will give credit where it’s due. OOP packaged these concepts and made them easy to understand for everyone. OOP when used properly can be powerful. My focus for this article was to show that OOP being taught as the champion of these 4 pillars is not neccearily true and many other non OOP languages do these pillars better.
OOP is a tool and you need to learn when to not use certain things. Just because you can inherit you probably should think twice before doing it.