[In this reprinted #altdevblogaday-opinion piece, CCP's lead technical artist Rob Galanakis examines several of the problems afflicting canonical object oriented programming, and poses possible solutions for what we can do about them.]
It has been commonplace over the past few years to bash Object Oriented Programming. Functional programming going mainstream. Data oriented design becoming commonplace for performance. The resurgence of dynamic languages. OO bastions going multi-paradigm. Why is everything going wrong for traditional OOP?
Because it took a while but we realized that canonical OOP sucks. Let's look at .NET's humble System.Diagnostics.Process class.
- The sin of statefulness. ProcessStartInfo, the mutable type that represents the filename, args, std io, and other state of the Process, has 20 mutable (get/set) properties. The Process type itself has over 50 properties (mostly immutable). The problem here is that the Process itself transitions between three states- not started, running, and finished- and only a subset of properties are valid at any given time. This whole situation is impossible to reason about- you either need to look at the extensive tests that would need to be written to test all the combinations of state, or you'd need to look at it under the debugger to know what's going on.
- Inheritance. This situation is bad enough. But have you ever seen someone subclass Process? I have, a few times, and it makes things even more impossible to reason about. You presumably subclass it to ensure certain state is set up by default, such as Filename. What if someone mutates that default, though? You either allow it, which makes your class sort of pointless and breaks its invariant (Filename won't change), or you don't allow it by raising an Exception, or even worse, just silently returning, which would break the fundamental contract of your base class and the Liskov Substitution Principle (you are quite clearly changing the behavior if you are raising an exception or not fulfilling the contracts the base class makes). There's no point to inherit stateful objects like this, but that is canonical OOP.
- Code reuse through inheritance/polymorphism. Obviously code reuse is a good thing. The problem is the way OOP encourages it, through polymorphism via inheritance. Process does not implement any interfaces. You could not pass Process to a method or class that, say, is responsible for managing IO and std streams in general, not just for Process. Actually, this isn't a big problem- just either wrap the Process in something (don't subclass it!), or pass in only the actual data/methods needed. The ease of getting around this quite clearly demonstrates that, if you were to take away inheritance, it really wouldn't be such a big deal- would it?
- Messy contracts and abstractions. What are the contracts on Process? Good luck trying to figure them out by reading the documentation (which is extensive). I think everyone has put an asynchronous process into a deadlock, even when following MSFT's directions. Understanding how to use Process still requires a pretty thorough understanding of the underlying system, and it ends up in a no-man's land between simplicity and power. These messy (not just leaky) abstractions are the major problem when consuming other people's code- I can't count how many 3rd party modules I've seen crashes or problems in, if they have a reasonable enough API to figure out in the first place.