Consider the following example:
// Let's say this method lives in a class MyClass
public void doSomething(Foo foo, Bar bar) {
foo.prepareProcessing();
Worker work = foo.createWorker();
work.dispatchTask(new Task(bar), 12);
work.start();
foo.waitForWorker(work);
foo.finishProcessing();
}
Contrast it to the following:
public void doSomething(Foo foo, Bar bar) {
foo.runTaskAtPriority(bar, 12);
}
In the first example, classes Foo
and MyClass
are tightly coupled: MyClass
not only depends on Foo
, but also on Foo
's dependencies, like the Worker
and Task
classes. Furthermore, MyClass
depends on very specific internals of Foo
: It has to call several methods in a certain order. It's much more likely that changing something in Foo
will also require changing MyClass
.
The second example only depends on Foo
itself. So if Foo
stops using a Worker
internally, MyClass
doesn't have to care. MyClass
and Foo
are loosely coupled.
In the end, it's all about knowledge: The more you need to know about a certain class/method in order to use it, the tighter the coupling.
"Depending on abstractions instead of concretions" is a common way to achieve low coupling. But introducing abstract classes/interfaces doesn't automatically reduce coupling - you rather need to think about ways to reduce the possible interactions (knowledge) one class has with another one.