Wednesday, January 30, 2008

Evolving the Object Oriented Paradigm Part I

Object oriented programming is a great paradigm. It's biggest problem is it's dead. "What?" you ask. "Object oriented programming is the majority of the industry and continues to grow with new OO languages with new features all the time!" This is true. But the OO paradigm itself has stopped evolving. The cornerstones of OOP like classes, interfaces, inheritance, etc. are virtually all carbon copies of each other in different languages. Not many languages work towards improving these cornerstones. What was the last language to really improve upon the idea of a "class"? We hide the missing features and rigidity of OOP with design patterns, mixins, delegates, functional programming concepts, etc. which are all good to have but we shouldn't need to rely them.

Proving by example

In this article, I'm going to walk through piece by piece a supposedly simple relationship of just 3 types: Shape, Rectangle, and Square. And for the sake of this article we'll be talking mostly about statically-typed languages like Java, C#, D, etc. and all three types are immutable (meaning you can't change the shape's internal data after instantiated). Sounds easy right? Well let's see and at the end I'll give how I think OOP could be improved to easily accommodate for the problems.


So let's start with the basic type: Shape. A Shape in this example only two methods: numSides() which returns its number of sides and getArea() which returns its area. The Shape class has no data itself at the moment and can't be instantiated so the designer has to choose between an abstract Shape class or a Shape interface (to some this is an easy decision but at the point its more about personal preference).


For this example, I'll assume Shape will never have any data or method implementations so I'll make an interface:


   interface Shape
{
int numSides();
float getArea();
}

Okay. One type done. Easy right? Keep reading.

Now it's Rectangle's turn. I want rectangles to be a concrete (or non-abstract) type so I make a class:

   class Rectangle implements Shape
{
protected float height, width;

public Rectangle(float h, float w)
{
height = h;
width = w;
}

public int numSides() { return 4; }
public float getHeight() { return height; }
public float getWidth() { return width; }
public float getArea() { return height * width; }
}

NOTE: I'm using Java syntax just to be uniform but it doesn't matter.

Now you're saying, "Two types done and I fail to see a problem." Keep reading.


