Patterns should be used only where they can be the best solution or help in the creation of a good solution (do you agree?).
I see design patterns strictly as implementation details. If you document your public APIs and program to that documentation, in general it won't matter (or affect you much) where you have design patterns. That is, you don't go "I have a bridge pattern here, and I will implement a visitor on top of it". Instead, it is "this class will have different implementations on various operating systems so it will be implemented using a bridge pattern". Then, when you use it, you are indifferent to it being implemented as a bridge - because you look at the public API, not a bridge pattern.
how much effort should one actually invest in creating loosely coupled, flexible designs?
Loose coupling can be achieved by following a simple set of rules. If you respect these, your code will be (more) loosely coupled, as you write it (i.e. any effort is already part of the development process).
Among the rules (not an exhaustive list):
- define your interfaces by thinking (or writing) client code (how the class will be used), not what the class will do (i.e. desing for interface, not implementation)
- "tell, don't ask"
- construct objects from already created parts
- pass into the constructor the actual objects you will use (not factories for the members, parameters for the factories of the parameters, or anything like that).
- DRY (if you have two lines that appear in the same order in two places, extract them into a separate function and so on).
- If the creation of an object is a more complex operation, implement the creation of the intermediary parts as a factory method/class (i.e. not in the constructor body).
- YAGNI (create things as you need them, not before).
These rules are followed differently, depending on language, development methodology followed by your team (e.g. TDD), time budget constraints and so on.
For example, in Java, it is good practice to define your interface as an interface
and write client code on that (then, instantiate the interface with an implementation class).
In C++ on the other hand, you do not have interfaces, so you could only write the interface as an abstract base class; Since in C++ you only use inheritance when you have a strong requirement for it (and as such avoid the overhead of unnecessary virtual functions), you will probably not define the interface separately, just the class header).
Those who oppose design patterns say that the costs to using these patterns often outweighs the benefits.
I think they're doing it wrong. If you write loosely coupled (and DRY) code, integrating design patterns into it comes with minimal extra effort. Otherwise, you will have to adapt your code for implementing a design pattern.
If you have to do lots of changes to implement a design pattern, your problem is not the design pattern -- it is your code base being monolithic, and tightly coupled. This is a bad/suboptimal design problem, not a design patterns problem.
What I'd like to know, is how much effort should I actually put in creating additional levels of abstraction and designs, only to allow my application to follow OO principles such as loose coupling, programmign to an interface, etc. Is it really worth it? How much effort should I put in this?
Your questions make the (unstated) assumption that the only benefit of loose coupling is the ability to easily implement design patterns. It is not.
Among the benefits of loose coupling are:
- refactoring and redesign flexibility
- less wasted effort
- testability
- increased possibility to reuse the code
- design simplicity
- less time spent in the debugger
... and a few others that don't come to my mind right now.