ArticlesEngineering

Objections

Object-oriented programming is not beneficial.

Object-oriented programming (OOP*) is not beneficial. This way of writing software often hurts more than helps.

*The mainstream manifestations of OOP as in C++ and Java. I have less distaste for the message-passing, Smalltalk-style implementations.

Object-oriented programming is hard to understand. To a programmer who has dealt with objects over most of their career, objects can seem easy. However, there is a very large vocabulary of knowledge that we have internalized to be effective. For example, the rules of prototypal inheritance and this are a lot to learn. The warts of multiple inheritance and obscurities like covariance and contravariance are lost even on senior developers. At scale, object systems become so hard to bear that people turn to code generators to get around object-oriented shortcomings in languages where OOP is a forced paradigm.

Even when you feel comfortable with OOP, the constructs are very often footguns to a good design. There is a buffet of patterns and options to building objects. While flexibility in solving a problem is highly desirable, objects can easily go wrong. Internal state, getter/setter noise, responsibility mismatch, God objects, and flaky hierarchies are some examples. Starting with an object oriented approach is unnecessary. The most important thing about design is making few decisions to allow code to adapt to a changing set of requirements and additions. OOP segments both methods and data across “classes” and these boundaries are painful to work around as a design grows.

It is also important to remember the job at hand. Programmers transform data and read and write to I/O. (You can model human interaction as a very inefficient I/O device as well.) Object oriented design is not a prerequisite to meeting our goals. Over time, we have seen technology shift towards data processing, short transactions, and less stateful code. The idea of a program receiving input (potentially unstructured depending on wire protocol), hydrating that data into class instances, manipulating the instances and performing side-effects, serializing the results for the wire protocol, and then sending output is overly complicated. Service boundaries are important, but we do not need to buy into OOP to achieve those.

I advocate functional programming. The learning curve to functions can be just as high as objects if someone is interested in set or category theory. However as the ideas get harder, the pitfalls are mostly in the readability of the code. Most code is very simple and more complex solutions are a-la-carte. In OOP, you start in the world of pitfalls day one. Over the function-as-value hurtle, we can get things done and grow as we need. We pick the battles we fight.

Functions do not shoehorn a design in the same way picking which methods and values pack into which class does. We can create tons of functions that operate on data which are not coupled to any particular object. We can be much more granular about code reuse beyond the limited public/private modifiers on methods as the functions do not need to coexist with any particular object. This decoupling makes functions more reusable and the creation and deletion of functions carry much less initial cost and long-term overhead.

For the job at hand, working in functions is the most straightforward way to transform data and encapsulate I/O via message passing and dispatch. The use of functions is close to the real task. We see this as more systems are being written with input/output contracts and determinism in mind. As far as service boundaries, functional specifications have proven to be more powerful than object hydration and serialization. Generative and property-based testing where the tests write themselves are at the forefront of functional development.

JavaScript is a unique language in that you can do whatever you want, mostly. There is no forced object-oriented and functional requirement. Especially looking at ES6+ where class sugar has made prototypal inheritance bearable, it may seem either is fine. However, I believe the real work is solved on the functional programming side. Functional programming enables systems to be more understandable, reusable, decoupled, and testable.

We cannot avoid objects in JavaScript. They are part of the language and most of the standard library provided by JavaScript is methods on objects. For example, Array.prototype.map is most naturally called on the array itself. There are libraries like Lodash that syntactically decouple the values from their methods. However, method chaining is commonplace and Lodash supports that as well. At best we can strive to minimize our usage of objects in JavaScript.

Method chaining is the defacto way to compose transformations and build domain specific languages (DSLs) in JavaScript libraries. The proposed pipeline operator should match some of chaining syntax’s elegance while bringing the benefits of standardized data composition which ultimately means better testing and more usable functions.