Now it's time for Square. Square represents a fun quirk of OOP. Conceptually, a square is a rectangle where all of its sides are equal. When represented in a programming language, it needs less internal data than its parent class. Squares really only need the side length. Now you could just have a Square constructor that takes in a length and passes it on Rectangle's constructor as both the height and width. However, keeping both the height and the width is redundant. Now, one extra float per Square is trivial on a modern machine. But imagine you need to tens of thousands of them. And imagine this example is about two other classes in the same situation only the subclass needs 1 kilobyte less data than its parent class (maybe the parent uses a 1 kB array for part of an algorithm that the sub classes doesn't use). Now you are saving megabytes of memory in your program. And that can be significant. Especially if your project has a memory usage limit placed on it by your customer (those are always fun).


Since this isn't a third-party supplied class, we can make a IRectangle interface (I hate .NET for this naming convention) that Rectangle and Square will share and we'll place getHeight() and getWidth() in there:


   interface IRectangle extends Shape
{
float getHeight();
float getWidth();
}

class Rectangle implements IRectangle
{
// Implementation is the same as previous example.
}

class Square implements IRectangle
{
protected float length;

public Square(float l)
{
length = l;
}

public int numSides() { return 4; }
public float getArea() { return length * length; }
public float getHeight() { return length; }
public float getWidth() { return length; }
}
Under Java naming conventions, you may keep the interface called "Rectangle" and call the class "RegularRectangle" or "BaseRectangle" or some other fun name. No matter the naming convention you just introduced an unnecessary type. We have 4 actual types to represent 3 conceptual types. And if you had any code before Square was created that used the Rectangle class, you have to go back and change them.

And if Rectangle was from a third-party library that we couldn't change, the best non-inheritance method would be to create an adapter/wrapper class that took in a Rectangle and implemented our IRectangle interface:


   class RectangleWrapper implements IRectangle
{
protected Rectangle rect;

public RectangleWrapper(Rectangle r)
{
rect = r;
}

public int numSides() { return rect.numSides(); }
public float getArea() { return rect.getArea(); }
public float getHeight() { return rect.getHeight(); }
public float getWidth() { return rect.getWidth(); }
}

Hooray for unnecessary classes. Don't get me wrong. The Adapter pattern is great but it's ridiculous sometimes that you have to produce things like what's above. And we introduced yet another type. It took 5 actual types to represent 3 conceptual types.


But wait. There's more.

Okay, going back to our previous 4 type solution. Think that's all of the problems? Thinking it's only one extra type, what's the big deal? Read on.

Okay, let's imagine we have our 4 types: Shape, IRectangle, Rectangle, and Square. And someone (maybe the customer) decides this little library needs a Rhombus type. Piece of cake right?

Conceptually, a rhombus is a 4 sided polygon with all sides of an equal length. By definition, a square is a rhombus. But a square is also still a rectangle. From the current setup you have two options.

The first option is to declare a IRhombus interface, a Rhombus class and make Square also implement Rhombus. This can be seen here:

   interface IRhombus extends Shape
{
float getSideLength();
}

class Rhombus implements IRhombus
{
protected float length;

public Rhombus(float l)
{
length = l;
}

public int numSides() { return 4; }
public float area() { return length * length; }
public float getSideLength() { return length; }
}

class Square implements IRectangle, IRhombus
{
// rest of the implementation is the same

public float getSideLength() { return length; }
}

This solution works. Its flexible. But had to add 2 types. Up to 6 real types representing 4 conceptual types.


The second option is to not have an IRhombus interface and just have Square inherit Rhombus cause they have the same internal data. But conceptually, is a Square a Rhombus acting like a Rectangle? Or is it a Rectangle acting a Rhombus? It's neither because a Square IS both at the same time. And since most statically types OOP languages now disallow multiple inheritance, one must sacrifice true definition for the "next best" definition. In this very simple example, its clear which to pick but that's not always the case. If these Shape classes were more in depth (maybe so they could be drawn into a screen), a Rhombus would have extra data needed like the internal angles of the shape which Square would not need.

And all we did was add a Rhombus. We completely ignored plenty of 4 sided shapes: trapezoids, parallelograms, rhomboids, kites, and quadrilaterals that don't fit into any other category. Then we need round shapes, triangles, pentagons, hexagons, etc.

It gets worse.

Imagine someone using these classes wrote the following code:

   Rectangle rect = new Rectangle(6.0, 6.0);


This is perfectly legal. It's still a valid rectangle. But it's also a square. However any type checks of this object against a Square will fail because someone declared it with Rectangle's constructor. This can happen a lot if the passed in height and width are unknown at compile time and stored in variables.

Typical solution to this? Factory pattern. The factory pattern is a either a method or a class of methods that takes in input and decides which class to allocate and return. We'll put it in the Rectangle class (cause we can't put it in IRectangle where it belongs) as a static method to avoid creating a factory class and for another reason you'll see in a moment.

   // The following is part of the Rectangle class

static public IRectangle create(float height, float width)
{
if(height == width)
return new Square(height);
else
return new Rectangle(height, width);
}

static void test()
{
IRectangle r1 = new Rectangle(3.0, 4.0); // Rectangle
IRectangle r2 = new Rectangle(6.0, 6.0); // Rectangle

IRectangle r3 = Rectangle.create(3.0, 4.0); // Rectangle
IRectangle r4 = Rectangle.create(6.0, 6.0); // Square
}



And to truly prevent the "public" and inheriting classes from using Rectangle's constructor to make Squares like r1 and r2 above, we need to make the constructor private so they only use the factory method. If we had used a seperate factory class, we would have to keep Rectangle's constructor public which takes away the added safety of the static method has. So now that we have the factory method, any old code using "new Rectangle" (and preferably "new Square" too for uniformity) needs to be changed to use our factory method.

And we need to do the same thing to Rhombus.

The Point

Why is good object-oriented design this complicated to change? Unless you design everything perfectly before you write any code (good luck with that in the real world), your code will be constantly evolving. New features and new/different/misunderstood customer requirements evolves the software past the initial design. Changes made today may be taken back out in a month.

The object-oriented paradigm needs a language that can adapt to these things. In the next article, I'll present a proposal for a simpler OOP that deals with all these problems. And it represents Shape, Rectangle, Rhombus, and Square without redundant data (and regardless of third-party or not) in exactly 4 types.

See you tomorrow for Part II.

-Kaja

1 comment:

Bram W said...

Some time ago I started writing a non-numeric math library and faced similar problems.

Take for instance the invertible matrices. First of all you don't want to use the factory pattern for checking whether a matrix is invertible is quite expensive.
Secondly it would be nice to have abstract classes like field, ring, division ring, group, abelian group etc.. The matrices for instance, can be defined on any field, not exclusively the real numbers. However here arises a very nasty problem, which I haven't solved yet:

A division ring (e.g: invertible matrices) is both a group with regard to addition as multiplication.

Nice article.