In the last article, I talked about problems of properly modeling conceptual types (in that case, Shape, Rectangle, Rhombus, and Square) to actual types in a typical object-oriented language. I wrote about the rigidity of its inheritance and difficulty of evolving your code or using third-party code.
In this article, I'll propose two simple changes to the existing OOP model that fix all the problems I addressed. In future articles, I'll be proposing more new features but for now I'll be focusing on just two.
The "concept"
In the last article, I used the words "concept" and "conceptually" a lot. It is my opinion that the conceptual idea/model should be as close as possible (which is a tenant of something called "concept programming" which takes this idea to a whole new level that I will not).
For this first proposal, I have thrown out classes and interfaces as the base units of the object-oriented paradigm. Now you're asking, "Thrown out? How can you have OOP without classes and interfaces?" Calm down. They're not gone. But they're no longer seperate units. Instead, classes and interfaces have been merged into a new unit called the "concept" (may be a temporary name. not to be confused with concepts from concept-oriented programming or concept programming which are also not the same thing).
A concept therefore consists of two pieces: the "interface" and the "implementation" (Not the same as the Objective-C equivalents of these words either. It's hard to come up with original names).
The "interface"
Let's take a look at a Shape concept and explain by example:
abstract concept Shape
{
interface
{
int numSides();
float getArea();
}
}
An interface is a series of public methods that must be implemented by all concepts that inherit Shape. It is similar to interfaces in language like Java only interfaces except these are not a separate type (so you can't name the interface). A concept's interface is enclosed in a block as seen above.
An "abstract" concept either has no implementation or an incomplete one that can't be instantiated. More on implementations in a bit. Like the last article, Shape is abstract and will have no implementation so this is all we need to specify for Shape.
Concept inheritance
As with any OO language, there has to be some type of inheritance. Concept inheritance describes an "is a" relationship.
We need to declare our Rectangle concept which inherits Shape so let's declare just the interface part for now:
concept Rectangle : Shape
{
interface
{
int getHeight();
int getWidth();
}
// implementation to be filled in later
}
(Concept inheritance is represented by ":" in this example but that's irrelevant. Used for simplicity.)
A Rectangle "is a" Shape. But concept inheritance is "interface inheritance" not class/implementation inheritance. Rectangle inheriting Shape means Rectangle is agreeing to implement Shape's interface (in addition to Rectangle's own interface).
Now some of you are asking, "No more implementation inheritance?" Don't worry. It's here. We'll get to it later.
The "implementation"
Now, let's fill in all of Rectangle:
concept Rectangle : Shape
{
interface
{
int getHeight();
int getWidth();
}
implementation
{
protected int height, width;
public Rectangle(int h, int w)
{
height = h;
width = w;
}
public float numSides() { return 4; }
public float getHeight() { return height; }
public float getWidth() { return width; }
public float getArea() { return height * width; }
}
}
An "implementation" is the default implementation of a concept's interface within that concept. Its like the typical definition of a "class" except its bound to the concept like the interface and must implement all of its concept's interface (unless its abstract). Rectangle fulfills its inheritance requirement because it implements the two methods from Shape and the two methods in Rectangle's interface.
Going in the same order as the last article, we'll add Square next. Remember in the last article that we had to create a IRectangle interface and modified Rectangle to implement the new interface?
concept Square : Rectangle
{
implementation
{
protected float length;
protected Square(int l)
{
length = l;
}
// Methods to implement the combined interface from Shape and Rectangle
public int numSides() { return 4; }
public float getArea() { return length * length; }
public float getHeight() { return length; }
public float getWidth() { return length; }
// We'll talk about this in a moment
public float getPerimeter() { return 4 * length; }
}
}
There's a couple things to note here. First, Square does not specify an interface block because it has nothing of its own to add to the Rectangle interface. Second, we didn't have to create any new types. A concept always has an interface and therefore can always act like one.
Some of you are asking, "Well what if a coder creates a new concept which doesn't inherit anything and doesn't specify any interface but specifies an implementation?" The answer is only the methods specified in the interface are publicly callable on a concept variable. For example, in my new Square concept, there is a public getPerimeter() method in the implementation but not in its inherited interface. Well then its not visible on the Square type. So the second line:
Square s = new Square(6.0);
float p = s.getPerimeter();
will throw a compile time error saying getPerimeter() is not a method of Square's interface.
But sometimes you'll want to call getPerimeter() publicly. Sometimes you want to know if a variable is part of the concept just by the interface ("conceptually a Square") or whether it uses (or inherits) the concept's implementation ("physically a Square"). I'll use the type modifier "actual" to denote "physical" relationships.
Rectangle r1 = new Rectangle(3.0, 4.0); // Legal
actual Rectangle r2 = new Rectangle(3.0, 4.0); // Legal
Rectangle r3 = new Square(6.0); // Legal
actual Rectangle r4 = new Square(6.0); // Illegal. Square does not inherit Rectangle's implementation.
Square s1 = new Square(6.0); // Legal
float p1 = s1.getPerimeter(); // Illegal. getPerimeter() not in Square interface
actual Square s2 = new Square(6.0); // Legal
float p2 = s2.getPerimeter(); // Legal
So this "actual" keyword lets the programmer distinguish between which of the two inheritance types the object really is if needed. More on implementation inheritance right after this bold header:
Implementation inheritance
Implementation inheritance is a wonderful thing. Sometimes. When it's not abused. Everyone's seen it. Blindly inheriting file streams, collections, etc. and not changing any of its behavior rather than using them normally. People overriding one method with no idea about the base class internals and what effects this will have on the object and on other methods. It happens.
With this new concept design, I'm hoping it will encourage implementing the interface as opposed to automatically inheriting. But implementation inheritance is still necessary, so of course, it's included in this little design.
But first, let's define a Rhombus concept like the last article and show the Square implementing Rhombus's interface first.
concept Rhombus : Shape
{
interface
{
float getSideLength();
}
implementation
{
protected float length;
public Rhombus(float l)
{
length = l;
}
int numSides() { return 4; }
float getArea() { return length * length; }
float getSideLength() { return length; }
}
}
concept Square : Rectangle, Rhombus
{
implementation
{
// rest of the implementation is the same as previous Square definition
public float getSideLength() { return length; }
}
}
Notice again that we didn't have to define an IRhombus type. We have 4 actual types representing 4 conceptual types.
Also note that Square can inherit multiple concepts easily because we're just inheriting the interfaces and combining them.
Now we'll change Square to inherit the implementation of Rhombus in order to show off implementation inheritance. Implementation inheritance is much like class inheritance in other languages. To change over, all we need to write in the inheritance syntax and remove all fields and methods from Square that Rhombus already has (which leaves only the Rectangle-specific methods).
concept Square : Rectangle, Rhombus
{
implementation : Rhombus // says inherit Rhombus' implementation
{
public Square(float l)
{
super(l);
}
public float getHeight() { return length; }
public float getWidth() { return length; }
}
}
We just switched from implementing the Square interface manually to inheriting from Rhombus to do most of the implementation for us very easily without creating any new types or making old interface types useless.
Behold the flexibility of a concept.
Factories
Up until now, I had solved most of the problems yesterday except the using of the factory pattern to ensure:
IRectangle rect = new Rectangle(6.0, 6.0);
really became a Square. So we settled for this factory method:
IRectangle rect = Rectangle.create(6.0, 6.0);
But why must we settle? Why can't I do this with concepts:
Rectangle r = new Rectangle(6.0, 6.0);
Square s = (Square) r;
Well you can do this by using the factory pattern built into this little language here. First we'll add a private constructor to the Rectangle implementation to make our next step legal:
private Rectangle() { }
Simple. Now we'll introduce the "factory constructor". This is a special type of constructor that returns an object. That object must be a non-null object of its declaring concept (in this case Rectangle).
public factory Rectangle(float h, float w)
{
if(h == w)
{
return new Square(h);
}
else
{
actual Rectangle r = new Rectangle();
r.height = h;
r.width = w;
return r;
}
}
The factory constructor uses the keyword "factory" to distinguish itself from a typical constructor. Unlike the typical constructor, the factory constructor allocates the memory it needs during the constructor, not before. And then at the end of the factory constructor will be a built in check to make sure the returned value is not null. The private Rectangle constructor is a still typical constructor.
So the factory constructor looks like a normal constructor and is very simple to use:
Rectangle r1 = new Rectangle(3.0, 4.0); // Rectangle
Square s1 = (Square) r1; // Type conversion error. Not a Square
Rectangle r2 = new Rectangle(6.0, 6.0); // Square
Square s2 = (Square) r2; // Works fine.
See how much that simplifies things? Because one does not plan on the factory pattern until they need it and then have to go back and change their code to use factory methods instead of constructors.
And you know what else it's great for? Object pooling/caching. For example, did you know that in Java 5, autoboxing an "int" to an "Integer" calls the static Integer.valueOf(int) method and not Integer's constructor (the other Number wrapper classes do the same thing)? Why does it do this? Because the Integer class has a static array of Integer objects (Byte, Short, and Long do this too) for values -256 to 255, and if the passed in value is in that range, you get the corresponding object from that array, otherwise it allocates a new one. So Integer.valueOf(0) always returns the same object (for that class loader). And if they had factory constructors, they could move that logic into the constructor syntax and save some memory for people who don't know to use Integer.valueOf().
Review
I did not introduce a single revolutionary thought about the object-oriented paradigm in this article. I glued two existing standalone constructs together into the new standalone construct called a concept. I added a special syntax and a tiny bit of new constructor behavior to make factory constructors. That's it. And look at all the things from the last article I solved. These two little ideas provide a great deal more flexibility than the old model.
So why has the object-oriented paradigm stopped evolving? Why are other languages not looking for things like this? Why are they just carbon copying everything?
In upcoming articles, I'll be adding more features to this new model. Some of them new. Some of them reused and modified to fit this model. But all of them about simplifying and adding flexibility to the OO model.
-Kaja