Better Without Pure OOP

How software is better without OOP

In the last article the claim was that OOP as a mental model over-complicates reasoning about the technical challenges that have to be faced in programming and software architecture. Object oriented software projects on enterprise level, that had some time to ripen, often end up with an uncanny resemblance to a Rube Goldberg machine, not unlike the one shown in the video below:

As a quick reminder here are the core values that we need to follow in order to maintain a high quality in how we do software:

Core values

  1. Expressive problem solving
  2. Simplicity of abstractions
  3. Easy reasoning about code

Object oriented programming and software design doesn’t necessarily help with any of those core values. In fact it tends to seduce the practitioner to get into a mindset that over-complicates things. So the before mentioned 3 values can actually be compressed into one:

Domain-Driven Design

In his 2003 book “Domain-driven design” Eric Evans makes the point, that in order to keep a large object-oriented system maintainable (keep them easy to extend and fix) they should be designed paying attention to the following categories of objects/classes; all objects/classes should fall in one of them:

Domain driven design categories 2

This is not what is taught in the traditional school of object-oriented design and programming. Not by any stretch of the imagination. In pure OOP, every kind of object/class comes with it’s own set of behavior. This is how traditional encapsulated demands it.

Eric Evans conclusions are definitely based on real-world problems and a lot of experience in solving them. Some people call this architectural model “anaemic”. I disagree with this. Often it’s just the right kind of restrictions that make handling things so much better and simpler in the long run.

Test-induced damage

David Heinemeier Hansson not long ago made a point about the fact that making object-oriented systems testable usually requires changes to the architectures so severe, that the design actually becomes worse. Making object-oriented code testable makes it a lot more complex, not simpler, and therefore raises the costs of maintenance and extension. As a matter of fact that is true, but there’s a way out, if we are open-minded enough to question some of the dogmas in object-oriented software-design and programming.

Lets start with some ground-work.

First principles

In order to see OOP clearer and extract the good parts, let’s revisit it’s corner stones with the goal to extract 1st principles from them:

Classes and objects/instances

I mentioned Alan Kay in the previous articles. He invented object-orientation in order to be able to compartmentalize partial problems into (more or less) independent “machines”, that resemble cells of living organisms. He called them objects and they where supposed to work in conjunction. The first principle to extract from this is a “method to structure and order program code”. Which is especially important in the area of types of data a program is working on.

Encapsulation

Encapsulation is the process of hiding away details of a partial problem solution. The most important aspect of this is, that the interface behind which the details are hidden, must be considerably easier to reason about. If the result of an encapsulation is harder to reason about than the solution itself, this particular implementation must be considered a failure and should be removed or replaced. So encapsulation actually is the metaphorical task of putting a problem into a box, put the smallest possible number of levers, buttons and gauges on it and give it a name. Thus viewed encapsulation is perfectly possible without the use of classes and inheritance and therefore without the risk of over-complication.

Composition, inheritance and delegation

Fortunately inheritance is considered mostly harmful nowadays. It took the enterprise software community almost 30 years to admit to that and acknowledge the conceptional superiority of composition and delegation. Something that was already clear to the functional programming community in the 1970th.

Local State

A large fraction of the flaws in software development are due to programmer not fully understanding all the possible states their code may execute in. John Carmack

The severity of how problematic state-management actually is, is heavily downplayed by the object oriented community and mostly neglected. To help with understanding that, lets have a closer look at the act of programming itself. Especially object oriented programming. The programmer picks a partial problem and decides to solve it by encapsulating it into a set of variables (data) and methods (transformations). By grouping them together and naming the group, he/she creates an object that handles the problem. The sum of all the variables in an object is referred to as it’s “local state”.

Let’s look at this from an exaggerated perspective to understand what happens in a worst case scenario: Each of this variables expresses a certain condition and all of the conditions together are making up the sum of the complexity of this objects. Let’s assume an object has 3 variables where each variable can be in 3 different conditions each. All together the object can be in 27 different states then. With an object that has 5 variables where each variable can be in 5 different states, we already get exponential 3125 states.

Conscious State-Management

If a programmer is not conscious about the state he implements, his classes and objects can very fast degrade into exponentially raising complexity, where several thousand conditions need to be cared for. And this poses a problem. Exceedingly the programmers job is once more to properly see and understand the causes of complexity in order to make program behavior predictable and be able to consciously procure robustness.

In fact most of the errors occurring in maintaining and extending software are caused by a lack of understanding of local state as John Carmack points out in the above mentioned quote. If an encapsulation is parametrized in an unexpected way, this can put the program in a state that was not considered when it was programmed. If an operating system’s programming library doesn’t come with robust object-local state in the built-in objects, this will inevitably raise the probability of errors in the applications based on top of it. The metaphorical building on sand …

Taming local state

