I think most people would agree that these patterns are great tools, but should be used with moderation and not as the answer for everything. Using them too much would over-complicate the application with little benefit. Patterns should be used only where they can be the best solution or help in the creation of a good solution (do you agree?).

With this in mind:

The book I'm reading (Head First Design Patterns) frequently emphasizes the importance of loose coupling. This loose coupling is acheived by following principles such as 'program to an interface, not an implementation' and 'encapsulate what varies'.

Basically, most patterns I learned so far exist mostly to allow designs to be loosely coupled, and thus more flexible.

I understand the importance and benefits of loose coupling.

But my question is, how much effort should one actually invest in creating loosely coupled, flexible designs?

Those who oppose design patterns say that the costs to using these patterns often outweighs the benefits. You invest a lot of time in creating a loosely coupled design using some pattern, where in reality - the loose coupling, 'programing to an interface, not an implementation', and all of these principles - might not actually be so important.

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?

4 Answers
4

Basically, building loosely coupled code isn't really very hard. Training your mind to think in terms of loose coupling, on the other hand, is. And, well, I believe it to be totally worth it.

Suppose you take a job that uses an iterative approach to software development, as has been recommended by most everyone over the last 20 years. You build something that works beautifully in iteration 1. In iteration 2 you build on top of it, adding some new features, and somewhat bending one or two things a bit when they don't fit into your iteration 1 concept of how things should work. Now comes iteration 3, and you find out the requirements demand you rethink your basic architecture. How do you tear your code apart and rebuild it without going back to square one?

This happens. A lot. It either makes projects run late, or it makes everyone too scared to do what needs to be done in later iterations. In the best case, you get a Big Ball of Mud. Things like loose coupling and the single responsibility principle mitigate these problems in a big way. That's why SOLID principles are touted--they really do help.

And you will find that after you've got a few loosely coupled designs under your belt, it starts to come naturally. Patterns are things that people found that worked for them, so they documented them. They're useful tools that also come naturally with time.

Amount of effort required is not always going to tell you enough; I think a better measure would be the effort to payoff ratio. But figuring out what the payoff might be is not always straightforward and error free.

The thing to keep in mind with patterns that increase flexibility is that it costs some effort to put them in, and unless you can see exactly why you need them, you may end up having the complication in the code, but never ever using it. If the flexibility is never flexed, it might as well not be there.

There is one thing that you should do always (or as close to it as you reasonably can get): making the individual components of your software (objects for OOP, functions for functional or procedural programming) into black boxes. A black box is something whose insides (implementation) we don't know or care about.

If we don't know or care about how it works, then we aren't going to try to use anything but the interface, so if the implementation changes without changing the interface, that only matters inside the black box -- nothing outside it can be affected. It also makes thinking about the program easier, because you don't have to keep every single detail of the whole program in your head. The more details you can legitimately forget, the more room there is in your head for the details that matter.

I think you've misunderstood the objections to design patterns; I'm not a fan of them, but I very much like the principle of loose coupling and programming to an interface. My strongest objection to them is that they're presented as a pre-packaged solution that doesn't encourage understanding the principles behind them, but rather encourages memorization of details. I'm not going to recommend that you don't study them, but when you do, remember that understanding the principles behind them is more important than the specifics of any particular pattern.

One of the objections to design patterns is that they're 'obvious' or that 'I was using them before I heard of them, just not by that name'. The reason people can make those objections, or the originators of design patterns could come up with them in the first place, is that they understood the principles behind them.

Using black boxes organizes your code in a sane way and they don't typically cost much (if anything); use them everywhere. Using a design pattern or some other technique that adds a layer of abstraction or complicates things has a cost; never use them just because they're pretty or neat or clever; use them when they fit the problem and the cost is worth paying to get the benefit.

Your question seems to equate loose coupling with Design Patterns, but they are really separate, but related things.

Loose coupling is a fundamental concern in programming. As soon as we moved away from machine code and into symbolic languages, programs became loosely coupled with their execution. Etc.

OO itself is basically a model for creating loosely coupled components. Encapsulation makes the internals of objects loosely coupled with other objects. Functional programming maybe even more so, because the function execution is loosely coupled with the execution of other functions in the extreme.

Almost by definition, any design you do is going to involve loose coupling. There are ways you can arrange to have things tightly coupled inadvertently, but I've only seen a few cases where someone actually designed a system to be more tightly coupled.

(Every now and then I work with someone who wants to prevent future developers from making design choices by embedding static references to particular framework artifacts, forcing their use - on purpose. But that's really the exception. Most design revolves around breaking things up for modularity and reuse in lots of contexts.)

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.