Usually though, if you try to make the code clean and as small as possible while still allowing unit tests to be written, you normally hit a decent spot. YAGNI applies very heavily here too and it's often better to make the code less generic and refactor it later when required.
Curiously, I'm going to at least partially credit Ruby for introducing the idea of convention over configuration.
I am not really a ruby guy, but I'm very thankful RoR came along and shook things up.
I think the problem is that OOP is automatically considered "virtuous", despite the context. There's nothing wrong with the single responsibility principle and what not, but there is something wrong with thinking that classes and objects are always the best way to approach a problem. Java ends up being the target of jokes because by forcing everything into classes/objects even when it doesn't make sense, it leads to cartooned versions of those ideas.
That's why TDD is absolutely fundamental. In theory TDD can help fight over-engineering, and keep focus on relevant code. SOLID and design patterns are then here to help refactor code when a spec changes. They are recipes.
Obviously TDD fizzbuzz done correctly would have resulted in a single class implemented. Now imagine requirements changes and booze should be printed if the number can be divided by 20,breeze if the number can be divided by 40,bubble when divisible by 56, the easiest way to refactor that single class would be to introduce a CHAIN OF RESPONSIBILITY. Which again is not necessary at first place, if the number of cases remains small.
In your example we can observe that we still only have one action: print $VARIABLE if $COUNTER mod $VALUE. This doesn't call for anything like a chain of responsibility, since it can be solved by a collection of tuples and a for loop.
My personal guess about the difference between an average programmer and a good one is that the good one knows when to add space for more features (because he can anticipate the customers needs).
DRY, SRP, LSP, OCP are very valuable. There's a reason there's a score of enterprise-grade software being written with exactly those principles in mind. I am not discounting Functional Programming in anyway. But to dismiss OOP and the design patterns is a fool's errand. It has its place, and rightly so.
Look at what the programming Lords (Ken Thompson! Rob Pike! Lars Bak!) at Google came up with when they sat down to design a language-- Go (which is part-OO, part-imperative [1]), or Dart (OO again). OO detractors often point out that "popular opinion isn't always right" and that "people are afraid of change" and there's some level of truth to that, but the fact is, a lot of developers out there are proficient with OO, and see it as a good way to develop massive programs (1 million plus SLOC) unless some radical shift happens in the coming years [2].
[1] https://golang.org/doc/faq#Is_Go_an_object-oriented_language
This is where the mess begins, because it becomes so cryptic to figure out how to use the FactoryFactory to create your own Factory to create some other types you need. It may be trivial to people who've been hacking with the same languages and frameworks for years, but the principle of least surprise is abandonded for new users attempting to use a framework for the first time.
Functional programming suffers from the same overuse of idioms which are unwelcoming to novices, and often throws away some useful ones in favor of ease of use. FP doesn't necessarily dismiss OOP, and it's often used in combination with functions to write practical programs.
Functional programmers don't see OOP as a problem - it's a very practical tool. The problem is that it's the wrong tool for many problems - particularly if you're just computing functions[1]. That a lot of developers are proficient with OOP and see it as a good way to develop programs is part of the cause - they're blind to alternative methodologies and shoehorning something into an object where it doesn't fit is seen as a being skillful. Also, anything that doesn't conform to the language's narrow view of "the right way" is considered an anti-pattern.
[1]:http://www.yegor256.com/2014/05/05/oop-alternative-to-utilit...
In the case of Spring though, they have a tough problem to solve. They are gluing technologies together, so they are always going to have factories of factories, adapter of adapter, and that kind of things. And if in the bunch of tech they glue there is an outlier that behaves unlike the other, or a poorly designed one, they will need to handle it somehow.
Another feature that weights on Spring design is that almost the whole framework is public. Generally you would have a core architecture and well defined key extension points. Not with Spring, and that means more "useless" abstraction and therefore more mess.
My solution has been to think of code locality as being valuable. I start off writing something the straightforward way. When I go to implement something similar, I decide whether to abstract the code or not based on how much easier/harder it will be to naively trace the code. If I've done this thing 3 or 4 times, it's a concern and should be separated. If I'm just making something shorter/"nicer"/DRYer I should probably just write stupid code.
My favorite example is the single responsibility principle, which, while a useful guideline, is 100% qualitative. All classes and functions do more than one thing.
Is making a sandwich one thing? Is putting mustard on the bread one thing? Is opening the mustard jar one thing?
The exception might be if you had a function call bitFliper that turned 0s to 1s and vice versa. But that's probably as close as you could get in most programming languages.