46

I found this quote in "The Joy of Clojure" on p. 32, but someone said the same thing to me over dinner last week and I've heard it other places as well:

[A] downside to object-oriented programming is the tight coupling between function and data.

I understand why unnecessary coupling is bad in an application. Also I'm comfortable saying that mutable state and inheritance should be avoided, even in Object-Oriented Programming. But I fail to see why sticking functions on classes is inherently bad.

I mean, adding a function to a class seems like tagging a mail in Gmail, or sticking a file in a folder. It's an organizational technique that helps you find it again. You pick some criteria, then put like things together. Before OOP, our programs were pretty much big bags of methods in files. I mean, you have to put functions somewhere. Why not organize them?

If this is a veiled attack on types, why don't they just say that restricting the type of input and output to a function is wrong? I'm not sure whether I could agree with that, but at least I'm familiar with arguments pro and con type safety. This sounds to me like a mostly separate concern.

Sure, sometimes people get it wrong and put functionality on the wrong class. But compared to other mistakes, this seems like a very minor inconvenience.

So, Clojure has namespaces. How is sticking a function on a class in OOP different from sticking a function in a namespace in Clojure and why is it so bad? Remember, functions in a class don't necessarily operate just on members of that class. Look at java.lang.StringBuilder - it operates on any reference type, or through auto-boxing, on any type at all.

P.S. This quote references a book which I have not read: Multiparadigm Programming in Leda: Timothy Budd, 1995.

GlenPeterson
  • 14,890
  • 6
  • 47
  • 75
  • 28
    I believe writer simply didn't understand OOP properly and just needed one more reason to say Java is bad and Clojure is good. /rant – Euphoric Sep 25 '13 at 13:15
  • 7
    Instance methods (unlike free functions or extension methods) can can't be added from other modules. This becomes more of a restriction when you consider interfaces which can only be implemented by the instance methods. You can't define an interface and a class in different modules and then use code from a third module to bind them together. A more flexible approach, like haskell's type classes should be able to do that. – CodesInChaos Sep 25 '13 at 13:22
  • 4
    @Euphoric I believe the writer *did* understand, but the Clojure community seems to like to make a straw man of OOP and burn it as an effigy for all the evils of programming before we had good garbage collection, lots of memory, fast processors, and lots of disk space. I wish they would quit beating on OOP and target the real causes: Von Neuman architecture, for example. – GlenPeterson Sep 25 '13 at 13:26
  • @GlenPeterson Because I originally wanted to write only a single sentence, and it kept growing – CodesInChaos Sep 25 '13 at 13:26
  • 5
    My impression is that most criticism of OOP is actually criticism of OOP as implemented in Java. Not because that's a deliberate straw man, but because it's what they associate with OOP. There are pretty similar issues with people complaining about static typing. Most of the issues aren't inherent in the concept, but just flaws in a popular implementation of that concept. – CodesInChaos Sep 25 '13 at 14:05
  • 3
    Your title does not match the body of your question. It is easy to explain why tight coupling of functions and data is bad, but your text asks the questions "Does OOP do this?", "If so, why?" and "Is this a bad thing?". So far, you have been lucky enough to receive answers which deal with one or more of these three questions and none assuming the simpler question in the title. – itsbruce Sep 25 '13 at 15:31
  • Coupled with the fact that inheritance is evil (you must contain rather than extend) and there is no advantage in replacing conditionals with polymorphism, we can conclude that OOP is meaningles from very beginning, http://valjok.blogspot.com/2013/01/java-oop-is-sux-from-very-beginning.html – Val Sep 25 '13 at 17:46
  • 1
    `Before OOP, our programs were pretty much big bags of methods in files. I mean, you have to put functions somewhere. Why not organize them?` - "Put related functions on the same object" is "OOP" in the same way C++ is "C with classes". From the outsider's perspective, okay, it might _look_ that way, but you're not doing OOP/C++ in a maintainable way if that's the criteria you go by. – Izkata Sep 25 '13 at 18:38
  • 1
    Who talks about this crap over dinner? It'll only lead to nerdy fisticuffs – James Sep 25 '13 at 19:00
  • Even if you organize your files full of methods just like you would with classes, there's more to OOP than just being able to organize and file the code. VB has Modules and Classes; don't confuse the two. – JeffO Sep 26 '13 at 03:01
  • 1
    Side note: You can (and should) use data structures in OOP if you want to decouple data and functions. Procedural code (code using data structures) makes it easy to add new functions without changing the existing data structures. OO code, on the other hand, makes it easy to add new classes without changing existing functions. Mature programmers know when to use one and when to use the other. Sometimes you really do want simple data structures with procedures operating on them. – Hbas Oct 02 '13 at 01:39
  • 1
    *downside to object-oriented programming is the tight coupling between function and data.* => THAT is effectively the definition of an object! So the downside of OO is that it is OO? – radarbob Jul 01 '22 at 22:55
  • Generically the bad aspect of coupling is unintended side-effects when the code is maintained making changes much less reliable. If not for this single bad effect coupling is not harmful. The whole idea of OOP is to encapsulate related data and functionality together as a single cohesive unit. In this case coupling is an aspect of cohesiveness thus a good thing. – polcott Jul 05 '22 at 23:16

