Tags
computer code, computer language, computer languages, computer programming, computer science, imperative programming language, language design, Object-Oriented Programming, OOP, programming language
This is another note for a friend: a followup to a discussion about how some programmers really hate Object-Oriented Programming (OOP) languages.
Most of those who hate OOP hold up Imperative Programming (IP) as the One True Way to write code. The key difference is the IP is function (or verb) oriented whereas OOP is object (or noun) oriented.
I’ve never really understood that active dislike. It’s just another way to organize the same code you’d write anyway.
I suspect some of that opprobrium is a reaction to those who touted OOP as the New Thing that would save computer programming (as if such a thing could actually exist). That crowd also needed to understand: It’s just another way to organize the same code you’d write anyway.
For instance, suppose you wanted to implement a simple 2D point for a graphics application. In an imperative programming language (IPL), you define a data type and some functions that manipulate it:
public record Point: int x, int y. public function Point Point_Move (Point p, int x, int y): // function definition public function int Point_Distance (Point p1, Point p2) // function definition
In an object-oriented programming language (OOPL), you define a class and some methods that manipulate the instance data:
public class Point: int x, int y, method Point move (int x, int y): // method definition, method int distance (Point p): // method definition.
One striking difference involves namespace. In the example, the IPL adds several entries to the namespace — the record and the functions that manipulate it. And because the functions are in the public global namespace, they need longer names to differentiate them from other similar functions.
The OOPL example just adds one name — the class. The method names are part of the class namespace. I’ve always felt that made things a bit simpler and more elegant. It’s one of several things that, at least for me, commends OOP.
On the other hand, for the IPL example, you can do something like this:
public record Point: int x, int y. public record Point_Methods: function Point move (Point, int, int), function int distance (Point, Point). private function Point Point_Move (Point p, int x, int y): // function definition private function int Point_Distance (Point p1, Point p2): // function definition Point_Methods Points = {Point_Move, Point_Distance}
Now the two examples are effectively the same. Notice how similar their usage examples are. First the IPL example:
Point p1, p2. Points.move(p1, 0,0). Points.move(p2, 10,10). print Points.distance(p1,p2).
And then the OOPL example:
Point p1, p2. p1.move(0,0). p2.move(10,10). print p2.distance(p1).
Very similar!
§
What’s really happening here is that, either during compilation or during run time, we need to bind data with code that manipulates it. OOP is just another way to accomplish this.
Let’s consider a simple expression we might find in code:
x = a + b
It doesn’t matter right now what ‘a’ or ‘b’ (or ‘x’) really is. What matters is that this expression requires two operations. First there is a ‘+’ operation that takes ‘a’ and ‘b’ as parameters. Then there is a ‘=’ operation that takes ‘x’ and the result of the first operation as parameters.
Here it also doesn’t really matter what the ‘+’ and ‘=’ operations are, but we’ll call them ‘plus’ and ‘set’ (which is an expected definition for those operators).
Once you get down to the lower levels of actual code execution, in an IPL context, we might end up with something like this:
t = add_<type-of(a)>_<type-of(b)> (a, b) x = set_<type-of(x)>_<type-of(t)> (x, t)
Whereas in an OOPL context it might look like this:
t = <type-of(a)>_add_<type-of(b)> (a, b) x = <type-of(x)>_set_<type-of(t)> (x, t)
Which is — again — pretty similar. The key difference is the emphasis placed on the target data object (‘a’ and ‘x’).
In both cases, we seek to dispatch the operation to code that can handle the data types. This may happen at compile time (in static languages) or at run time (in dynamic languages).
The difference is that in an IPL we start with the operation (function), but in an OOPL we start with the (first) data type.
Imagine that the expression shown above involves integer (int) data objects.
Then the IPL example resolves to:
t = add_int_int (a, b) x = set_int_int (x, t)
And the OOPL example resolves to:
t = int_add_int (a, b) x = int_set_int (x, t)
Importantly, the code in the ‘add’ and ‘set’ functions will be essentially the same. The first function adds two integers; the second assigns (sets) an integer value into an integer object.
§
Really the only difference is that IP is “verb-oriented” whereas OOP is “noun-oriented,” but both always contain both nouns and verbs.
Various IPLs might have constructs like:
set(x, add(a, b)) (set x (add a b))
While OOPLs might have constructs like:
x.set(a.add(b)) set: x add: a b
And it’s almost a matter of preference which you think is best. I happen to like both the OOPL constructs, but I’m fine with the IPS constructs, too. (I have a special fondness for the Lisp-like syntax.)
§
One aspect of OOP that gets criticism is the idea of inheritance and hierarchy.
A canonical example is a hierarchy of animals. The platypus is an egg-laying mammal (with a bill!), which seems to confuse people. They often conceptualize an animal hierarchy as having a sub-branch of, say, birds (who lay eggs and have bills and beaks), and how can you then have the platypus in the mammal branch?
To me, firstly, takes the hierarchy requirement way too seriously. The real world is obviously messy and not neatly comparted. So why would your hierarchy be so neat? Secondly, and more importantly, it reveals bad OOD (Object-Oriented Design).
Reptiles, for instance, also lay eggs.
This suggests that egg-laying is a trait potentially shared by any animal. One solution would be to include all potential animal traits at the highest level and use sub-classing to specialize.
A better solution is to implement an animal traits element capable of holding arbitrary animal traits.
But even this is hand-waving without knowing what the animal hierarchy is meant for, how it will be used. It may be that the application doesn’t care about eggs, and it’s all a moot point.
(But sometimes you “need the eggs.”)
§
On trait touted for OOP was “code reuse” which good programmers have always done when it comes to (a) sharing functional code within a project and (b) reusing previously written modular code.
That much isn’t really exclusive to OOP, although it can be syntactically cleaner. (Something I also like about OOP.)
The special OOP claim involves inheritance, where a sub-class has available the functionality of the base class(es). I think there’s something to this.
For example, I have a Python hierarchy for file-handling. It starts with a base class, fileobj, that gathers all the machinery for file names, dates, sizes, and type.
A subclass, filecontent, adds a layer of abstract content handling: file data access in the abstract.
The third layer branches out: datafile, textfile, linefile, tabfile, csvfile. These handle, respectively, binary files, unstructured text files, line-oriented text files, TAB-delimited files, and CSV-delimited files.
Basically I’ve just hidden the little file-handling basics I have to write over and over into easy to use (extendable if necessary) classes.
§
But it also illustrates one reason people disdain object hierarchies.
In the past I’ve, several times, had a go at a general data IO hierarchy that starts with an abstract notion of a data stream. That entails the concepts:
- Open
- Close
- Ready (to accept data)
- EOF (end of data)
- Error
(Not all data streams know their size, so length is not a basic concept.)
But you might see the problem already. The Ready concept applies to output streams, the EOF concept applies to input streams. One can use a Flag that mean either, depending, that confuses the semantic — the opposite of good design.
Alternately, we can move them to the next layer down, where we define the Input and Output abstractions. They seem appropriate at that level.
But what happens when we have a two-way stream? One that reads and writes?
Now we have to combine the Input and Output classes, forming the dreaded “Diamond Inheritance” pattern of multiple inheritance.
I never cared for either solution. Ultimately, a data-handling hierarchy just doesn’t easily divide into Input and Output.
§
So, as with the Platypus, it’s really more a matter of careful design.
And it really is a matter of preference, of course, but I’m sold on OOD. For whatever it’s worth, I learned to program a good decade before I learned OOD, so for me it’s definitely not a matter of “what I learned first.”
(My love of vi, however, definitely is.)
∅