Say we consider two inherently coupled elements, using a real-life like example:
Body
PhysicalIllness
Note: the following code is pseudo-Java for the sole purpose of syntax-coloring, pure syntax doesn't really matter here.
Body
is a class that holds several basic properties, such as stamina
, focus
, etc., plus an array of PhysicalIllness
representing all the
illnesses it has contracted.
class Body {
int focus
int stamina
PhysicalIllness[] physicalIllnesses
}
PhysicalIllness
is an abstract class, extended by concrete illnesses. Everything they do/react to depend on their host body. They are born inside a body, they "live" within it and their existence doesn't mean anything outside of a body.
Questions
In such a scenario, wouldn't having a Body
instance injected into PhysicalIllness
's constructor, and stored as a (say, host_body
) reference used throughout the life of the illness, be fine? The illness could then respond to life events (say slept
, hour_elapsed
) on its own and impact its host body accordingly:
abstract class PhysicalIllness {
Body hostBody
PhysicalIllness(Body hostBody) {
this.hostBody = hostBody
}
void onAcquired() {}
void onHourElapsed() {}
void onBodySlept() {}
void onGone() {}
}
class Headache extends PhysicalIllness {
void onAcquired() {
this.hostBody.focus -= 10
}
void onHourElapsed() {
this.hostBody.focus += 2
}
// ...
}
Tight coupling actually seems natural to me here. However, it does produce a cyclic/circular dependency, as Body
holds references to PhysicalIllness
instances and PhysicalIllness
also holds a reference to its "parent", its host body.
Could you people point to any downside of designing things this way, in terms of code maintenance/unit-testing, flexibility, or anything else? I realize there are other answers about this, but since every scenario is different, I'm still unsure if they apply here as well.
Alternative (without circular dependency)
One alternative would obviously be to remove the coupling by having PhysicalIllness
instances be notified of every event by the body (which would pass itself as argument in the process). This requires every method of PhysicalIllness
to have a Body
parameter:
abstract class Illness {
void onAcquired(Body hostBody) {}
void onHourElapsed(Body hostBody) {}
// ...
}
class Headache extends Illness {
void onAcquired(Body hostBody) {
hostBody.focus -= 10
}
void onHourElapsed(Body hostBody) {
hostBody.focus += 2
}
// ...
}
class Body {
// ...
void onHourElapsed() {
for (PhysicalIllness illness in this.physicalIllnesses) {
illness.onHourElapsed(this);
}
}
// ...
}
I feel like this is clunky and actually less logical, because it means a physical illness can exist outside of a body (you can construct one without a host body), and therefore all methods require the "obvious" host_body
parameter.
If I had to summarize this post with one single question: should tight coupling and/or circular dependency between parent/children components be avoided in all situations?