12

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.

22
  • 4
    I like the base question, but I have a hard time with the details. E.g. why does inheritance stop to make sense when you max out immutability?
    – Martin Ba
    Commented Jun 2, 2017 at 10:15
  • 1
    Your objects don't have methods? Commented Jun 2, 2017 at 10:27
  • 6
    Possible duplicate of Complete immutability and Object Oriented Programming
    – gnat
    Commented Jun 2, 2017 at 10:56
  • 5
    "From the OOP point of view, a piece is responsible for generating valid moves from its position on the board." - definitely not, designing a piece like that would most probably hurt SRP.
    – Doc Brown
    Commented Jun 2, 2017 at 11:35
  • 1
    @lishaak You "struggle to explain the problem with inheritance in simple terms" because it's not a problem; poor design is a problem. Inheritance is the very essence of OOP, the sine qua non if you will. A lot of the so-called "problems with inheritance" are actually problems with languages not having an explicit override syntax, and they're a lot less problematic in better-designed languages. Without virtual methods and polymorphism, you don't have OOP; you have procedural programming with funny object syntax. So it's no wonder you're not seeing the benefits of OO if you're avoiding it! Commented Jun 2, 2017 at 11:52

2 Answers 2

28

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?

10
  • 6
    Everybody should read this. And then read On Understanding Data Abstraction, Revisited by William R. Cook. And then read this again. And then read Cook's Proposal for Simplified, Modern Definitions of "Object" and "Object Oriented". Throw in Alan Kay's "The big idea is 'messaging'", his definition of OO Commented Jun 2, 2017 at 15:42
  • 1
    And last but not least The Treaty of Orlando. Commented Jun 2, 2017 at 15:42
  • 1
    @Euphoric: I think that it is a fairly standard formulation that matches the standard definitions for "functional programming", "imperative programming", "logic programming", etc. Otherwise, C is a functional language because you can encode FP in it, a logic language, because you can encode logic programming in it, a dynamic language, because you can encode dynamic typing in it, an actor language, because you can encode an actor system in it, and so on. And Haskell is the world's greatest imperative language since you can actually name side-effects, store them in variables, pass them as … Commented Jun 2, 2017 at 19:22
  • 1
    I think we can use similar ideas to those in Matthias Felleisen's genius paper about language expressivity. Moving an OO program from, say, Java to C♯ can be done with only local transformations, but moving it to C requires a global restructuring (basically, you need to introduce a message dispatch mechanism and redirect all functions calls through it), therefore Java and C♯ can express OO and C can "only" encode it. Commented Jun 2, 2017 at 19:36
  • 1
    @lishaak Because you're specifying it to different things. This keeps it at one level of abstraction and prevents duplicating positional information storage that otherwise could become inconsistent. If you simply resent the extra typing then stick it in a method so you only type it once..A piece doesn't have to remember where it is if you tell it where it is. Now pieces only store information like color, moves, and image/abbreviation, that are always true and immutable. Commented Jun 3, 2017 at 12:32
2

The most useful concepts introduced into the mainstream by OOP, to my mind, are:

  • Proper modularization.
  • Data encapsulation.
  • Clear separation between private and public interfaces.
  • Clear mechanisms for code extensibility.

All these benefits can also be realized without the traditional implementation details, like inheritance or even classes. Alan Kay's original idea of an "object-oriented system" used "messages" instead of "methods", and was closer to Erlang than e.g. C++. Look at Go which does away with many traditional OOP implementation details, but still feels reasonably object-oriented.

If you use immutable objects, you still can use most of the traditional OOP goodies: interfaces, dynamic dispatch, encapsulation. You don't need setters, and often don't even need getters for simpler objects. You also can reap the benefits of immutability: you're always sure that an object did not change in the meantime, no defensive copying, no data races, and it's easy to make the methods pure.

Take a look how Scala tries to combine immutability and FP approaches with OOP. Admittedly it's not the most simple and elegant language. It's reasonably practically successful, though. Also, take a look at Kotlin that provides many tools and approaches for a similar mix.

The usual problem with trying a different approach than the language creators had in mind N years ago is the 'impedance mismatch' with the standard library. OTOH both Java and .NET ecosystems currently have reasonable standard-library support for immutable data structures; of course, third-party libraries also exist.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.