December 10, 2011

Some thoughts about OOP

When I finished my career I thought OOP was all about classes, inheritance, this and GoF design patterns. I also was taught that it was the only valid programming paradigm.

I've been programming for a while now and I'm starting to have doubts on some of this truths.

What's exactly OOP?

Object oriented paradigm is a way to namespace data and methods based on the actors of our program. That simple? More or less; it depends on the implementation.

It also may include features such as data abstraction, encapsulation, messaging, modularity, polymorphism, and inheritance.

John Doe scenario

John Doe has the requirements for a new game based on Alfred Hitchcock's movie "the birds". His first implementation (in pseudocode) uses classic OOP:

class Dude {
  function paint() {
    // show the dude on the screen
  }
  function run() {
    // run implementation
  }
}

class Bird {
  static num_birds = 0
  function constructor() {
    Bird.num_birds++;
  }
  function paint() {
    // show the bird on the screen
  }
  function fly() {
    self.y++
    // hundred lines of amazing code
  }
  function attackDude(dude) {
    // attack implementation
  }
}

As you can see all the methods are now namespaced by the actors of this applications (birds and dudes), so there is no confusion when we say bird.paint().

There is also an extra parameter (implicit in most languages, explicit in Python for instance) called this or self.

This parameter contains the state of the instance. This way functionality and state are bundled together.

Requirement changes, the root of all evil

A new movie is out, is called "Superman" and John Doe's boss loves it.

He has a meeting with him and he is told that the main character of the game has to be able to fly.

He first tries:

class Dude extends Bird..

After introducing bugs (dudes shouldn't be able to attack other dudes!), he decides for a better option.

class FlyingThing {
  function fly() {
    self.y++
    // hundred lines of amazing code
  }
}
class Dude extends FlyingThing...
class Bird extends FlyingThing...

Next meeting: We want some chickens that can attack dudes but can run instead of fly! They should also increase the Birds.num_birds variable of course!

class Chicken extends Bird ?
class Chicken extends Dude ?

No, no, no... what about?

::: java
class RunningThing
class Chicken extends RunningThing, FlyingThing ?

Polymorphism or multiple inheritance are hard. Most of the OOP languages don't support it.

Classes are not a flexible way to represent our reality and inheritance is a wrong metaphor: Do we inherit from our father or our mother? Do we inherit their personality too Mr.Lamarck?

Saying foo is a bar because foo wants to reuse bar functionality is also wrong: A plane and a bird both can fly, but you can't relate both in any other way.

Is it really the true way?

In my opinion Object Oriented Paradigm is not wrong at all, I just think there are other ways to use it with less complexity by:

Sacrifice abstraction: Instances are not needed

Instances can save you some typing but IMO they increase complexity.

  • Classes can be singletons that namespace your functionality.
  • They don't need to contain any data, just methods.
  • No need to differentiate static and instance namespaces.

Sacrifice encapsulation: Decoupling methods from data

Instead of having an implicit self object you can just pass it as an argument (like Python does).

  • Functions are easier to test and they tend to have less bugs if they are idempotent. Classic OOP methods produce different results depending on the state of the instance.
  • It allows you to reuse functions by passing different contexts.

Lets implement "the birds" game with this new approach

I'm using Javascript to show a real implementation.

var Flyable = {
  fly: function (speed) {
    return function (self) {
      self.y += speed
      // hundred lines of amazing code
    }
  }
};

var Dude = {
  paint: function (self) {
    // show the dude on the screen
  }
, fly: Flyable.fly(1)
, run: function (self) {
    // run implementation
  }
};

var Bird = {
  paint: function (self) {
    // show the bird on the screen
  }
, fly: Flyable.fly(2)
, attackDude: function (self, dude) {
    // attack implementation
  }
};

var Chicken = {
  paint: Bird.paint
, attackDude: Bird.attackDude
, run: Dude.run
};

var dude = {name: 'Bill'};
Dude.paint(dude);

var birds = [{y: 0, x: 0}, {y: 1, x: 3}]
  , chickens = [{y: 2, x: 3}, {y: 2, x: 4}];

Chicken.run(chickens[0]);

// functional methods work better with explicit self
birds.forEach(Bird.fly);
birds.concat(chickens).forEach(Bird.paint);

// A chicken eats a magic mushroom and learns to fly... faster than anyone!
Flyable.fly(3)(chickens[0]);