I can't tell you how many times I've seen a project go full onion-pattern with a web service that basically just recieves HTTP requests and makes a few itself and returns some data. Instead of a HTTP request handler that fetches data, perhaps does some mutation and returns it, there's a whole "service layer", a domain model, data fetching abstraction. To change something you have to navigate the winding dependency graph and fight off all the deamons. That's not the pit of success, they're the catacombs of failure.
We should design codebases so that future developers fall into the pit of success, even if it means we haven't been able to show off our knowledge of software architecture etc. The best pattern you can ever apply, is the one that makes change easiest. If you make change hard, the code becomes artificially stale -- I call it 'calcified'. Coupling makes code hard to change.
Thinking about problems through objects, in my experience, encourages people to make 'one model to rule them all', for example in a retail app, you might have a "Product" which ends up having product information, media assets, pricing, promotions, retail and stock keeping information etc which in a peice of software undergoing 'rapid development', multiplies the likelihood of coupling. Which is where Domain Driven Design steps in and tries to preach. Thinking about your problem in terms of data, or events can often help you find the right concepts to create your abstractions around. Another good heuristic is to think about caching, can you cache pricing for the same length of time you can cache product information, for example?
Bit of a ramble, but explains why I tend to shy away from objects, except for where I have a bonafide business problem that needs to be modelled -- with this, the models are only used to make the code make sense to developers and help arrive at the result they produce, rather than the models being the result themselves.
> Software can be considered "general" if it can be used, without change, in a variety of situations. Software can be considered "flexible", if it is easily changed to be used in a variety of situations.
We frequently forget that these are both valid paths to the same end. We tend to laser-focus in on generality, at the expense of flexibility.
And for small problems like the one described, it is usually much easier to go for flexibility at the expense of generality. And I think we should.
Learning it starts to train you to look for these opportunities, and eventually you start thinking "what if this class becomes a foundational class? I want everyone working on it to have a great time. I don't like being cursed out for not predicting the obvious future", and so now there's a bunch of onion-y stuff added.
A reasonable way to OOP is: 1. Understand your domain and work hard to match its structures. 2. Delegate and separate where there's an unquestionable benefit, not for the sake of it. 3. Likewise for inheritance hierarchies. 4. Don't create entities unnecessarily.
Every single decision should have a clear practical rationale. SOLID on its own is not a rationale - it's a guide, but to use it you need to understand what a concern is from the domain POV, not from the code POV.