79484937

Date: 2025-03-04 21:49:43
Score: 2.5
Natty:
Report link

Why would interfaces be immune to the problems that cyclic dependencies introduce? All of the problems caused by cyclic dependencies happen irrespective of the specifics of how those dependencies are introduced.

The linked post says:

Are there good cycles?

Some cycles are valid, helpful, and do not affect maintainability or reliability: String and Object, File and Folder, Node and Edge. Usually, such circles sit within one package and do not contribute into circular dependencies between packages.

This is flat wrong. Cyclic dependencies are never "good," at best you could say that some very few examples could be regarded as a necessary evil, but that's as far as I'd go. I'm not sure the examples provided are even good ones, either.

Let's look at the examples given. Look at File and Folder. Folders form a tree structure into which files are placed. Think about if you have a binary tree that stores integers…can you think of any good reason that Integer should know what TreeNode it's placed in? Why should the story be any different for File? It isn't. Files should exist without any knowledge of the folder they're in. Think about hard links in Unix where you can create two handles to the same file. Which folder is it in?

Your instinct might be to object here on the basis of: What if I have a file, and I want to know what folder it's in? Again, I point to the tree example, what if I have an integer and I want to know what node it's in? Should I ask the integer? No, I should ask the tree, and it will go ahead and locate the node for me. Similarly, I should ask the FileManager of the file system to give me that information.

Node and Edge are another example. What is the role of a node? To encapsulate some content within a graph structure. What is the role of an edge? To connect nodes. Why should a node ever know about the existence of an edge? If your design for a graph introduces the concept of an edge as a first-class citizen by making it a class, then go all the way and structure that design with rigor. Otherwise, don't introduce the concept of edges as first-class citizens and use the design most do, just have nodes reference each other directly.

In such a design, a Graph would be a collection of edges with each edge having two nodes. (Could a node have zero or one node? Depends on the context of how the graph will be used.) The Graph class would also probably want to be able to look up edges by node, which implies that it would also keep edges indexed by the nodes they connect. Node never needs to know about Edge.

I've saved the hardest example for last, Object and String. This is an absolutely unavoidable example, but not because of anything design element to do with the objects conceptually, rather because of practical reasons. When Java was introduced, various OO design principles were compromised in order to make Java performant. That's why Object knows about String, and it's the only reason. It's also why Object has equals and hashCode methods. If Java were to be redesigned today from the ground up, it's unlikely that it would have any of these methods and they would instead be captured by interfaces that other classes could implement, e.g.:

interface Stringifiable {
  String toString();
}

interface Equatable<E extends Equatable<E>> {
  boolean equals(E equatable);
}

interface Hashable<E> extends Equatable<E> {
  long hashCode();
}

The Object class could provide static utility methods that current reside in java.util.Objects that would help provide implementations of these methods, and the platform could even provide standard annotations as part of the JDK that allow the compiler to just call those default utility methods to save you having to implement the methods yourself.

Of course, this is all only possible in retrospect, it can't be added now because it would break backwards compatibility. But it would look something like this:

@Stringifiable @Equatable @Hashable
class Foo {
  // blah blah blah
}

Now the compiler would know to go ahead and add those methods. This approach could work for cloneable, serializable, etc, too. (Arguably those two were mistakes and shouldn't be provided by the JDK at all, at least not as core things, but that's neither here nor there.) The point is that introducing cyclic dependency is pretty much always not good, the question is, can it be avoided? In early Java, it couldn't as a matter of practicality.

The core reason that cyclic dependencies are always bad from a principled standpoint is that classes should only ever encapsulate data and behaviors that are intrinsic to the type being defined, meaning "free from context." If you examine the API of a class or crack it open and look at its insides, is everything inside it inherent to that thing itself, or does it reference bits of its outside context?

Every time you design a class that encapsulates some of its context, you reduce the generalizability of that type because it can no longer exist outside that context. This is why Integer shouldn't have a reference to the TreeNode that contains it, because it means that Integer no longer makes sense in any other context where there isn't a TreeNode. A hacky approach to this is to say, "Well, let's just set that field to null." If this makes sense, then it means that Integer must also store a null reference to everything that might ever contain it: array, a graph node, a tree node, nodes that are in data structures that may not have been conceived of yet. Obviously this is bad.

With this in mind, now it should be clear why cyclic dependencies are always bad. There's no way a cycle between two classes can exist without violating this principle. If we stick to proper design, and A has a reference to B, that means B is intrinsic to A. If that's the case, then A cannot be both a separate class and intrinsic to B. If they are intrinsic to each other, then they both APIs belong in the same class. If they're not, then the dependency should only go one direction (at most).

Reasons:
  • Blacklisted phrase (1): I want to know
  • RegEx Blacklisted phrase (1): I want
  • Long answer (-1):
  • Has code block (-0.5):
  • Contains question mark (0.5):
  • Starts with a question (0.5): Why would in
  • Low reputation (1):
Posted by: severoon