Skip to content

Instantly share code, notes, and snippets.

@shanecelis
Last active December 26, 2021 03:08
Show Gist options
  • Save shanecelis/fce8321808a99564a873ba5c805abce7 to your computer and use it in GitHub Desktop.
Save shanecelis/fce8321808a99564a873ba5c805abce7 to your computer and use it in GitHub Desktop.
In Defense of Coupling (a programming talk sketch)

In Defense of Coupling

One of the truisms in software development is that decoupling is good.

Often the most useful patterns I’ve come across are ones that add a new decoupling tool into my toolbox. But coupling exists on a spectrum and tight coupling is not necessarily bad. Tight coupling is often benign and proper as you will see.

So I want to share the many ways that two statements of code may be variously coupled starting from the simplest, tightest coupling to a looser and looser coupling. And some Unity Engine specific decoupling will also be shown, but those can be ignored if you're not familiar with Unity.

Here are the many couplings of statements A and B.

Tightest Coupling

statementA;
statementB;

When this code runs, we know statementA runs followed by statementB. It’s the simplest and tightest coupling. We can say that a implies b (a -> b): If a runs then b runs. Likewise we can say that b implies a (b -> a): If b runs then a has run. In this sense these two cannot be broken up. They are tightly coupled.

a -> b
b -> a

(WRITING NOTE: I'm tracing what "runs" but maybe it'd be better to determine what code "knows" about what other code since that's probably the higher concern in OO code.)

Why bother thinking about implications? Because this is essentially what you’re going to be thinking through in an ad hoc fashion when you’re debugging and it helps illustrate what complexity you’re taking on depending on the kind of coupling you decide to make.

Conditional Coupling

statementA;
if (condition) {
  statementB;
}

Here it’s very similar but we’ve lost an implication. a no longer implies b (a -/-> b): If a has run, then we can’t be sure that b has run. It might or it might not have; depends entirely on the conditional.

b -> a
a -/-> b
a ∧ conditional -> b

Extracted Method Coupling

statementA;
MethodB();

...

void MethodB() {
  statementB;
}
a -> b
b -/-> a

Here we’ve extracted b into its own method in the same class. We’ve lost an implication. Method b can be called from anywhere else so it no longer implies a.

Extended Class Coupling

virtual void MethodA() {
  statementA;
}

...

override void MethodA() {
  base.MethodA();
  statementB;
}
b -> a
a -/-> b

This is the most common form of extension advanced by the object oriented paradigm. You couple classes via inheritance. It can be very useful but it’s a very tight and exclusive coupling for languages that only allow single inheritance.

Extracted Class Coupling

statementA;
var b = new ClassB();
b.MethodB();
a -> b
a -> b class
b -/-> a

Here we’ve extracted b into its own class. We instantiate the class and call it so a is tightly coupled to class b but not vice versa. Anyone else can instantiate class b too.

Singleton coupling

statementA;
B.Instance.MethodB();
a -> b class
a -> b instance
a -> b

Here we’re not only tightly coupled to the class B. We’re tightly coupled to the instance of the class B, namely the one global instance. (This is actually probably a tighter coupling than the last one, but it seems easier to talk about after extracted class coupling.)

b -> any callsite for b

Dependency Injection Coupling

statementA;
// b is setup elsewhere
b.MethodB();
a -> b
a -/-> b class
a -> b class decendant
b -/-> a

Here we’ve reduced the coupling from a to b. A still relies on class b or any of its decendants. (Dependency Injection sounds scary but it means you provide a class its dependencies---any other class instances it uses---rather than having it instantiate them itself. This makes it easier to test. There are Dependency Injection libraries and frameworks that make this much more complicated.)

Dependency Injection via Interface

statementA;
// b is setup elsewhere
b.Method();

...
interface C { ... }
class B : C { ... }
a -/-> b implementation
a -/-> b 
a -/-> b class decendant
a -> c
b -/-> a

Dependency Injection via Inspector

class A : MonoBehaviour {
  public B b; // Set in inspector
  void Start() {
    statementA;
    b.MethodB();
  }
}

class B : MonoBehaviour {
  void MethodB() { ... }
}

This is a variant of dependency injection but the dependencies are provided in Unity's inspector.

This is probably one of the quickest ways to connect two things in Unity so maybe it's worth discussing somewhat. (WRITING NOTE: In fact, this may be so prevalent it may be the way to start this whole talk.)

Often times you may start with a few objects: here's the high score, here's a trigger, here's the player. And a game manager object knows about all of them, and possibly polls them, or they all know about the game manager and inform it when something happens. Quickly the game manager becomes this all knowing god object that is difficult to make changes to.

UnityEvent coupling

class A : MonoBehaviour {
  public UnityEvent b;
  void MethodA() {
    statementA;
    b.Invoke();
  }
}

class B : MonoBehaviour {
  void MethodB() {
    statementB;
  }
}

The coupling here exist in the Unity inspector. UnityEvent is less of an event system proper and more of a method binding system. Class A doesn’t know class B. But the component A must know an instance of component B.

Delegate coupling

statementA;
delegateD();

...

delegateD = () => statementB;
a -> b
b -/-> a

This is using composition over inheritance. However, our delegate may be called from anywhere, so b does not imply a.

Whenever, I am facing a kind of cyclic dependency---e.g., class X requires class Y and class Y requires class X---I can usually cut that by adding a delegate to one of the classes.

C# event coupling

class A {
  public delegate void delegateD();
  public event delegateD eventD;
  void MethodA() {
    statementA;
    eventD();
  }
}

class B {
  void MethodB() {
    statementB;
  }
}

...
// glue code.
a.eventD += () => b.MethodB();
a -/-> b
b -/-> a
a -> eventD

Here a and b are very loosely coupled. They do not know of each other, and a running does imply anything about b or vice versa. Some other glue code stiches them together.

Event System Coupling/Message Passing System Coupling

statementA;
Raise(new EventA());

...

AddHandler<EventA>(_ => statementB);
a -> eventA
eventA ∧ handler -> b

Here we’re pretty deeply decoupled. A doesn’t know B but they both know of an event system, or message passing system. There are constraints on this kind of system though. Typically one passing information on through events but there’s no information that comes back. In fact typically one has no idea whether anything responds to the events they emit at all. So if you need information back, e.g., B must always follow A, then an event system may be too loosely coupled.

Conclusion

This concludes my uncomprehensive catalog of the many ways that statements a and b may be coupled. If a coupling exists that pains you, find it here and look further down the list to see what would be better, but I would not go further down the decoupling list than I had to. I hope in seeing this list and coupling as a spectrum, you are driven away from unbridled decoupling. A solution more "decoupled" than another solution is not strictly speaking better.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment