I think of it like a table where the rows are the data types, and the columns are the behaviours. (So e.g. rows are classes, columns are interfaces; or rows are algebraic data types, columns are functions which accept those types.)
An object-oriented language is organised by the "rows":
- For every behaviour associated with a class, the implementation of that behaviour is written within the class.
- Adding a new "row" is easy: you just write a new class and implement whatever interfaces it needs.
- Adding a new "column" is hard: if you add a method to an interface, or create a new interface, you have to edit every class all over the project.
A functional language is organised by the "columns":
- For every data type that a behaviour is implemented for, the implementation of that behaviour is written where the function is declared.
- Adding a new "column" is easy: you just write a new function and make it handle whatever data types it needs to.
- Adding a new "row" is hard: e.g. if you add a new variant to some enum type, you have to edit every function that consumes that enum type.
Of course this is just the broad idea, both OO and FP languages have design patterns which allow organising in the opposite way. For example, in an OO language the visitor pattern groups by the implementations of one behaviour for many classes; in an FP language, first-class functions can be included as fields in a data type so that these functions are implemented together where the type is constructed. It's just that these design patterns are only needed when you want to organise code in a way that isn't directly encouraged by the paradigm you're writing in.
So this is a fundamental problem in language design, and I don't think it can really be "solved", there are just different options with different trade-offs. Ultimately, if one module in a program can add a new "row" and another can add a new "column" to the big "table", then someone needs to decide who is responsible for filling in the "cell" where the new row and the new column overlap.
That is, when there is a new type and a new function, and the new type has to work with the new function, then the behaviour has to be defined somewhere.
All of that said, there are some approaches in existing languages which do address the problem, even if they don't outright solve it. I'll mention traits in Rust: in the analogy, types (structs and enums) are the "rows", and traits are the "columns".
- A trait can be implemented on multiple types, and a type can implement multiple traits. So you can add either a new row or a new column.
- An implementation for a trait can be written either where the type is declared, or where the trait is declared ─ or elsewhere, within reasonable limits. You aren't allowed to implement third-party traits for third-party types, because this would cause the code to break if one of those third parties ever decided to implement the same trait for the same type themselves.
- There's no free lunch: the "new row, new column" problem still requires an implementation to be written somewhere. But at least it never requires changing third-party code, unless both the new "row" and the new "column" are in third-party libraries.
On the other hand, this allows code to be organised neither by "row" nor by "column", at least within a project you can scatter trait implementations across many files, which can lead to a messy project structure. This isn't exactly a language design flaw, because it's the user's responsibility to organise their code in a logical way; and it's also attenuated by modern IDEs which can efficiently locate all implementations of a trait. Still, some people might consider it a drawback that the language allows users to make more of a mess if they don't organise their code well.