Object-oriented – Can we really use immutability in OOP without losing all key OOP features

functional programmingimmutabilityobject-oriented

I see the benefits of making objects in my program immutable. When I am really deeply thinking about a good design for my application I often naturally arrive at many of my objects being immutable. It often comes to the point where I would like to have all my objects immutable.

This question deals with the same idea but no answer suggests what is a good approach to immutability and when to actually use it. Are there some good immutable design patterns? The general idea seems to be "make objects immutable unless you absolutely need them to change" which is useless in practice.

My experience is that immutability drives my code more and more to the functional paradigm and this progression always happens:

  1. I start needing persistent (in the functional sense) data structures like lists, maps etc.
  2. It is extremely inconvenient to work with cross references (e.g. tree node referencing its children while children referencing their parents) which makes me not use cross references at all, which again makes my data structures and code more functional.
  3. Inheritance stops to make any sense and I start to use composition instead.
  4. The whole basic ideas of OOP like encapsulation start to fall apart and my objects start to look like functions.

At this point I am practically using nothing from the OOP paradigm anymore and can just switch to a purely functional language. Thus my question: is there a consistent approach to good immutable OOP design or is it always the case that when you take the immutable idea to its fullest potential, you always end up programming in a functional language not needing anything from the OOP world anymore? Are there any good guidelines to decide which classes should be immutable and which should remain mutable to ensure that OOP does not fall apart?

Just for convenience, I will provide an example. Let's have a ChessBoard as an immutable collection of immutable chess pieces (extending abstract class Piece). From the OOP point of view, a piece is responsible for generating valid moves from its position on the board. But to generate the moves the piece needs a reference to its board while board needs to have reference to its pieces. Well, there are some tricks to create these immutable cross-references depending on your OOP language but they are pain to manage, better not have a piece to reference its board. But then the piece cannot generate its moves since it does no know the state of the board. Then the piece becomes just a data structure holding the piece type and its position. You can then use a polymorphic function to generate moves for all kinds of pieces. This is perfectly achievable in functional programming but almost impossible in OOP without runtime type checks and other bad OOP practices… Then, a move is just a function which makes a new board from an old board, again a functional idea having perfect sense but having nothing to do with OOP anymore.

Best Answer

Can we really use immutability in OOP without losing all key OOP features?

Don't see why not. Been doing it for years before Java 8 got all functional anyway. Ever heard of Strings? Nice and immutable since the start.

  1. I start needing persistent (in the functional sense) data structures like lists, maps etc.

Been needing those all along as well. Invalidating my iterators because you mutated the collection while I was reading it is just rude.

  1. It is extremely inconvenient to work with cross references (e.g. tree node referencing its children while children referencing their parents) which makes me not use cross references at all, which again makes my data structures and code more functional.

Circular references are a special kind of hell. Immutability wont save you from it.

  1. Inheritance stops to make any sense and I start to use composition instead.

Well here I'm with you but don't see what it has to do with immutability. The reason I like composition isn't because I love the dynamic strategy pattern, it's because it lets me change my level of abstraction.

  1. The whole basic ideas of OOP like encapsulation start to fall apart and my objects start to look like functions.

I shudder to think what your idea of "OOP like encapsulation" is. If it involves getters and setters then just please stop calling that encapsulation because it's not. It never was. It's manual Aspect Oriented Programming. A chance to validate and a place to put a breakpoint is nice but it's not encapsulation. Encapsulation is preserving my right to not know or care what's going on inside.

Your objects are supposed to look like functions. They're bags of functions. They're bags of functions that move together and redefine themselves together.

Functional programming is en vogue at the moment and people are shedding some misconceptions about OOP. Don't let that confuse you into believing this is the end of OOP. Functional and OOP can live together quite nicely.

  • Functional programming is being formal about assignments.

  • OOP is being formal about function pointers.

Really that it. Dykstra told us goto was harmful so we got formal about it and created structured programming. Just like that, these two paradigms are about finding ways to get things done while avoiding the pitfalls that come from doing these troublesome things casually.

Let me show you something:

fn(x)

That is a function. It's actually a continuum of functions:

f1(x)
f2(x)
...
fn(x)

Guess how we express that in OOP languages?

n.f(x)

That little n there chooses what implementation of f is used AND it decides what some of the constants used in that function are (which frankly means the same thing). For example:

f1(x) = x + 1
f2(x) = x + 2

That is the same thing closures provide. Where closures refer to their enclosing scope, object methods refer to their instance state. Objects can do closures one better. A closure is a single function returned from another function. A constructor returns a reference to a whole bag of functions:

g1(x) = x2 + 1
g2(x) = x2 + 2

Yep you guessed it:

n.g(x)

f and g are functions that change together and move around together. So we stick them in the same bag. This is what an object really is. Holding n constant (immutable) just means it's easier to predict what these will do when you call them.

Now that's just the structure. The way I think about OOP is a bunch of little things that talk to other little things. Hopefully to a small select group of little things. When I code I imagine myself AS the object. I look at things from the point of view of the object. And I try to be lazy so I don't over work the object. I take in simple messages, work on them a little, and send out simple messages to only my best friends. When I'm done with that object I hop into another one and look at things from it's perspective.

Class Responsibility Cards were the first to teach me to think that way. Man I was confused about them back then but damn if they aren't still relevant today.

Let's have a ChessBoard as an immutable collection of immutable chess pieces (extending abstract class Piece). From the OOP point of view, a piece is responsible for generating valid moves from its position on the board. But to generate the moves the piece needs a reference to its board while board needs to have reference to its pieces.

Arg! Again with the needless circular references.

How about: A ChessBoardDataStructure turns x y cords into piece references. Those pieces have a method that takes x, y and a particular ChessBoardDataStructure and turns it into a collection of brand spanking new ChessBoardDataStructures. Then shoves that into something that can pick the best move. Now ChessBoardDataStructure can be immutable and so can the pieces. In this way you only ever have one white pawn in memory. There are just several references to it in just the right x y locations. Object oriented, functional, and immutable.

Wait, didn't we talk about chess already?