|
| 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 in favour of **scope-first for all properties**. The type-first argument rests on the assumption that `I·Entity` displaces `G·Foo` unintentionally — but instance configuration is always deliberate. A developer who writes `forType(Entity.class).setFormatter(...)` knows `Foo IS-AN Entity`; if they wanted Foo to keep its global renderer they would have set `I·Foo` separately. The argument collapses entirely at broad overrides such as `forType(Object.class).setFormatter(...)`, where the developer has unambiguously stated that everything in this grid uses this formatter and global type-specific registrations cannot claim precedence. **Global registrations are defaults. Instance registrations are decisions. Decisions outrank defaults regardless of type specificity.** |
| 71 | + |
| 72 | +--- |
| 73 | + |
| 74 | +## Current Implementation |
| 75 | + |
| 76 | +The implementation uses **scope-first** for all properties. For a `Foo extends Entity` column the effective chain is: |
| 77 | + |
| 78 | +``` |
| 79 | +per-column → I·Foo → I·Entity → I·Object → G·Foo → G·Entity → G·Object → Default |
| 80 | +``` |
| 81 | + |
| 82 | +This is built in two pieces: |
| 83 | + |
| 84 | +**Instance chain** — `EasyGridConfigurationClassMap.getOrCreate(Foo)` walks the Java class hierarchy within the same map, producing a `ColumnConfigurationImpl` chain: |
| 85 | + |
| 86 | +``` |
| 87 | +I·Foo(impl) → I·Entity(impl) → I·Object(impl) |
| 88 | +``` |
| 89 | + |
| 90 | +**Global chain** — `GlobalEasyGridConfiguration.resolve(Foo)` similarly produces: |
| 91 | + |
| 92 | +``` |
| 93 | +G·Foo(impl) → G·Entity(impl) → G·Object(impl) |
| 94 | +``` |
| 95 | + |
| 96 | +**Bridge** — `InstanceEasyGridConfiguration.forType(Foo)` wraps both into a `ColumnConfigurationLink`: |
| 97 | + |
| 98 | +``` |
| 99 | +ColumnConfigurationLink(primary = I·Foo chain, fallback = G·Foo chain) |
| 100 | +``` |
| 101 | + |
| 102 | +`ColumnConfigurationLink.get()` consults the primary chain first and only falls back to the global chain when the primary returns `null` for a given property. |
| 103 | + |
| 104 | +**Per-column** — `InstanceEasyGridConfiguration.resolve(Foo)` wraps the link in a fresh `ColumnConfigurationImpl` whose fields are overridden by column-level setters (`setNullRepresentation`, `setFormatter`, `setRendererFactory`). |
| 105 | + |
| 106 | +**Default** — when the entire chain returns `null` for `getRendererFactory()`, `EasyColumn.createRenderer` applies a `ColumnConfigurationTextRenderer` with null-representation support as the last resort. |
0 commit comments