State is the central problem in software architecture and programming. A truth that is heavily downplayed and neglected by the object oriented community. Which leads to a situation where the value of existing wisdom cannot be assessed adequately.

Due to the fact that software is about data and transformations over it, state is everywhere and can only be mitigated. Functional programming has a set of intriguing solutions all together with an adequate language to talk about this class of problems:

Side effects and purity

Instead of attempting to handle the exponential complexity of state by encapsulating problems within more state (OOP), functional programming takes a completely different road:

Functional programming identified that state is only problematic if it is mutable; this means: if variables can be changed. A set of constants poses no risk of raising error-probability. A set of variables does. So the question his: How can we program to be able to treat our variables as constants? Simply by doing as much as possible with functions only. A free-floating function that is not connected to an object and does not mutate variables outside of itself has very interesting characteristics:

This class of functions is called pure. The act of combining pure functions is called function-composition.

Pure functions:

  1. … are an expressive tool to solve problems
  2. … lead to simple abstractions
  3. … are extremely easy to reason about

If a function does more than transforming a given input into an output, it’s not considered pure anymore. A setter-method on an object for instance is not pure, because it changes the state of the object. Such a function is considered as having one or more side-effects. When called a second time with the same arguments, the outcome of this call might be completely different. This kind of code is therefore less predictable than a pure function. Code not written as pure functions poses the risk of raising error-probability.

Side-effects are always the outcome

Computing is all about data and the transformation of data. One could express this as a stream of transformations that can be expressed perfectly in a functional paradigm. But in order to actually do something with the results, side-effects are absolutely necessary. If the result of a computation is displayed to a screen, send over the network or written to a hard drive, this is always a side-effect.

This fact can easily be misunderstood as a weakness of functional programming. In fact the object oriented community tapped into this trap: If side-effects are the most important thing, let’s program everything as side-effect. Programming with setters and getters is exactly this: Programming by side-effects.

But are side-effects really the most important thing? I’d argue: Most definitely not! The most important aspect in programming is to keep the major part of the code:

  1. Expressive in the way it solves problems
  2. Simple in it’s abstractions
  3. As easy to reason about as possible

If this is understood, then the benefits of functional programming can be cashed in on.

Functionally pure reasoning

Though side-effects are always standing at the end (and often at the beginning) of computational transformations, the complete stretch in-between (usually the major part of the code) can actually be expressed in a purely functional way. If done so, the resulting code checks a mark on each of our core values.

Programmers can handle pure functions with complete ease of mind. A program that is implemented mostly by pure functions, has a high level of maintainability: Each partial-solution will be a pure function. When reasoning about this program, each of it’s parts can be considered completely decoupled from another and it’s save to compose and change. Seen this way expressing a problem solution in a functional way actually poses a better form of encapsulation, than the OOP-way.

The activity of expressing as much of a program as possible as a composition of pure functions and use side-effects only when absolutely necessary, can therefore be understood as the superior way of taming local state. It means complete awareness over all the situations in which state is handled and concise management of it, instead of accidental correctness (this it what usually happens when programming OOP under a tight schedule).

Only if local state is tamed, system behavior can be designed concisely and under controlled conditions. Pure functions are the superior tool to do that.

First class functions

Another building-block of simplification, that affects software-design and -architecture are so called “first class functions”. What that means is simply that functions can be handled like variables. Most of the programming languages today do support 1st class functions (sometimes called lambdas or delegates). See the following list of OOP design patterns that can be replaced with 1st class functions:

An implementation using 1st class functions usually reduces complexity considerably compared to this OOP design patterns. Universities that teach functional concepts before OOP see that students solve the class of problems (solved by OOP design patterns) almost automatically with 1st class functions. Usage of first class functions for this problems can be so apparent, that no prior knowledge of OOP design patterns is necessary.

Classifying Domain-Driven Design

As we can see being aware of the wisdom of functional programming helps with getting better in pursuing the core values that make programs better. They help us in reasoning, to keep abstractions simple and more expressive. Now lets revisit Eric Evans’ Domain-driven design from the perspective of data and transformations upon it:

Domain driven design categories 2

Entities and value objects represent the classes of data a system operates upon. Entities represent the business domain the system is built for. Value objects are carrier objects, to transport data between services. Services represent the transformations that are applied to entities and value objects. Services are therefore the extracted behavior, that should actually be part of the domain objects themselves if we stick to the traditional school of object oriented analysis and design.

Functional design categories

If we look closer, we recognize that Eric Evans arrived at the same point like the practitioners of functional programming. By pure necessity caused by real world problems and experience.

Revisiting test-induced damage

As a matter of fact there is no coding-construct in software that is better testable than pure functions. There is nothing to be added or to be made more complex, nor is there anything that ca be removed. Pure functions and test-driven-development fot together in a perfect way. Combined with 1st-class functions, all problems concerning testing and dependency injections “promptly vanish in a puff of logic”, as Douglas Adams would state it.