· 5 min · #unity #architecture #metrics
Bloom & Place devlog #2: the baseline I'm trying to beat
Before refactoring anything, I measured the prototype's coupling. Here are the numbers I have to improve on, including the line of code I'm most embarrassed by.
Don’t refactor what you didn’t measure
Last post I committed to a falsifiable architecture claim. That only works if the “before” picture is real, not a strawman I cooked up to make the after look good. So before touching VContainer, I ran Ce/Ca over the existing prototype as it stands today.
A quick refresher on the metrics
Three numbers, all from Robert C. Martin’s Agile Software Development: Principles, Patterns, and Practices. Devlog #7 goes deep on these. Short version below so this post stands on its own.
- Efferent Coupling (
Ce). Outgoing arrows. How many other classes does this class depend on? Count the distinct external types it references by name. High Ce means this class knows about a lot of stuff and breaks when any of it changes. - Afferent Coupling (
Ca). Incoming arrows. How many other classes depend on this one? High Ca means lots of code will break if I change this class. - Instability Index (
I). Ratio of outgoing to total, computed asI = Ce / (Ce + Ca), range 0.00 to 1.00. AnI = 0means stable (depended on but doesn’t reach out). AnI = 1means unstable (reaches out but nothing depends on it). Neither extreme is universally good or bad. It depends on the class’s role. Foundation and data classes should be near 0, while view leaves can sit at 1.
The Stable Dependency Principle ties them together. Dependencies should always flow from less stable (high I) toward more stable (low I). When a high-I class depends on a low-I class, that’s correct. When a low-I core class reaches into a high-I manager, that’s the violation worth hunting for.
The headline numbers
Computed across every script in the prototype. I = Ce / (Ce + Ca), range 0.00 (stable) to 1.00 (unstable). The verdict column is whether the I value is appropriate for that class’s architectural role, not just whether the number is low.
| Class | Layer | Ce | Ca | I | Status | Verdict |
|---|---|---|---|---|---|---|
Plant | Core | 5 | 4 | 0.56 | Unstable | Borderline |
PlantSelectionManager | Manager | 3 | 2 | 0.60 | Unstable | Borderline |
ScoreManager | Manager | 3 | 1 | 0.75 | Very unstable | Unhealthy |
Seat | Core | 2 | 3 | 0.40 | Fairly stable | Healthy (watch) |
PlantParticleEffect | View | 1 | 0 | 1.00 | Fully unstable | Healthy |
CustomCursorManager | Foundation | 0 | 2 | 0.00 | Fully stable | Healthy |
PlantOutlineHover | Foundation | 0 | 1 | 0.00 | Fully stable | Healthy |
PlantPreference | Data | 0 | 2 | 0.00 | Fully stable | Healthy |
SeatTag | Data | 0 | 2 | 0.00 | Fully stable | Healthy |
Reading the table by row, in the order the verdicts hurt most:
ScoreManager, Unhealthy. Manager-layer classes can sit at I ≤ 0.60 without alarm. 0.75 is over the line. Three outgoing dependencies (Seat,Plant,PreferenceChecker) and only one incoming (PlantSelectionManager). It reaches outward more than it gets reached inward, which is the wrong shape for a class that’s supposed to be a score sink.Plant, Borderline. Core-layer classes should sit at I ≤ 0.40.Plantis at 0.56. The Ce list is the giveaway:Seat,PlantPreference,PlantOutlineHover,PlantSelectionManager,CustomCursorManager. Two of those (the managers) are higher in the architecture thanPlant. Core reaching upward into managers, wrong direction.PlantSelectionManager, Borderline. I = 0.60 sits on the edge of acceptable for a manager. The real problem isn’t the number, it’s thatPlantSelectionManagerandPlanthave a circular dependency. Each one knows about the other, which breaks the Acyclic Dependencies Principle outright.Seat, Healthy on paper, watch in practice. I = 0.40 is fine for core. ButSeat.OnMouseDowncallsPlantSelectionManager.Instancedirectly, which is the SDP-violation arrow that doesn’t show up in the raw I number. It shows up when you ask which layer is each dependency from.- Foundation and Data layers, all Healthy. Ce = 0 across the board. They depend on nothing and get depended on. Exactly the shape you want at the bottom of the architecture.
PlantParticleEffect, Healthy despite I = 1.00. Leaf node, view layer. I = 1.00 is correct for view code. The metric only flags this if you read it without context.
The lesson the table teaches in one sentence: the bad numbers cluster in the middle of the architecture, not at the edges.
The line I’m most embarrassed by
// Seat.cs
void OnMouseDown() {
PlantSelectionManager.Instance.OnSeatClicked(this);
}
Seat is core gameplay state. PlantSelectionManager is, well, a manager. The dependency arrow points the wrong direction. Core is reaching into the manager layer, which violates the Stable Dependency Principle in the most literal way possible: an unstable handler in a stable class.
The reason it’s there is the reason these things are always there. I needed selection state from inside an input callback, and .Instance was the closest thing in arm’s reach.
Why I’m publishing this before the fix
Two reasons. First, if I publish only the after-numbers, you have no way to evaluate the claim. “We made it better” with no baseline is marketing.
Second, writing the baseline down forces me to commit to it. The Ce/Ca for Plant won’t quietly drift while I work, because it’s pinned to this post. If the refactor doesn’t move it, that’s on the architecture, not on me re-running the tool with friendlier inputs.
What I want the after-table to look like
Targets, written down before any refactor lands so I can’t shift them later:
| Class | Ce now | Ce target | I Now | I Target | How |
|---|---|---|---|---|---|
Plant | 5 | 2–3 | 0.56 | 0.25–0.35 | Drop direct deps on PlantSelectionManager and CustomCursorManager, replace with events |
PlantSelectionManager | 3 | 3 | 0.60 | ~0.60 | Number can stay; kill the circular dependency with Plant |
ScoreManager | 3 | 2 | 0.75 | 0.50–0.60 | Inject via VContainer, remove static Instance, become a pure event subscriber |
Seat | 2 | 1 | 0.40 | 0.20–0.30 | Remove PlantSelectionManager call from OnMouseDown; publish a seat-clicked event instead |
Foundation- and Data-layer classes (CustomCursorManager, PlantOutlineHover, PlantPreference, SeatTag) already sit at I = 0.00. The refactor shouldn’t move them. If Ce on any of those goes above 0, that’s a regression I have to explain.
PlantParticleEffect is allowed to stay at I = 1.00. It’s a view leaf and that’s the right shape for a view leaf.
Next
Devlog #3 zooms out: what coupling actually is, why a metric from 1968 still earns its keep, and why “loosely coupled” is harder to live up to than to say.