6 Answers6

46

In theory, loose function-data coupling makes it easier to add more functions to work on the same data. The down side is it makes it more difficult to change the data structure itself, which is why in practice, well-designed functional code and well-designed OOP code have very similar levels of coupling.

Take a directed acyclic graph (DAG) as an example data structure. In functional programming, you still need some abstraction to avoid repeating yourself, so you're going to make a module with functions to add and delete nodes and edges, find nodes reachable from a given node, create a topological sorting, etc. Those functions are effectively tightly coupled to the data, even though the compiler doesn't enforce it. You can add a node the hard way, but why would you want to? Cohesiveness within one module prevents tight coupling throughout the system.

Conversely on the OOP side, any functions other than the basic DAG operations are going to be done in separate "view" classes, with the DAG object passed in as a parameter. It's just as easy to add as many views as you want that operate on the DAG data, creating the same level of function-data decoupling as you would find in the functional program. The compiler won't keep you from cramming everything into one class, but your colleagues will.

Changing programming paradigms doesn't change best practices of abstraction, cohesion, and coupling, it just changes which practices the compiler helps you enforce. In functional programming, when you want function-data coupling it's enforced by gentlemen's agreement rather than the compiler. In OOP, the model-view separation is enforced by gentlemen's agreement rather than the compiler.

Karl Bielefeldt
  • 146,727
  • 38
  • 279
  • 479
  • A template mechanism like C++, or a dynamic language, allows function (-templates) which stay the same even if the data-structure radically changes. The important part is having a reliable efficient interface. – Deduplicator Jul 08 '22 at 23:47
14

In case you didn't know it already take this insight: The concepts of object-oriented and closures are two sides of the same coin. That said, what is a closure? It takes variable(s) or data from surrounding scope and binds to it inside the function, or from an OO-perspective you effectively do the same thing when you, for example, pass something into a constructor so that later on you can use that piece of data in a member function of that instance. But taking things from surrounding scope is not a nice thing to do - the larger the surrounding scope, the more evil it is to do this (though pragmatically, some evil is often necessary to get work done). Use of global variables is taking this to the extreme, where functions in a program are using variables at program scope - really really evil. There are good descriptions elsewhere about why global variables are evil.

If you follow OO techniques you basically already accept that every module in your program will have a certain minimum level of evil. If you take a functional approach to programming, you are aiming for an ideal where no module in your program will contain closure evil, though you may still have some, but it will be a lot less than OO.

That's the downside of OO - it encourages this kind of evil, coupling of data to function through making closures standard (a kind of a broken window theory of programming).

The only plus side is that, if you knew you were going to use lots of closures to start with, OO at least provides you with an idealogical framework to help organise that approach so that the average programmer can understand it. In particular the variables being closed over are explicit in the constructor rather than just taken implicitly in a function closure. Functional programs that use lots of closures are often more cryptic than the equivalent OO program, though not necessarily less elegant :)

