Disclaimer: I’m the lead designer of the Corinna object-oriented system going into the Perl core . I’m probably a bit biased here.
It seems like a week can’t go by without another blog entry or video explaining why object-oriented programming (OOP) is bad . While the content of many of those articles is bad, if you read enough of them, some common themes emerge: “mutability” and “genericity.” Both are difficult to solve, so it’s worth explaining what’s wrong here. But first, what’s an object?
What’s an Object?
Let’s take an extended quote from my book Beginning Perl .
ÆVAR THE PERSONAL SHOPPER
You’re an awfully busy person and have little free time but plenty of disposable income, so you’ve decided to hire a personal shopper. His name is Ævar (any resemblance to reviewers of this book, living or dead, is purely coincidental) and he’s friendly, flamboyant, and most of all, cheap.
Because Ævar is new to both your city and the job, you have to tell him carefully how much money he can spend, exactly what quality of products you want, and where to buy them. You may even have to tell him which route to drive to pick up the goods and how to invoice you.
That, in essence, is procedural code and that’s what you’ve been doing up to now. You’ve been carefully telling the computer every step of the way what to do.
After a few months of explaining every little detail, Ævar gets upset and says, “þegiðu maður, ég veit alveg hvað ég er að gera” (Icelandic for “Shut up dude; I know what I’m doing”). And he does. He knows what you like and where to get it. He’s become an expert. In OO terms, you might now be doing this:
my $aevar = Shopper::Personal->new({ name => 'Ævar', budget => 100 }); $aevar->buy(@list_of_things_to_buy); my $invoice = $aevar->get_invoice;
You’re no longer telling Ævar every little step he needs to take to get your shopping done. He’s an expert, and he has all the knowledge needed to do your shopping for you and present you with the bill.
That’s all objects are. They are experts about a problem domain, but that’s actually a problem.
Generic Experts
This issue is extremely important, but it’s one that’s not touched on often enough.
OOP works best in constrained environments. For a company, if your developers really understand OOP (they often don’t), OOP is great because you can create custom experts for your domain. Your objects know how your custom authentication and authorization system work, so you can share this code with other developers in the company and they don’t have to rewrite it.
Until that one team has some custom authorization rules dealing with SOX compliance, GDPR, PCI, or a host of other things you’ve never heard of. They might ask you to make your objects more “generic” to allow them to handle their custom needs, but that starts to add complexity and, potentially bugs. If enough teams ask for this, your beautiful authorization object can become an unmaintainable god object. Systems grow. Needs grow. And software often suffers as a result.
This is fundamentally a problem with abstraction. Just because I develop a good abstraction for my use case doesn’t mean I’ve developed a good abstraction for yours.
In other words, for generic OOP, it can often be problematic because your solution may not be general enough to fit other people’s needs. For Perl, the LWP objects to handle Web communication tend to work very well because it’s a well-constrained problem space.
In contrast, my own HTML::TokeParser::Simple module is less generally useful because it fits a particular use case that doesn’t map well to many closely-related problem spaces, For example, when you need to map out the hierarchical structure of HTML, the stream of tokens my module produces aren’t well=suited to this. Thus, you may find yourself using a class that kinda works for what you want, but is ill-suited for what you need.
Another interesting problem is dealing with scale. For example, in a small company with maybe 20 to 30 programmers, a well-designed object can be a godsend. In a company with 200 to 300 programmers, a well-designed object can often be a source of pain because it doesn’t quite fit the needs of other teams. However, it’s often hard to make an object extensible in a way that fits the needs of others because it’s hard to plan for needs that you’re not aware of. If you’re lucky, your company might have a policy that all shared objects have a shared interface (possibly with default implementations) and different teams implement them to suit their unique needs.
If you’re unlucky, different teams all implement their own Customer
class
and if your code interacts with their code, there’s no guarantee of
interoperability. I speak from bitterly personal experience where another team
implemented a User
object that had only the two methods they needed, not the
many, many more that were generally available. A search through their
multi-million line codebase revealed many custom-made user objects, none of
which really worked like the others, but any of which could potentially leak
into other’s code (this is where static typing or type constraints really
shine, but that’s another article).
Objects for poorly-defined problem spaces don’t scale. If you might face this, define those problem spaces before the problem starts. Let people create their own custom objects, but with the minimum required common functionality.
Mutability
The big issue that is touched on, however, is a serious problem: mutable
state. You can send an object to many different areas of your code and then
some code far away from you mutates the state and suddenly you find that the
DateTime
object you were using has a different state from what you had
tested earlier.
As a general rule, sending an object somewhere is sending a reference, not a copy. Why is this bad? You can read Ricardo Signes' horror story about a bug dealing with mutable state in objects . When you’re building a complex system and you cannot depend on the state of your objects, you are in for a world of hurt when it bites you because it can be a nightmare figuring out what unexpectedly changed the state.
This is irrelevant in the face of immutable objects. Once you fully understand why changing an object’s state is bad, your programming becomes much more robust.
If you would like to know more about the issues with immutable objects, read my why do we want immutable objects? article.
As an aside, if you switch to a service-based architecture, you can’t send references, only copies. This makes the system more robust, but the trade=off is performance. This is why cloning is often shallow instead of deep. But, if you have immutable objects, sending a reference is irrelevant because you don’t risk mutating its state. Well, unless you’re using an ORM and something outside the program mutates that state. Speaking of which ...
Object-Relational Mappers
But then we come to the real problem: Object-Relational Mappers (ORMs). ORMs are almost by default mutable (there are immutable ORMs, but they’re not common).
my $customer = Acme::Model::Data::Customer->new($id);
$customer->name($new_name);
$customer->save;
This is real fun when separate parts of the code fetch the same object from the database and mutate it at the same time. Optimistic offline locking and similar strategies are your friend here.
Otherwise, you’re passing those around, it’s trivial for something to mutate the state and cause you grief. For Tau Station , we (I) learned this the hard way and now we often fetch data from the database and simply return a copy of the data structure. It’s faster and safer that way.
But regardless of the safety of protecting the underlying data, it still doesn’t alter the fact that not only is the state mutable, it’s stored outside the program. You can be as careful as you want, even creating read-only instances of ORM objects, but if two processes pick the same object and something changes the underlying state, you can still have incorrectly functioning software. ORMs make working with databases much easier, but there is a price to pay.
Teaching OOP?
Perhaps the biggest obstacle to effective OOP is, sadly, teaching it.
When I was learning Java in college, I still remember my first Java instructor, fresh out of college, struggling to explain the different between classes and instances. You could tell she knew the difference, but she had trouble articulating it in a way that students could understand. But that’s class-based OOP. Not all OOP systems are based around classes. Prototype-based OOP doesn’t have classes.
My second Java instructor had us build a Swing interface to read some records from a database and display them in a form. I immediately thought MVC and built model, view, and controller classes, along with a complete set of JUnit tests. My instructor rejected my code on the grounds of “I think this code might be good, but I don’t understand it. Please submit all of it as a single class.” I also had to not submit my JUnit tests.
Admittedly, my two Java instructors constitute an anecdote and not information you can actually act on, but I’ve seen this again and again why people try to explain what objects are.
- Objects are data with behavior attached
- Objects are smart structs
- Objects are user-defined data types
- Objects are polymorphic containers
- ... and so on
All of those miss the point. They’re talking about the implementation or structural details of objects and not the reason for objects. Why do we have cars? So we can drive from place to place. We can argue about the engines later.
The reason we have objects, as I noted above, is that object can be experts about a problem space. I can hand an object to a junior developer who knows nothing about the underlying complexity and that junior developer can use that underlying complexity. Pretty cool, huh?
Using objects is great, but building objects? Learning how to do this well involves a bewildering array of concepts such as SOLID , GRASP , the Liskov substitution principle , the Law of Demeter , not exposing implementation details (also known as the abstraction principle ), and so on. The last point, I might add, is why I have objected to lvalue attribute in Corinna (unless we can also have lvalue methods). You should be able to freely convert this:
field $public_key :reader :writer;
To this:
method public_key ($optional_key = undef) {
# fetch key from keystore
}
And your interface remains unchanged on the outside of the class, allowing you to keep your contractual promises with the class' consumers.
But many developers have asked for this:
$object->public_key = $public_key; # field
# versus this:
$object->public_key($public_key); # method
For the above, we’ve tightly coupled the public interface to the private implementation just to add a little syntactic sugar.
Object-oriented design is confusing enough that my friend Bob Gregory , coauthor of the book Architecture Patterns with Python: Enabling Test-Driven Development, Domain-Driven Design, and Event-Driven Microservices , shared an anecdote of how he taught better design to developers. He got fed up with seeing classes with names like
StartHtml
(which would build the start of an HTML document), that he mandated classes be named after creatures. He wound up with classes like theAuthenticationFairy
and theSessionGnome
, experts in their particular domains. Weird, but it appeared to work.
Since many OOP developers are self-taught, and with many OOP implementations being hard to learn, and the bewildering array of complex and sometimes contradictory “best practices” we have to learn, OOP seems to be a more advanced concept than many developers realize.
Conclusion
I think there’s a good case to be made that OOP is not nearly as useful as it’s claimed. I have worked with far too many clients whose OOP systems are, quite frankly, broken. I’ve spent a considerable about of time and energy (and thereby client money) fixing these broken systems. For one, someone had created a system whereby every time you created a new system component, you had to create a new class holding information about that component. No specialized behavior—just information. In fact, for each component, sometimes you had to add more than one new class—again, just for storing information (almost all of it strings). I proposed a different, data-driven system which, when done, would replace about 30 (and growing) classes with two (not growing).
In contrast, great OOP systems are a joy to work on and have made my life easier. I strongly recommend them, but they don’t seem terribly common.
In the research study “Productivity Analysis of Object-Oriented Software Developed in a Commercial Environment” (Potok, Vouk, Rindos, 1999), the researchers found that there is very little quantitative evidence that OOP improves productivity in commercial environments. On the plus side, there was also little evidence that it hinders productivity. And I know that when I hand a hideously complex authorization object to a new developer, so long as they stick to the interface, it magically works. I call that a win.
I love programming in OOP and (mostly) prefer it over procedural code (we’ll skip other paradigms for now). Further, a great many programmers seem to reach for it as a first solution rather than the last, so I’ll continue to work on the Corinna OOP system to make OOP easier for Perl, but I’m hard-pressed to combat criticisms against OOP itself. For now, just remember:
- Immutability is your friend
- Keep your interfaces small
- Well-constrained problem spaces are better for OOP
- Poorly-constrained problem spaces can be great for OOP, but they don’t scale
There’s a lot more than that, but those are great starters for decent OOP programming.
Until next time, happy hacking!