|
| 1 | +# Configuration Resolution: Scope-First vs. Type-First |
| 2 | + |
| 3 | +## The Two Strategies |
| 4 | + |
| 5 | +Given `Foo extends Entity` with configurations at four points in the matrix: |
| 6 | + |
| 7 | +| | Instance | Global | |
| 8 | +|---|---|---| |
| 9 | +| **Foo** | I·Foo | G·Foo | |
| 10 | +| **Entity** | I·Entity | G·Entity | |
| 11 | + |
| 12 | +**Scope-first** exhausts all instance settings before looking at global ones: |
| 13 | +``` |
| 14 | +I·Foo → I·Entity → G·Foo → G·Entity → Default |
| 15 | +``` |
| 16 | + |
| 17 | +**Type-first** exhausts all Foo-specific settings before looking at Entity: |
| 18 | +``` |
| 19 | +I·Foo → G·Foo → I·Entity → G·Entity → Default |
| 20 | +``` |
| 21 | + |
| 22 | +Both agree on `I·Foo` being first and `G·Entity` being last. The dispute is the middle two. |
| 23 | + |
| 24 | +--- |
| 25 | + |
| 26 | +## Where Scope-First Makes Sense |
| 27 | + |
| 28 | +**Setting: `textAlign` or `nullRepresentation`** |
| 29 | + |
| 30 | +A developer creates a reporting grid and writes: |
| 31 | +```java |
| 32 | +easyGrid.configuration.forType(Entity.class).setTextAlign(RIGHT); |
| 33 | +``` |
| 34 | +The intent is *"every Entity-like column in this grid should be right-aligned."* Since `Foo IS-AN Entity`, Foo columns should also be right-aligned. Instance context wins: the developer explicitly opted every Entity column into this layout decision, and Foo, being substitutable for Entity, should inherit it. |
| 35 | + |
| 36 | +If type-first were applied, `G·Foo` would sit between `I·Foo` and `I·Entity`. An unrelated global renderer for Foo would silently block the instance-level alignment — which is the opposite of what the developer intended. |
| 37 | + |
| 38 | +--- |
| 39 | + |
| 40 | +## Where Type-First Makes Sense |
| 41 | + |
| 42 | +**Setting: `rendererFactory`** |
| 43 | + |
| 44 | +Globally, `Foo` has a specialized renderer registered: |
| 45 | +```java |
| 46 | +GlobalEasyGridConfiguration.forType(Foo.class) |
| 47 | + .setRendererFactory(FooRenderers.of(...)); |
| 48 | +``` |
| 49 | +Independently, an instance configures Entity columns with a generic formatter: |
| 50 | +```java |
| 51 | +easyGrid.configuration.forType(Entity.class).setFormatter(e -> e.getId().toString()); |
| 52 | +``` |
| 53 | + |
| 54 | +Under scope-first, `I·Entity` wins over `G·Foo`. The Foo-specific renderer — which encodes *what a Foo looks like*, arguably a type invariant — gets silently replaced by a generic Entity formatter that the developer may not have intended to apply to Foo. |
| 55 | + |
| 56 | +Type-first preserves the type contract: `G·Foo` sits above `I·Entity`, so Foo's rendering semantics are honoured even across scope boundaries. |
| 57 | + |
| 58 | +--- |
| 59 | + |
| 60 | +## Liskov Substitution Principle |
| 61 | + |
| 62 | +LSP says: a `Foo` must be usable wherever an `Entity` is expected, without callers needing to know the difference. |
| 63 | + |
| 64 | +Applied to configuration, it has two implications that pull in opposite directions: |
| 65 | + |
| 66 | +**LSP supports scope-first for behavioural settings.** If you configure `Entity` at instance level — null representation, alignment, a locale-specific formatter — you are specifying how *this grid* handles all Entity values. A `Foo` substituting for `Entity` should satisfy those same observable postconditions. Refusing to inherit `I·Entity` because Foo has a `G·Foo` entry would mean the grid behaves differently for `Foo` vs `Entity` in ways the calling code did not anticipate. |
| 67 | + |
| 68 | +**LSP supports type-first for type-defining settings.** LSP also requires that subtypes honour their own invariants. If `Foo` has a type-level rendering contract — *this is how a Foo is displayed* — then substituting a generic `Entity` renderer for it violates the Foo invariant. The renderer is not just a postcondition on the grid; it is a property of `Foo` itself. |
| 69 | + |
| 70 | +The tension resolves to: **scope-first is correct as a default ordering**, but a setting that constitutes a type invariant (renderer factory, type-specific formatter) has stronger claims when registered at global-type specificity than a setting that constitutes an instance context decision (null representation, alignment). |
| 71 | + |
| 72 | +--- |
| 73 | + |
| 74 | +## The Current Bug |
| 75 | + |
| 76 | +The current `InstanceEasyGridConfiguration` constructs the parent of any instance-level type config by calling `GlobalEasyGridConfiguration.resolve(type)` — jumping straight to global scope. For a `Foo` column, the effective chain is: |
| 77 | + |
| 78 | +``` |
| 79 | +I·Foo → G·Foo → G·Entity → Default |
| 80 | +``` |
| 81 | + |
| 82 | +`I·Entity` never appears. Instance-level Entity configuration is invisible to Foo columns within the same instance. This is wrong under either strategy. |
| 83 | + |
| 84 | +--- |
| 85 | + |
| 86 | +## What Should Be Implemented |
| 87 | + |
| 88 | +The correct chain for scope-first (the generally right default) is: |
| 89 | + |
| 90 | +``` |
| 91 | +I·Foo → I·Entity → I·Object → G·Foo → G·Entity → G·Object → Default |
| 92 | +``` |
| 93 | + |
| 94 | +Concretely, when `InstanceEasyGridConfiguration` creates a config for `Foo`, its parent should be `I·Entity` (obtained by walking the instance's *own* class-map up the hierarchy), not `G·Foo` directly. Only after the instance hierarchy is exhausted should the chain cross into the global scope — at the matching type level. |
| 95 | + |
| 96 | +This means `EasyGridConfigurationClassMap.getOrCreate` needs to resolve parents by walking the *same map's* hierarchy first, and the cross-scope link (`resolve` jumping to global) should only be installed at the root of each instance's hierarchy chain, not at every node. |
0 commit comments