Benedict
  • 1,047
  • 5
  • 12
  • 9
    Quote of the day: "some evil is often necessary to get work done" – GlenPeterson Sep 25 '13 at 15:00
  • 8
    You haven't really explained *why* the things you call evil are evil; you're just calling them evil. Explain why they're evil, and you might have an answer to the gentleman's question. – Robert Harvey Sep 25 '13 at 15:36
  • 2
    Your last paragraph saves the answer though. It may be the only plus side, according to you, but that's no small thing. Us so called "average programmers" actually welcome a certain amount of ceremony, certainly enough to let us know what the hell is going on. – Robert Harvey Sep 25 '13 at 15:44
  • If OO and closures are synonymous, why have so many OO languages failed to provide explicit support for them? The C2 wiki page you cite has even more disputation (and less consensus) than is normal for that site. – itsbruce Sep 25 '13 at 16:12
  • I added a link to another question that talks about the evil of global state. I might have been exercising some poetic license when I said it was the 'only plus side'. I think you misunderstood what I meant by 'average programmer'. I mean a normal human being whose profession is a programmer - and though some competence level is expected, it is elitist to require that level to be that of a psychic. I agree with you - any paradigm or language that forces or encourages readable code in some way is worth celebrating. Myself, I'm not always convinced by the OO approach, but that's me. – Benedict Sep 25 '13 at 16:13
  • 2
    @itsbruce They're made largely unnecessary. The variables that would be "closed over" instead become class variables passed into the object. – Izkata Sep 25 '13 at 18:43
  • @Izkata Unnecessary, in a type-safe language, at the cost of large amounts of boiler-plate and endless subclassing (or lumpy and unstructured imperative logic). I wouldn't call that "largely unnecessary". It`s *possible*, but I wouldn't want to go there if I had a sane alternative. – itsbruce Sep 26 '13 at 09:11
  • @itsbruce See, "at the cost of large amounts of boiler-plate and endless subclassing (or lumpy and unstructured imperative logic)" sounds like OOP done wrong, like someone went crazy with buzzwords and what's _possible_ in OOP at the cost of maintainable code. Even in Java, the boilerplate isn't going to be large if done right (although it will be more than, say, Python). – Izkata Sep 26 '13 at 10:00
  • I want to qualify my earlier comment. I should have said, "Functional Programming Quote of the day - some evil is often necessary to get work done." I'm not advocating evil in general, just in the purely functional programming sense. – GlenPeterson Sep 26 '13 at 13:11
  • Actually, in Java, a lexical closure is sometimes the only simple way to ensure immutability - see my "Immutable Map" example here: http://glenpeterson.blogspot.com/2013/07/immutable-java-with-lists-and-other.html – GlenPeterson Sep 26 '13 at 13:29
7

It's about type coupling:

A function built into an object to work on that object can't be used on other types of objects.

In Haskell you write functions to work against type classes - so there are many different types of objects any given function can work against, so long as it's a type of the given class that function works on.

Free-standing functions allow such decoupling which you don't get when you focus on writing your functions to work inside of type A because then you can't use them if you don't have a type A instance, even though the function might otherwise be general enough to be used on a type B instance or type C instance.

Jimmy Hoffa
  • 16,039
  • 3
  • 69
  • 80
  • 3
    Isn't that the whole point of interfaces? To provide the things that allow type B and type C to look the same to your function, so it can operate on more than one type? – Random832 Sep 25 '13 at 22:11
  • 2
    @Random832 absolutely, but why embed a function inside of a data type if not to work with that data type? The answer: It's the only reason *to* embed a function in a data type. You could write nothing but static classes and make all your functions *not* care about the data type they're encapsulated in to make them completely decoupled from their owning type, but then why even bother putting them in a type? Functional approach says: Don't bother, write your functions to work towards interfaces of sorts, and then there's no reason to encapsulate them with your data. – Jimmy Hoffa Sep 25 '13 at 22:26
  • You still have to implement the interfaces. – Random832 Sep 26 '13 at 19:09
  • 2
    @Random832 the interfaces are data types; they need no functions encapsulated in them. With free functions, all the interfaces need to extoll is what data they make available for functions to work against. – Jimmy Hoffa Sep 26 '13 at 19:19
  • 2
    @Random832 to relate into real-world objects as is so common in OO, think of the interface of a book: It presents information(data), that's all. You have the free function of turn-page which works against the class of types that have pages, this function works against all sorts of books, news papers, those poster spindles at K-Mart, greeting cards, mail, anything stapled together in the corner. If you implemented the turn-page as a member of the book, you miss out on all the things you could use turn-page on, as a free function it's not bound; it just throws a PartyFoulException on beer. – Jimmy Hoffa Sep 27 '13 at 15:05
  • @JimmyHoffa I think I finally understood what Functional programming is all about. Thanks – Mahdi Jul 27 '16 at 14:12
4

In Java and similar incarnations of OOP, instance methods (unlike free functions or extension methods) can't be added from other modules.

This becomes more of a restriction when you consider interfaces which can only be implemented by the instance methods. You can't define an interface and a class in different modules and then use code from a third module to bind them together. A more flexible approach, like Haskell's type classes should be able to do that.

