Lets look at this practically
Obj 3 now knows
Obj 4 exists. So what? Why do we care?
"High-level modules should not depend on low-level modules. Both should depend on abstractions."
OK but, aren't all objects abstractions?
DIP also says
"Abstractions should not depend on details. Details should depend on abstractions."
OK but, if my object is properly encapsulated doesn't that hide any details?
Some people like to blindly insist that every object needs a keyword interface. I'm not one of them. I do like to blindly insist that if you're not going to use them now you need a plan to deal with needing something like them later.
If your code is fully refactor-able on every release you can just extract interfaces later if you need them. If you have published code that you don't want to recompile and find yourself wishing you were talking through an interface you'll need a plan.
Obj 3 knows
Obj 4 exists. But does
Obj 3 know if
Obj 4 is concrete?
This right here is why it's so nice to NOT spread
new everywhere. If
Obj 3 doesn't know if
Obj 4 is concrete, likely because it didn't create it, then if you snuck in later and turned
Obj 4 into an abstract class
Obj 3 wouldn't care.
If you can do that then
Obj 4 has been fully abstract all along. The only thing making an interface between them from the start gets you is the assurance that someone wont accidentally add code that gives away that
Obj 4 is concrete right now. Protected constructors can mitigate that risk but that leads to another question:
Are Obj 3 and Obj 4 in the same package?
Objects are often grouped in some way (package, namespace, etc). When grouped wisely change more likely impacts within a group rather than across groups.
I like to group by feature. If
Obj 3 and
Obj 4 are in the same group and layer it's very unlikely that you'll have published one and not want to refactor it while needing to change only the other one. That means these objects are less likely to benefit from having an abstraction put between them before it has a clear need.
If you are crossing a group boundary though it's really a good idea to let objects on either side vary independently.
It should be that simple but unfortunately both Java and C# have made unfortunate choices that complicate this.
In C# it's tradition to name every keyword interface with an
I prefix. That forces clients to KNOW they are talking to a keyword interface. That messes with the refactoring plan.
In Java it's tradition to use a better naming pattern:
FooImple implements Foo However, this only helps at the source code level since Java compiles keyword interfaces to a different binary. That means when you refactor
Foo from concrete to abstract clients that don't need a single character of code changed still have to be recompiled.
It's these BUGS in these particular languages that keeps people from being able put off formal abstracting until they really need it. You didn't say what language you're using but understand there are some languages that simply don't have these problems.
You didn't say what language you're using so I'll just urge you to analyze your language and situation carefully before you decide it's going to be keyword interfaces everywhere.
The YAGNI principle plays a key role here. But so does "Please make it hard to shoot myself in the foot".