I rarely ever use classes anymore. My life is complicated enough, I like my code to be simpler. I used to be proud of my complex classes hierarchy and clever designs.. now simple objects and mostly functions. I don't and won't argue with anyone who prefer to use class hierarchies, but it really annoys me when I need to spend 5mins wrapping my mind around all those class relationships where a simple imperative (or functional) code would have done the job perfectly.
Most of the big design philosophies are around "building for the future", but the truth is that we're almost always wrong about it. And thus, most of time, the code needs to be rewritten. And a simpler, straightforward code is much more easier to rewrite or refactor.
I also went through the stages of using classes with inheritance, and then using plain functions on untyped Plain-old-Data objects.
There's a third stage, though: using classes without inheritance, but with interface/protocol declarations. This is made much easier in the latest generation of languages (Go, Rust, Elixir) that infer the interfaces/protocols which a given piece of data supports, allowing you to use interfaces/protocols without classes.
I rarely ever use functions anymore. My life is complicated enough, I like my code to be simpler. I used to be proud of my numerous functions and clever designs.. now simple objects and mostly classes. I don't and won't argue with anyone who prefer to use plain functions, but it really annoys me when I need to spend 5mins wrapping my mind around all those functional relationships where a simple OOP (or imperative) code would have done the job perfectly.
It sounds like you're trying too hard to reason about functions as if they were, well, objects. You don't need to wrap your mind around "functional relationships" because functions don't have relationships. They just take arguments and return values (which can be other functions). Functional programming is not more complicated, if anything it's less complicated. It's just different, so it requires un-learning a lot of the things you learned about procedural and object-oriented programming. And if you have a good compiler/typechecker it will do the job with much less potential for bugs.
I have a hard time thinking of a situation where proper objects-and-functions code becomes easier to understand by removing the functions. Do you have any examples ?
Couldn't have said it better. Classes are useful, but only occasionally so. About the only time I find myself using classes is when there's several functions that have a very strong relationship to a particular portion of state (like when using an ORM). The rest of the time it's referentially transparent functions.
This is highly dependent on your language. A language where functions can be passed as values allows functions to be injected as parameters (for polymorphic behavior). In Clojure, you can even replace a function definition just for unit tests without dependency injection at all.
For example, with-redefs. This allows you to replace a given function anywhere in the whole codebase with any lambda. This rebinding only lasts inside the scope of the s-expression. When I realized that, I realized everything I knew about DI and interfaces were just hacks of incredible complexity to get around a poor language that wasn't built to allow unit testing. I now think it's impossible to have both unit testing and a good design in Java/C#. Neither were made to be easy to unit test, it wasn't even a concept when Java was made.
In C#'s defense, there is a way to mock a static function by replacing it using the Mocks library. To MS's shame, that library requires the 13k edition of Visual Studio. When I realized that, I realized I want to leave C# for good. I'm not interested anymore in a language that resists testing so much I have to choose between an easy to understand design and unit tests.
Hm, that may be true for Clojure (or Python, as long as you don't redefine anything important), but I wouldn't how to do that with Haskell, for instance.
It's true that you can't replace arbitrary pieces with no source changes in Haskell. On the other hand, there are a few approaches to making this easier - implicit parameters seems made for this kind of thing, "foo = fooBy whatever" as a pattern leaves it easy to swap in an alternative; and of course keeping functions pure it becomes less necessary to swap out internal bits because they don't matter.
Maybe I'm not interested in testing what the function does? But yes, for pure functions, it's kind of a weak argument. More generally, you can't really swap a different implementation in place.
when you require unit tests to finish quickly for functions doing io or otherwise time consuming processes. this being a good idea or not I'll leave to the threads about dhh's tdd article
I would argue that functions have no business performing global side-effects in the first place --- these things are better expressed as state encapsulators (such as objects) anyway.
Most of the big design philosophies are around "building for the future", but the truth is that we're almost always wrong about it. And thus, most of time, the code needs to be rewritten. And a simpler, straightforward code is much more easier to rewrite or refactor.