CodesInChaos
  • 5,697
  • 4
  • 19
  • 26
  • You can do that easily in Scala. I'm not familiar with Go, but AFAIK you can do it there also. In Ruby, it is quite common practice as well to add methods to objects after the fact to make them conform to some interface. What you describe seems rather like a badly designed type system than anything even remotely related to OO. Just as a thought experiment: how would your answer be different when talking about Abstract Data Types instead of Objects? I don't believe it would make any difference, which would prove that your argument is unrelated to OO. – Jörg W Mittag Sep 25 '13 at 13:48
  • 1
    @JörgWMittag I think you meant Algebraic datatypes. And CodesInChaos, Haskell very explicitly discourages what your suggesting. It's called an orphaned instance and issues warnings on GHC. – daniel gratzer Sep 25 '13 at 13:56
  • 4
    @JörgWMittag My impression is that many who criticize OOP criticize the form of OOP used in Java and similar languages with its rigid class structure and focus in instance methods. My impression of that quote is that it criticizes the focus on instance methods and doesn't really apply to other flavours of OOP, like what golang uses. – CodesInChaos Sep 25 '13 at 13:56
  • 2
    @CodesInChaos Then perhaps clarifying this as "static class based OO" – daniel gratzer Sep 25 '13 at 13:59
  • @jozefg: I'm talking about Abstract Data Types. I don't even see how Algebraic Data Types are remotely relevant to this discussion. – Jörg W Mittag Sep 25 '13 at 14:09
  • If you've got two different modules A and B that "bind together" a class C and an interface I, and pass the object to a function F that expects that interface, then how does F know whether to use A or B when operating on that C? The only way is to use a wrapper object - even if there aren't two modules, the only way for F to find the "binding" module is to pass it an object that knows about it (i.e. the wrapper object) – Random832 Sep 25 '13 at 22:13
3

Object Orientation is fundamentally about Procedural Data Abstraction (or Functional Data Abstraction if you take away side-effects which are an orthogonal issue). In a sense, Lambda Calculus is the oldest and most pure Object-Oriented language, since it only provides Functional Data Abstraction (because it doesn't have any constructs besides functions).

Only the operations of a single object can inspect that object's data representation. Not even other objects of the same type can do that. (This is the main difference between Object-Oriented Data Abstraction and Abstract Data Types: with ADTs, objects of the same type can inspect each other's data representation, only the representation of objects of other types is hidden.)

What this means is that several objects of the same type may have different data representations. Even the very same object may have different data representations at different times. (For example, in Scala, Maps and Sets switch between an array and a hash trie depending on the number of elements because for very small numbers linear search in an array is faster than logarithmic search in a search tree because of the very small constant factors.)

From the outside of an object, you shouldn't, you can't know its data representation. That's the opposite of tight coupling.

Jörg W Mittag
  • 101,921
  • 24
  • 218
  • 318
  • I have classes in OOP that switch internal data structures depending on the circumstances so object instances of these classes can be using very different data representations at the same time. Basic data hiding and encapsulation I'd say? So how is Map in Scala different from a properly implemented (wrt data hiding and encapsulation) Map class in an OOP language? – Marjan Venema Sep 25 '13 at 16:57
  • 1
    In your example, encapsulating your data with accessor functions in a class (and thus tightly coupling those functions to that data) actually allows you to loosely couple instances of that class with the rest of your program. You are refuting the central point of the quote - very nice! – GlenPeterson Sep 26 '13 at 13:26
1

Tight coupling between data and functions is bad because you want to be able to change each independently of the other and tight coupling makes this hard because you can't change one without knowledge of and possibly changes to, the other.

You want different data presented to the function to not require any changes in the function and similarly you want to be able to make changes to the function without needing any changes to the data it is operating on to support those function changes.

Michael Durrant
  • 13,101
  • 5
  • 34
  • 60
  • 2
    Yes, I want that. But my experience is that when you send data to a non-trivial function that it was not explicitly designed to handle, that function tends to break. I'm not just referring to type safety, but rather any data condition that was not anticipated by the author(s) of the function. If the function is old and often used, any change that allows new data to flow through it is likely to break it for some old form of data that still needs to work. While decoupling may be the ideal for functions vs. data, the reality of that decoupling can be difficult and dangerous. – GlenPeterson Sep 26 '13 at 13:08
  • 1
    I also want a pink unicorn. Loose coupling breaks too, and you still need knowledge of each other. You just need that knowledge _everywhere_, whereas in OOP that knowledge is at least confined to _one place._ – user949300 Jun 29 '22 at 22:27