# Example 1: Storifying Mixed Abstractions and Extracting Logic into Leaf Types This is a real-world example from a production codebase showing how to transform a complex function by extracting logic into a new leaf type. ## Key Learning: From Fat Function to Lean Orchestration + Leaf Type The original function contained ALL the logic. After refactoring: - **Orchestration layer** (thin): `upsertIfaceAddrHost` - reads like a story - **Leaf type** (juicy logic): `IPConfig` - owns IP collection, validation, testable in isolation - **Result**: Most complexity moved to testable leaf type with 100% coverage potential This is a real world example from a production codebase and what the developer chose to refactor. It is not perfect, and it could be improved further, but it demonstrates the core refactoring pattern: ## Before refactoring ```go // upsertIfaceAddrHost sets any IP from iface or returns error if provided IP not match to the interface func (c *Config) upsertIfaceAddrHost(iface net.Interface) error { addr, err := iface.Addrs() if err != nil { return fmt.Errorf("network addr: %w", err) } var ( addrIP4Added bool addrIP6Added bool ) for _, a := range addr { ipnet, ok := a.(*net.IPNet) if !ok || !ipnet.IP.IsGlobalUnicast() { logger.Debug().Str("addr", a.String()).Msg("Not a global unicast address") continue } if ipnet.IP.To4() == nil { // validate IP6 if addrIP6Added { // already added. skip continue } if !c.parseIP6(ipnet) { return fmt.Errorf("IP6 %q address is not valid", c.IP6) } logger.Debug().Str("ip6", c.IP6).Msg("set IP6") addrIP6Added = true continue } if addrIP4Added { continue // already added. skip } if !c.parseIP4(ipnet) { return fmt.Errorf("IP4 %q address is not valid", c.IP4) } logger.Debug().Str("ip4", c.IP6).Msg("set IP4") addrIP4Added = true } if !addrIP4Added && !addrIP6Added { return fmt.Errorf("IP address is not valid. IP4: %q, IP6: %q", c.IP4, c.IP6) } return nil } func (c *Config) isIP4Set() bool { return len(c.IP4) > 0 } func (c *Config) isIP6Set() bool { return len(c.IP6) > 0 } func (c *Config) parseIP4(ipnet *net.IPNet) bool { if c.IP4 == ipnet.IP.To4().String() { logger.Debug().Str("addr", ipnet.IP.To4().String()).Msg("IP4 match to interface") return true } if c.IP4 == anyIPv4 || c.IP4 == "" { logger.Debug().Str("addr", ipnet.IP.To4().String()).Msg("Using interface IP for NodeIP") // use first ip found from interface c.IP4 = ipnet.IP.To4().String() return true } return false } func (c *Config) parseIP6(ipnet *net.IPNet) bool { if c.IP6 == ipnet.IP.To16().String() { logger.Debug().Str("addr", ipnet.IP.To16().String()).Msg("IP6 match to interface") return true } if c.IP6 == anyIPv6 || c.IP6 == "" { logger.Debug().Str("addr", ipnet.IP.To16().String()).Msg("Using interface IP for NodeIP") // use first ip found from interface c.IP6 = ipnet.IP.To16().String() return true } return false } ``` ## Code Smells Identified The `upsertIfaceAddrHost` function suffers from: 1. **Fat Function Anti-Pattern** - All logic crammed into one function (48 lines, complexity 12) 2. **Hidden Side Effects** - `parseIP4/parseIP6` names hide mutation 3. **Mixed Abstraction Levels** - Combines low-level iteration with high-level business logic 4. **No Leaf Types** - All logic lives in methods, nothing is extracted to testable types 5. **Flow Control Complexity** - Nested ifs, continues, boolean flags tracking state 6. **Poor Testability** - Must mock `net.Interface` to test anything **The Core Problem**: All the juicy logic is trapped in a complex orchestration function. We need to extract it into a leaf type. ## After refactoring ```go // upsertIfaceAddrHost sets any IP from iface or returns error if provided IP not match to the interface func (c *Config) upsertIfaceAddrHost(iface net.Interface) error { addr, err := iface.Addrs() if err != nil { return fmt.Errorf("network addr: %w", err) } ipConfig := collectIPConfigFrom(addr) if err = c.AlignIPs(ipConfig); err != nil { return fmt.Errorf("align config IPs err: %w", err) } return nil } func collectIPConfigFrom([]net.Addr addresses) IPConfig { var ipConfig IPConfig for _, a := range addresses { ipConfig.AddAddress(a) } return ipConfig } type IPConfig struct { IP4 string IP6 string } func (c *IPConfig) AddAddress(a net.Addr) { ipnet, ok := a.(*net.IPNet) if !ok || !ipnet.IP.IsGlobalUnicast() { logger.Debug().Str("addr", a.String()).Msg("Not a global unicast address") return } if ipnet.IP.To4() != nil { if len(c.IP4) > 0 { return // already added } c.IP4 = ipnet.IP.To4().String() } if ipnet.IP.To4() == nil { if len(c.IP6) > 0 { return // already added } c.IP6 = ipnet.IP.To16().String() return } } func (c *IPConfig) Validate() error { if len(c.IP4) == 0 && len(c.IP6) == 0 { return errors.New("IP addresses are not found") } return nil } func (c *Config) AlignIPs(ipConfig IPConfig) error { if err := ipConfig.Validate(); err != nil { return fmt.Errorf("ip config is not valid: %w", err) } if err := c.alignIPv4(ipConfig.IP4); err != nil { return fmt.Errorf("align IPv4 err: %w", err) } if err := c.alignIPv6(ipConfig.IP6); err != nil { return fmt.Errorf("align IPv6 err: %w", err) } if len(c.IPv4) == 0 { c.ExistingClusterStartedIPv4Only = false } if c.ExistingClusterStartedIPv4Only && len(c.IPv6) > 0 { logger.Warn(). Str("IPv6", c.IPv6). Str("IPv4", c.IPv4). Msg("existing cluster is running in IPv4 only. Dual stack is not possible.") } return nil } func (c *Config) alignIPv4(ip string) error { if c.IPv4 == ip { logger.Debug().Str("addr", ip).Msg("IP4 match to interface") return nil } if c.IPv4 == anyIPv4 || c.IPv4 == "" { logger.Debug().Str("addr", ip).Msg("Using interface IP for NodeIP") // use first ip found from interface c.IPv4 = ip return nil } return fmt.Errorf("existing IPv4 [%s] mismatch configured [%s]", ip, c.IPv4) } func (c *Config) alignIPv6(ip string) error { if c.IPv6 == ip { logger.Debug().Str("addr", ip).Msg("IPv6 match to interface") return nil } if c.IPv6 == anyIPv6 || c.IPv6 == "" { logger.Debug().Str("addr", ip).Msg("Using interface IP for NodeIP") // use first ip found from interface c.IPv6 = ip return nil } return fmt.Errorf("existing IPv6 [%s] mismatch configured [%s]", ip, c.IPv6) } ``` ## Refactoring Thought Process ### Step 1: Identify What's Orchestration vs. Logic The original function does 3 things: 1. **Collects** IP addresses from interface (LOGIC) 2. **Validates** them (LOGIC) 3. **Aligns** Config state with discovered IPs (orchestration + logic) **Decision**: Extract the collection logic into a new type ### Step 2: Create a Leaf Type to Hold the Juicy Logic Instead of keeping all logic in `Config` methods: → **Created `IPConfig` type** - a leaf type (no dependencies on other types) → **Moved collection logic** into `IPConfig.AddAddress()` method → **Moved validation logic** into `IPConfig.Validate()` method **Why this matters**: - `IPConfig` is now a **leaf type** with testable logic - Can achieve 100% unit test coverage without mocking anything - Logic is isolated and reusable ### Step 3: Make Orchestration Read Like a Story `upsertIfaceAddrHost` now reads: 1. Get addresses from interface 2. Collect them into IPConfig 3. Align our config with what we collected No nested ifs, no continues, no boolean flags - just clear steps. ### Step 4: Honest Naming for Side Effects `parseIP4/parseIP6` → `alignIPv4/alignIPv6` The word "align" signals mutation, "parse" suggested read-only. ## Key Improvements ### Architecture * **Fat function became lean orchestration** - 48 lines → 12 lines in main function * **Created leaf type `IPConfig`** - Holds all the juicy IP collection logic * **Separated concerns** - Collection (IPConfig) vs. Alignment (Config methods) ### Readability * **Storified orchestration** - `upsertIfaceAddrHost` reads like: collect → align → done * **Honest naming** - `align*` reveals side effects vs. `parse*` hiding them * **Single level of abstraction** - Each function operates at one conceptual level ### Testability * **Leaf type with 100% coverage** - `IPConfig` can be fully unit tested without mocks * **Testable in isolation**: ```go // Test collection logic without network code func TestIPConfig_AddAddress(t *testing.T) { cfg := &IPConfig{} cfg.AddAddress(createIPv4Addr("192.168.1.1")) assert.Equal(t, "192.168.1.1", cfg.IP4) } ``` * **Integration tests for orchestration** - Test the seams between IPConfig and Config ### Complexity Reduction **Before**: Cognitive complexity 18, cyclomatic complexity 12 **After**: Max complexity 6 per function ## Refactoring Patterns Applied 1. **Type Extraction** → Created `IPConfig` leaf type for IP collection 2. **Storifying** → Top-level reads: collect → validate → align 3. **Honest Naming** → `align*` instead of `parse*` reveals mutation 4. **Single Responsibility** → Each function does ONE thing 5. **Early Returns** → Replaced `continue` with `return` for clarity ## The Leaf Type Strategy **Before**: All logic trapped in one place ``` Config.upsertIfaceAddrHost() { // ALL the logic here: iteration, validation, collection, alignment // 48 lines, complexity 12, impossible to test separately } ``` **After**: Logic extracted to leaf type ``` IPConfig (LEAF TYPE - no dependencies) ├─ AddAddress() // Collection logic (juicy!) └─ Validate() // Validation logic (juicy!) Config (ORCHESTRATOR) ├─ upsertIfaceAddrHost() // Thin story: collect → align └─ AlignIPs() // Thin coordination ``` **Result**: Most of the complexity now lives in `IPConfig`, a leaf type with 100% test coverage potential. # Example 2: Primitive Obsession with Multiple Types and Storifying Switch Statements This real-world example shows how to transform a 60-line function with nested switches and boolean flags into a 7-line story by extracting multiple leaf types. The original function was named `validateCIDR()` but actually mutated state - a classic naming smell that triggered deeper refactoring. ## Key Learning: From Primitive Obsession to Type-Rich Design (Without Over-Abstraction!) **Before**: All logic operates on raw `[]string` with manual parsing and boolean flags ``` One 60-line function └─ Manual string parsing + switch statements + boolean flags ``` **After**: Multiple focused leaf types with clear responsibilities ``` K3SArgs (Leaf Type - string slice wrapper) ├─ ParseCIDRConfig() → returns domain model └─ AppendCIDRDefaults() → mutation with explicit dependencies CIDRConfig (Leaf Type - domain model with private fields) ├─ clusterCIDRSet (private bool - controlled mutation) ├─ serviceCIDRSet (private bool - controlled mutation) ├─ ClusterCIDRSet() → accessor (read-only) ├─ ServiceCIDRSet() → accessor (read-only) └─ AreBothSet() → reads like English Note: No CIDRPresence wrapper! Private fields achieve same safety without wrapper ceremony. IPVersionConfig (Leaf Type - configuration) └─ DefaultCIDRs() → value generator Main Function (Orchestrator - 7 lines) └─ Story: create config → convert to type → append defaults → store back ``` **Result**: - Main function reduced from 60 to 7 lines - Most complexity lives in 3 leaf types (100% testable) - Each type can be tested without mocking anything - Code reads like English: "append CIDR defaults based on IP config" - **Avoided over-abstraction**: Rejected `CIDRPresence` wrapper, used private fields instead ## Code Smells Identified 1. **Misleading Name** - `validateCIDR()` doesn't validate - it mutates! Should return `bool` or `error` if validating 2. **Primitive Obsession (CRITICAL)** - Operating on raw `[]string`, manual parsing everywhere, no encapsulation 3. **Mixed Abstraction Levels** - Jumps between string splitting (`strings.SplitN`) and business logic (`isClusterCIDRSet`) 4. **Boolean Flags Tracking State** - Two booleans tracking related information instead of domain type 5. **Switch Statement Duplication** - Three nearly identical switch cases (IPv4/IPv6/dual) differing only in data values 6. **Fat Function** - 60 lines doing: parse + detect + construct + mutate 7. **Hard to Test** - Must construct entire Config object, can't test parsing independently **The Core Problem**: All the juicy logic is trapped in string manipulation and scattered across switch cases. We need multiple leaf types to separate parsing, configuration, and value generation concerns. ## Before Refactoring ```go // Original name was validateCIDR - misleading! func (c *Config) alignCIDRArgs() { var ( isClusterCIDRSet bool isServerCIDRSet bool ) // LOW LEVEL: String parsing for _, arg := range c.Configuration.K3SArgs { kv := strings.SplitN(arg, "=", 2) if len(kv) != 2 { continue } switch kv[0] { case "--cluster-cidr": isClusterCIDRSet = true case "--service-cidr": isServerCIDRSet = true } } // HIGH LEVEL: Business logic if isClusterCIDRSet && isServerCIDRSet { return // both set, nothing to do } // DUPLICATION: Same pattern repeated 3 times with different values switch { case c.isIP4Set() && c.isIP6Set(): if !isClusterCIDRSet { c.Configuration.K3SArgs = append(c.Configuration.K3SArgs, fmt.Sprintf("--cluster-cidr=%s,%s", clusterCIDRIPv4, clusterCIDRIPv6)) } if !isServerCIDRSet { c.Configuration.K3SArgs = append(c.Configuration.K3SArgs, fmt.Sprintf("--service-cidr=%s,%s", serviceCIDRIPv4, serviceCIDRIPv6)) } case c.isIP4Set(): if !isClusterCIDRSet { c.Configuration.K3SArgs = append(c.Configuration.K3SArgs, "--cluster-cidr="+clusterCIDRIPv4) } if !isServerCIDRSet { c.Configuration.K3SArgs = append(c.Configuration.K3SArgs, "--service-cidr="+serviceCIDRIPv4) } case c.isIP6Set(): if !isClusterCIDRSet { c.Configuration.K3SArgs = append(c.Configuration.K3SArgs, "--cluster-cidr="+clusterCIDRIPv6) } if !isServerCIDRSet { c.Configuration.K3SArgs = append(c.Configuration.K3SArgs, "--service-cidr="+serviceCIDRIPv6) } } } ``` ## First Refactoring Attempt: The Over-Abstraction Trap Before showing the final solution, let's see a common mistake: **over-abstracting booleans**. ### What We Tried (Over-Abstraction ❌) ```go // CIDRPresence - A wrapper that adds NO value type CIDRPresence bool const ( cidrPresent CIDRPresence = true ) func (p CIDRPresence) IsSet() bool { return bool(p) // Just unwraps the bool! } type CIDRConfig struct { ClusterCIDR CIDRPresence // Wrapped bool ServiceCIDR CIDRPresence // Wrapped bool } func (c CIDRConfig) AreBothSet() bool { return c.ClusterCIDR.IsSet() && c.ServiceCIDR.IsSet() } ``` ### Why This Is Over-Abstraction **Problems with CIDRPresence**: 1. ❌ **8 lines of code** for a trivial wrapper 2. ❌ **One method** that just unwraps: `return bool(p)` 3. ❌ **No type safety** - still just a bool underneath 4. ❌ **Not more readable** - compare: - `config.ClusterCIDR.IsSet()` (with wrapper) - `config.ClusterCIDRSet` (with good naming) 5. ❌ **No validation, no logic, no invariants** - pure ceremony 6. ❌ **Increases cognitive load** - one more type to understand **The Honest Question**: Is `config.ClusterCIDR.IsSet()` **significantly** clearer than `config.ClusterCIDRSet`? **Answer**: No! Good naming achieves the same clarity. **The Real Need**: We DO need controlled mutation (only parser should set these values), but we don't need a wrapper type to achieve it. ### The Better Solution: Private Fields Instead of wrapping with `CIDRPresence`, use **private fields with accessor methods**: ```go // ✅ Simple, safe, clear type CIDRConfig struct { clusterCIDRSet bool // Private: can only be set by ParseCIDRConfig serviceCIDRSet bool // Private: can only be set by ParseCIDRConfig } // Read-only accessors func (c CIDRConfig) ClusterCIDRSet() bool { return c.clusterCIDRSet } func (c CIDRConfig) ServiceCIDRSet() bool { return c.serviceCIDRSet } func (c CIDRConfig) AreBothSet() bool { return c.clusterCIDRSet && c.serviceCIDRSet } ``` **Why This Is Better**: - ✅ **4 lines** vs 8 lines for CIDRPresence wrapper - ✅ **Same safety** - compiler enforces that only parser can set values - ✅ **Same readability** - `ClusterCIDRSet()` is just as clear - ✅ **No wrapper ceremony** - fields are what they are: bools - ✅ **Controlled mutation** - private fields can't be set externally **Key Lesson**: Not every primitive needs a type. Use private fields when you need controlled mutation without wrapper overhead. --- ## After Refactoring (Final Solution) ```go // Main function: Now a 7-line story! func (c *Config) alignCIDRArgs() { ipConfig := IPVersionConfig{ IPv4Enabled: c.isIP4Set(), IPv6Enabled: c.isIP6Set(), } k3sArgs := K3SArgs(c.K3SArgs) k3sArgs.AppendCIDRDefaults(ipConfig) c.K3SArgs = []string(k3sArgs) } // ==================== LEAF TYPE 1: K3SArgs ==================== // K3SArgs represents K3S command-line arguments. // Encapsulates ALL argument list operations. // Design choice: Type alias (not struct) allows direct use in JSON configs: // type Config struct { // K3SArgs K3SArgs `json:"k3sArgs,omitempty"` // } type K3SArgs []string // ParseCIDRConfig extracts which CIDRs are already configured. // This is the ONLY place where CIDR flags can be set. func (args K3SArgs) ParseCIDRConfig() CIDRConfig { var config CIDRConfig for _, arg := range args { key, _, found := parseK3SArgument(arg) if !found { continue } switch key { case "--cluster-cidr": config.clusterCIDRSet = true // ✓ Controlled mutation in parser case "--service-cidr": config.serviceCIDRSet = true // ✓ Controlled mutation in parser } } return config } // AppendCIDRDefaults adds missing CIDR arguments based on IP configuration. func (args *K3SArgs) AppendCIDRDefaults(ipConfig IPVersionConfig) { existing := args.ParseCIDRConfig() if existing.AreBothSet() { return // nothing to do } defaults := ipConfig.DefaultCIDRs() if !existing.ClusterCIDRSet() { // ✓ Read-only access via method *args = append(*args, defaults.ClusterCIDRArg()) } if !existing.ServiceCIDRSet() { // ✓ Read-only access via method *args = append(*args, defaults.ServiceCIDRArg()) } } // parseK3SArgument splits a K3S argument into key and value. func parseK3SArgument(arg string) (key, value string, ok bool) { parts := strings.SplitN(arg, "=", 2) if len(parts) != 2 { return "", "", false } return parts[0], parts[1], true } // ==================== LEAF TYPE 2: CIDRConfig ==================== // CIDRConfig represents which CIDR configurations are present. // Uses private fields for controlled mutation - can only be set by ParseCIDRConfig. type CIDRConfig struct { clusterCIDRSet bool // Private: controlled mutation serviceCIDRSet bool // Private: controlled mutation } // ClusterCIDRSet returns true if cluster CIDR is configured. func (c CIDRConfig) ClusterCIDRSet() bool { return c.clusterCIDRSet } // ServiceCIDRSet returns true if service CIDR is configured. func (c CIDRConfig) ServiceCIDRSet() bool { return c.serviceCIDRSet } // AreBothSet returns true if both cluster and service CIDRs are configured. func (c CIDRConfig) AreBothSet() bool { return c.clusterCIDRSet && c.serviceCIDRSet } // ==================== LEAF TYPE 3: IPVersionConfig ==================== // IPVersionConfig describes which IP versions are enabled. type IPVersionConfig struct { IPv4Enabled bool IPv6Enabled bool } func (cfg IPVersionConfig) DefaultCIDRs() DefaultCIDRValues { return DefaultCIDRValues{ ipv4Enabled: cfg.IPv4Enabled, ipv6Enabled: cfg.IPv6Enabled, } } // DefaultCIDRValues generates default CIDR arguments based on IP config. type DefaultCIDRValues struct { ipv4Enabled bool ipv6Enabled bool } func (d DefaultCIDRValues) ClusterCIDRArg() string { return "--cluster-cidr=" + d.clusterCIDRValue() } func (d DefaultCIDRValues) ServiceCIDRArg() string { return "--service-cidr=" + d.serviceCIDRValue() } func (d DefaultCIDRValues) clusterCIDRValue() string { var cidrs []string if d.ipv4Enabled { cidrs = append(cidrs, defaultClusterCIDRIPv4) } if d.ipv6Enabled { cidrs = append(cidrs, defaultClusterCIDRIPv6) } return strings.Join(cidrs, ",") } func (d DefaultCIDRValues) serviceCIDRValue() string { var cidrs []string if d.ipv4Enabled { cidrs = append(cidrs, defaultServiceCIDRIPv4) } if d.ipv6Enabled { cidrs = append(cidrs, defaultServiceCIDRIPv6) } return strings.Join(cidrs, ",") } ``` ## Refactoring Thought Process ### Step 1: Recognize Primitive Obsession - The Root Cause **What's happening**: Function operates on raw `[]string` with manual parsing scattered throughout ```go // Config struct uses primitive type type Config struct { K3SArgs []string `json:"k3sArgs,omitempty"` // Just a slice! } // Parsing logic mixed into business logic for _, arg := range c.K3SArgs { kv := strings.SplitN(arg, "=", 2) // String parsing if len(kv) != 2 { continue } // Validation switch kv[0] { ... } // Business logic } ``` → **Decision**: Extract a `K3SArgs` type alias to encapsulate argument list operations ```go type K3SArgs []string // Type alias, not struct type Config struct { K3SArgs K3SArgs `json:"k3sArgs,omitempty"` // Now has methods! } ``` **Why type alias vs struct?** - ✅ Can use directly in JSON config structs (serializes as array) - ✅ Can convert to/from `[]string` easily: `K3SArgs(slice)` and `[]string(k3sArgs)` - ✅ No wrapper overhead - ✅ Backward compatible with existing JSON configs **Why this matters**: - Once you have a type, you can move ALL operations on that data into methods - Type can be used directly as a config field with JSON tags - Creates a testable boundary - Methods travel with the data everywhere it's used ### Step 2: Identify What Logic Belongs Where **Analysis of the original function**: 1. **Parse existing arguments** → Belongs in `K3SArgs.ParseCIDRConfig()` 2. **Track which CIDRs exist** → Needs domain type: `CIDRConfig` 3. **Determine defaults based on IP version** → Needs config type: `IPVersionConfig` 4. **Generate CIDR strings** → Needs value generator: `DefaultCIDRValues` → **Decision**: Extract 4 different types, each with one responsibility **Why this matters**: Instead of one 60-line function, we get 4 small leaf types that are independently testable. ### Step 3: Replace Boolean Flags with Domain Type **Before**: Two booleans tracking related state ```go var isClusterCIDRSet bool var isServerCIDRSet bool if isClusterCIDRSet && isServerCIDRSet { return } ``` **After**: Domain model with query method ```go type CIDRConfig struct { clusterCIDRSet bool // Private fields serviceCIDRSet bool } func (c CIDRConfig) AreBothSet() bool { return c.clusterCIDRSet && c.serviceCIDRSet } if existing.AreBothSet() { return } ``` → **Why this transformation matters**: - Reads like English: "are both set?" - Encapsulates the logic in one place - Extensible: easy to add DNS CIDR field - Groups related state ### Step 3.5: Recognize Over-Abstraction (Critical Decision!) **Temptation**: Wrap the bool in a type ```go // ❌ Over-abstraction! type CIDRPresence bool func (p CIDRPresence) IsSet() bool { return bool(p) } type CIDRConfig struct { ClusterCIDR CIDRPresence ServiceCIDR CIDRPresence } ``` **Questions to ask**: 1. Does `CIDRPresence` add meaningful methods? → **NO** (just `.IsSet()` which unwraps) 2. Does it enforce invariants? → **NO** (still just a bool) 3. Does it need controlled mutation? → **YES!** (should only be set by parser) 4. Is `.ClusterCIDR.IsSet()` clearer than `.ClusterCIDRSet()`? → **NO!** → **Decision**: Don't create `CIDRPresence` wrapper. Instead, use **private fields** for controlled mutation: ```go // ✅ Better: Private fields + accessor methods type CIDRConfig struct { clusterCIDRSet bool // Private: only parser can set serviceCIDRSet bool } func (c CIDRConfig) ClusterCIDRSet() bool { return c.clusterCIDRSet } func (c CIDRConfig) ServiceCIDRSet() bool { return c.serviceCIDRSet } ``` **Why this matters**: - Achieves same safety (compiler-enforced controlled mutation) - 4 fewer lines than wrapper approach - No ceremonial type wrapping - Just as readable: `ClusterCIDRSet()` vs `ClusterCIDR.IsSet()` **Key lesson**: Not every primitive needs a type. Use private fields when you need controlled mutation without wrapper overhead. ### Step 4: Eliminate Switch Statement Duplication **Problem identified**: Same pattern repeated 3 times ```go case c.isIP4Set() && c.isIP6Set(): if !isClusterCIDRSet { append(..., IPv4+IPv6) } if !isServerCIDRSet { append(..., IPv4+IPv6) } case c.isIP4Set(): if !isClusterCIDRSet { append(..., IPv4) } if !isServerCIDRSet { append(..., IPv4) } case c.isIP6Set(): // Same pattern again! ``` **What differs**: Only the CIDR values (IPv4 vs IPv6 vs both) → **Decision**: Extract value generation into `DefaultCIDRValues` type **Result**: The pattern disappears entirely - replaced by: ```go defaults := ipConfig.DefaultCIDRs() if !existing.ClusterCIDR.IsSet() { *args = append(*args, defaults.ClusterCIDRArg()) } ``` **Why this matters**: Duplication eliminated by separating data selection from flow control. ### Step 5: Storify the Main Function **Goal**: Make it read like a story at ONE abstraction level **Process**: ```go // Step 1: Create configuration object (HIGH LEVEL) ipConfig := IPVersionConfig{ IPv4Enabled: c.isIP4Set(), IPv6Enabled: c.isIP6Set(), } // Step 2: Convert to typed wrapper (HIGH LEVEL) k3sArgs := K3SArgs(c.K3SArgs) // Step 3: Apply business logic (HIGH LEVEL) k3sArgs.AppendCIDRDefaults(ipConfig) // Step 4: Store result (HIGH LEVEL) c.K3SArgs = []string(k3sArgs) ``` **Read it aloud**: "Create IP config, convert args to typed wrapper, append CIDR defaults, store back." → **Result**: All implementation details (parsing, switching, string building) are hidden in leaf types ## Key Improvements ### Architecture * **Fat function became lean orchestrator** - 60 lines → 7 lines * **Created 3 leaf types** - Each handles one concern: - `K3SArgs`: Argument list operations (parsing, appending) - **usable as config field** - `CIDRConfig`: Domain model with **private fields for safety** - `IPVersionConfig` + `DefaultCIDRValues`: CIDR value generation * **Clear separation** - Parsing vs Detection vs Value Generation vs Orchestration * **Type alias pattern** - `K3SArgs` as type alias enables direct use in config structs with JSON serialization * **Avoided over-abstraction** - Rejected `CIDRPresence` wrapper, used private fields instead (4 fewer lines, same safety) ### Readability * **Storified main function** - Reads like: create config → convert → append → store * **Fixed misleading name** - `validateCIDR()` → `alignCIDRArgs()` (now accurately describes mutation) * **Query methods read like English**: ```go if existing.AreBothSet() { return } if !existing.ClusterCIDR.IsSet() { /* ... */ } ``` * **Single abstraction level** - Main function operates entirely at HIGH level ### Testability * **All leaf types testable independently**: ```go // Test argument parsing without Config func TestK3SArgs_ParseCIDRConfig(t *testing.T) { args := K3SArgs{"--cluster-cidr=10.0.0.0/8", "--other-flag=value"} config := args.ParseCIDRConfig() assert.True(t, config.ClusterCIDR.IsSet()) assert.False(t, config.ServiceCIDR.IsSet()) } // Test CIDR value generation without network code func TestDefaultCIDRValues_ClusterCIDRArg(t *testing.T) { values := DefaultCIDRValues{ipv4Enabled: true, ipv6Enabled: true} arg := values.ClusterCIDRArg() assert.Equal(t, "--cluster-cidr=10.42.0.0/16,fd00:42::/56", arg) } // Test domain logic without parsing func TestCIDRConfig_AreBothSet(t *testing.T) { config := CIDRConfig{ ClusterCIDR: cidrPresent, ServiceCIDR: cidrPresent, } assert.True(t, config.AreBothSet()) } ``` * **No mocking needed** - Each type constructed with simple values * **100% coverage achievable** - All logic in leaf types ### Complexity Reduction **Before**: - 60 lines in one function - Cyclomatic complexity: 12 - Cognitive complexity: 18 - 3 nesting levels **After**: - Main function: 7 lines, complexity 1 - Largest helper: 15 lines, complexity 4 - Max nesting: 2 levels - **Most complexity in leaf types** (easily testable) ### Avoiding Over-Abstraction * **Rejected CIDRPresence wrapper** - Recognized it added no value: - Would be 8 lines for a trivial bool wrapper - Only one method: `.IsSet()` that just unwraps the bool - Not more readable than good naming - No validation, no logic, no invariants * **Used private fields instead** - Achieved same safety with less code: - Compiler-enforced controlled mutation - Only parser can set values - 4 fewer lines than wrapper approach * **Key decision**: Compared `config.ClusterCIDR.IsSet()` vs `config.ClusterCIDRSet()` honestly - **Answer**: Good naming is just as clear as method call - **Lesson**: Not every primitive needs a type ## Refactoring Patterns Applied 1. **Replace Primitive with Domain Type (Type Alias Pattern)** → Created `K3SArgs` type alias for `[]string` (usable in config fields) 2. **Extract Multiple Leaf Types** → Created 3 leaf types (`K3SArgs`, `CIDRConfig`, `IPVersionConfig`) instead of one complex function 3. **Storifying** → Main function reads: create config → convert → append → store (all at same abstraction level) 4. **Replace Boolean Flags with Domain Model** → `isClusterCIDRSet, isServerCIDRSet` → `CIDRConfig` with **private fields** and query methods 5. **Eliminate Switch Duplication** → Extracted value generation to `DefaultCIDRValues`, eliminated 3 duplicate cases 6. **Introduce Parameter Object** → Created `IPVersionConfig` to pass related configuration together 7. **Query Method Pattern** → `AreBothSet()`, `ClusterCIDRSet()`, `ServiceCIDRSet()` read like English questions 8. **Avoid Over-Abstraction** → Rejected `CIDRPresence` wrapper, used private fields with accessors for controlled mutation ## The Type Extraction Strategy **Before**: All logic in one place ``` Config.alignCIDRArgs() { // 60 lines of: // - String parsing (strings.SplitN, validation) // - Boolean flag tracking // - Switch statements with duplication // - String building (fmt.Sprintf, string concatenation) // - Slice mutation } ``` **After**: Multiple focused leaf types ``` K3SArgs (LEAF TYPE - no external dependencies) ├─ ParseCIDRConfig() // Parsing logic (juicy!) ├─ AppendCIDRDefaults() // Mutation logic (juicy!) └─ parseK3SArgument() // Helper (juicy!) CIDRConfig (LEAF TYPE - domain model with private fields) ├─ clusterCIDRSet (private bool) ├─ serviceCIDRSet (private bool) ├─ ClusterCIDRSet() // Accessor (read-only) ├─ ServiceCIDRSet() // Accessor (read-only) └─ AreBothSet() // Domain logic (juicy!) Note: No CIDRPresence wrapper! Private fields achieve same safety with less ceremony. IPVersionConfig (LEAF TYPE - configuration) └─ DefaultCIDRs() → DefaultCIDRValues DefaultCIDRValues (LEAF TYPE - value generator) ├─ ClusterCIDRArg() // String building (juicy!) ├─ ServiceCIDRArg() // String building (juicy!) ├─ clusterCIDRValue() // IPv4/IPv6 selection (juicy!) └─ serviceCIDRValue() // IPv4/IPv6 selection (juicy!) Config (ORCHESTRATOR) └─ alignCIDRArgs() // Thin story: 7 lines ``` **Result**: - Main function is 7 lines of pure orchestration - ALL complexity moved to leaf types - Each leaf type achieves 100% unit test coverage - No mocking required for any test ## Linter Metrics **Before**: - Lines: 60 - Cyclomatic complexity: 12 - Cognitive complexity: 18 - Functions: 1 (doing everything) - Testable units: 1 (requires full Config) **After**: - Main function: 7 lines, complexity 1 - Total lines: ~146 (across 5 types + helpers) - Max complexity per function: 4 - Testable units: 9 (all independently testable) - Leaf types: 3 (all with 100% coverage potential) ## Abstraction Balance: Comparison Table | Approach | Total Lines | Types | Readability | Safety | Ceremony | Verdict | |----------|-------------|-------|-------------|--------|----------|---------| | **CIDRPresence wrapper** | ~150 | 6 | Good | Low | High | ❌ Over-abstraction | | **Public bool fields** | ~142 | 5 | Good | Low | Low | ⚠️ Acceptable for small teams | | **Private bool + accessors** | ~146 | 5 | Good | **High** | Low | ✅ **Recommended** | **Why Private Fields Win**: - Only 4 extra lines vs public fields (2 accessor methods) - 4 fewer lines than CIDRPresence wrapper - Compiler-enforced mutation control (can only be set in `ParseCIDRConfig`) - Same readability as public fields - Best safety-to-complexity ratio - No wrapper ceremony ## Remaining Opportunities **What could still be improved** (and why we stopped): ### 1. Why We Rejected CIDRPresence Wrapper ❌ **Could have done**: ```go type CIDRPresence bool func (p CIDRPresence) IsSet() bool { return bool(p) } ``` **Why we didn't**: - ❌ 8 lines for a trivial bool wrapper - ❌ Only one method that just unwraps: `return bool(p)` - ❌ Not more readable: `config.ClusterCIDR.IsSet()` vs `config.ClusterCIDRSet()` - ❌ No validation, no logic, no invariants - ❌ Would add ceremony without benefit **What we did instead**: Private bool fields with accessor methods - ✅ Same safety (compiler-enforced controlled mutation) - ✅ 4 fewer lines - ✅ No wrapper overhead - ✅ Just as readable **Lesson**: **Not every primitive needs a type.** Ask: "Does this wrapper add meaningful logic or just ceremony?" ### 2. Why We Chose Private Fields Over Public Fields **Could have used public fields**: ```go type CIDRConfig struct { ClusterCIDRSet bool // Public ServiceCIDRSet bool // Public } ``` **Why we used private fields**: - ✅ Compiler enforces that only `ParseCIDRConfig` can set values - ✅ Single source of truth for where values come from - ✅ Easy to debug: only one place to check - ✅ Only 4 extra lines (2 accessor methods) - ✅ Public fields would work for small, disciplined teams, but private fields are safer **Lesson**: **Use private fields when mutation should be controlled.** Only 4 lines for compile-time safety. ### 3. DefaultCIDRValues Has Similar Methods **Could extract**: - `clusterCIDRValue()` and `serviceCIDRValue()` are similar - Could extract common pattern with constants as parameters **Why we stopped**: - Only 2 cases - extraction would be premature abstraction - Current code is clear and straightforward - YAGNI principle applies ### 4. K3SArgs Could Support More Operations **Could add**: - `Remove()`, `Update()`, `HasFlag()` methods **Why we stopped**: - YAGNI - only need parsing and appending for now - Add methods when you need them, not before ### 5. IPVersionConfig Is Just Two Bools **Could use enum**: ```go type IPVersion int const ( IPv4Only IPVersion = iota IPv6Only DualStack ) ``` **Why we stopped**: - Two bools are clear and simple enough - Enum would add complexity without clarity benefit - Current code is self-documenting ### 6. Why Type Alias Over Struct for K3SArgs ```go // ❌ Struct would require unwrapping for JSON type K3SArgs struct { args []string } type Config struct { K3SArgs K3SArgs // JSON: {"k3sArgs": {"args": [...]}} } // ✅ Type alias works directly type K3SArgs []string type Config struct { K3SArgs K3SArgs `json:"k3sArgs,omitempty"` // JSON: {"k3sArgs": [...]} } ``` --- ## Key Lessons: When to Stop Refactoring **Good refactoring knows when to stop.** We achieved our goals: - ✅ Main function reads like a story (7 lines) - ✅ All logic extracted to testable leaf types - ✅ No primitive obsession (created `K3SArgs` with real behavior) - ✅ **Avoided over-abstraction** (rejected `CIDRPresence` wrapper) - ✅ Switch duplication eliminated - ✅ Complexity under control - ✅ Controlled mutation via private fields - ✅ Type alias pattern enables clean JSON serialization **The Balance**: ``` Too Simple Sweet Spot Over-Engineering | | | Raw primitives Domain types Types for everything []string K3SArgs CIDRPresence wrapper bool flags CIDRConfig Every bool wrapped (private fields) ``` **Critical Questions Before Creating a Type**: 1. Does it have >1 meaningful method with logic? (Not just unwrapping) 2. Does it enforce invariants or validation? 3. Does it need controlled mutation? (Use private fields, not wrappers) 4. Is the method call **significantly** clearer than good naming? 5. Does it hide complex implementation? **If answers are mostly NO** → Use primitives with good naming (or private fields for safety) Further refactoring would be over-engineering at this point. # Example 3: Dependency Rejection Pattern - Incremental Global Elimination This real-world example shows how to handle `noglobals` linter failures by incrementally pushing global dependencies up the call chain, creating "islands of clean code" that are 100% testable. ## Key Learning: From Global Chaos to Clean Islands **The Problem**: `env.Configs.NATsAddress` accessed throughout codebase (20+ locations) **The Solution**: Reject dependency up one level at a time (bottom-up approach) **The Result**: Clean, testable types with globals only at entry points (2 locations) This pattern is DIFFERENT from other refactorings: - **NOT a one-time fix** - it's an incremental journey - **Start at the bottom** (leaf code) - **Create one clean island at a time** - **Slowly push globals toward main()** - **Pragmatic endpoint** - accept globals at top level ## Why Not Just Refactor Logger? **Important**: Some globals are designed to be global (like `slog.Logger`, `zerolog`). Those are fine! **Problem globals**: Configuration access like: - `env.Configs.NATsAddress` - `env.Configs.DBHost` - `env.Configs.RedisURL` - Any `env.Configs.*` scattered throughout code **Why these are problems**: - Makes code untestable (need to set global state) - Creates hidden dependencies - Impossible to run tests in parallel - Can't swap implementations - Tight coupling to global config struct ## Before Refactoring: Global Chaos ```go // Global config accessed everywhere package env var Configs struct { NATsAddress string DBHost string RedisURL string } // ❌ Deep in the messaging code - globals everywhere package messaging func PublishEvent(event Event) error { // Global accessed here conn, err := nats.Connect(env.Configs.NATsAddress) if err != nil { return fmt.Errorf("connect failed: %w", err) } defer conn.Close() data, err := json.Marshal(event) if err != nil { return err } return conn.Publish(event.Topic, data) } func PublishBatch(events []Event) error { // Global accessed here too conn, err := nats.Connect(env.Configs.NATsAddress) if err != nil { return fmt.Errorf("connect failed: %w", err) } defer conn.Close() for _, event := range events { // ... publishing logic } return nil } // ❌ In the order service - more global access package order func ProcessOrder(orderID string) error { // More globals db := connectDB(env.Configs.DBHost) defer db.Close() // Even more globals err := messaging.PublishEvent(orderCreatedEvent) // ... } // ❌ Testing is a nightmare func TestPublishEvent(t *testing.T) { // Must set global state! env.Configs.NATsAddress = "nats://test:4222" // Tests can't run in parallel // Can't easily test with different addresses // Global state leaks between tests } ``` **Problems Identified**: - `env.Configs.NATsAddress` used in 12 places - `env.Configs.DBHost` used in 8 places - Testing requires global state mutation - Parallel tests impossible - Hidden dependencies everywhere ## Step-by-Step Refactoring ### Step 1: Analyze Dependency Chain ``` DEPENDENCY MAP: main() └─ HTTP handlers (entry points) ├─ OrderService.ProcessOrder() [USES env.Configs.DBHost] │ └─ messaging.PublishEvent() [USES env.Configs.NATsAddress] │ └─ messaging.PublishBatch() [USES env.Configs.NATsAddress] └─ UserService.CreateUser() └─ messaging.PublishEvent() [USES env.Configs.NATsAddress] DEEPEST USAGE: messaging.PublishEvent/PublishBatch (furthest from main) START HERE: Extract messaging types first ``` ### Step 2: Create First Clean Island (Bottom Level) **Extract NATSClient - the deepest leaf**: ```go // ✅ Clean type with injected dependency type NATSClient struct { natsAddress string // Injected, not global! } func NewNATSClient(natsAddress string) *NATSClient { return &NATSClient{natsAddress: natsAddress} } func (c *NATSClient) PublishEvent(event Event) error { // Uses injected value, not global conn, err := nats.Connect(c.natsAddress) if err != nil { return fmt.Errorf("connect failed: %w", err) } defer conn.Close() data, err := json.Marshal(event) if err != nil { return err } return conn.Publish(event.Topic, data) } func (c *NATSClient) PublishBatch(events []Event) error { conn, err := nats.Connect(c.natsAddress) if err != nil { return fmt.Errorf("connect failed: %w", err) } defer conn.Close() for _, event := range events { // ... publishing logic } return nil } // ✅ Now testable without globals! func TestNATSClient_PublishEvent(t *testing.T) { // Start local test NATS server testNATS := startTestNATS(t) defer testNATS.Stop() // Create client with test address - NO GLOBALS! client := NewNATSClient(testNATS.URL()) // Test with real implementation err := client.PublishEvent(testEvent) assert.NoError(t, err) // Can run in parallel! // No global state needed! } ``` **Island #1 Created**: `NATSClient` is now 100% testable without global dependencies. ### Step 3: Push Global Up One Level **Now the callers (one level up) need updating**: ```go // ❌ Before - OrderService accessed globals package order func ProcessOrder(orderID string) error { db := connectDB(env.Configs.DBHost) defer db.Close() // This used to access env.Configs.NATsAddress internally err := messaging.PublishEvent(orderCreatedEvent) return err } // ✅ After - OrderService gets clean dependency package order type OrderService struct { dbHost string // Injected natsClient *NATSClient // Clean dependency! } func NewOrderService(dbHost string, natsClient *NATSClient) *OrderService { return &OrderService{ dbHost: dbHost, natsClient: natsClient, } } func (s *OrderService) ProcessOrder(orderID string) error { db := connectDB(s.dbHost) defer db.Close() // Use clean dependency - no globals err := s.natsClient.PublishEvent(orderCreatedEvent) return err } // ✅ Now OrderService is testable! func TestOrderService_ProcessOrder(t *testing.T) { testNATS := startTestNATS(t) defer testNATS.Stop() natsClient := NewNATSClient(testNATS.URL()) service := NewOrderService("localhost:5432", natsClient) err := service.ProcessOrder("order-123") assert.NoError(t, err) } ``` **Island #2 Created**: `OrderService` is now testable with clean dependencies. ### Step 4: Continue Up the Chain to Entry Points **Finally, reach the HTTP handlers (entry points)**: ```go // ✅ HTTP handler - global accessed only here (top level) package api type OrderHandler struct { orderService *OrderService } func SetupOrderHandler() *OrderHandler { // Global accessed only at entry point! natsClient := NewNATSClient(env.Configs.NATsAddress) orderService := NewOrderService(env.Configs.DBHost, natsClient) return &OrderHandler{ orderService: orderService, } } func (h *OrderHandler) HandleCreateOrder(w http.ResponseWriter, r *http.Request) { // All clean code from here down - no globals! err := h.orderService.ProcessOrder(orderID) // ... } ``` **Final state**: - Globals only in `SetupOrderHandler()` (acceptable!) - Everything below is clean, testable code - 2 global accesses (was 20+) ## Complete Before/After Comparison ### Before: Globals Everywhere (20+ accesses) ``` main() └─ handlers [env.Configs access] ├─ OrderService [env.Configs access] │ └─ messaging funcs [env.Configs access] └─ UserService [env.Configs access] └─ messaging funcs [env.Configs access] Testing: IMPOSSIBLE without global state mutation Parallel tests: NO ``` ### After: Globals Only at Top (2 accesses) ``` main() └─ handlers [env.Configs access - 2 locations ONLY] ├─ OrderService [clean - injected deps] │ └─ NATSClient [clean - injected deps] └─ UserService [clean - injected deps] └─ NATSClient [clean - injected deps] Testing: EASY - inject test values Parallel tests: YES Islands of clean code: 3 types (NATSClient, OrderService, UserService) ``` ## Testing Benefits Demonstrated ### Before: Global State Nightmare ```go func TestPublishEvent(t *testing.T) { // ❌ Must mutate global originalAddr := env.Configs.NATsAddress env.Configs.NATsAddress = "nats://test:4222" defer func() { env.Configs.NATsAddress = originalAddr // Restore }() // ❌ Can't run in parallel - global state shared // ❌ Tests can interfere with each other // ❌ Hard to test multiple scenarios } ``` ### After: Clean Dependency Injection ```go func TestNATSClient_PublishEvent(t *testing.T) { t.Parallel() // ✅ Parallel execution! // ✅ No global state needed testNATS := startTestNATS(t) defer testNATS.Stop() // ✅ Clean injection client := NewNATSClient(testNATS.URL()) // ✅ Easy to test edge cases tests := []struct { name string address string wantErr bool }{ {"valid", testNATS.URL(), false}, {"invalid", "nats://nonexistent:4222", true}, {"empty", "", true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // Each test gets clean client - no interference! c := NewNATSClient(tt.address) err := c.PublishEvent(testEvent) // ... assertions }) } } ``` ## Metrics ### Before Refactoring ``` Global Accesses: - env.Configs.NATsAddress: 12 locations (deep in code) - env.Configs.DBHost: 8 locations (deep in code) Total: 20 global accesses scattered Testability: - Testable types: 0 (all depend on globals) - Parallel tests: Impossible - Test complexity: High (global state setup/teardown) Code Quality: - Hidden dependencies: Everywhere - Coupling: Tight (everything to env.Configs) - Flexibility: Low (can't swap implementations) ``` ### After Refactoring ``` Global Accesses: - Entry points only: 2 locations (SetupOrderHandler, SetupUserHandler) - Deep code: 0 global accesses Testability: - Clean testable types: 3 islands (NATSClient, OrderService, UserService) - Test coverage: 100% on clean types - Parallel tests: Fully supported - Test complexity: Low (simple dependency injection) Code Quality: - Hidden dependencies: Eliminated - Coupling: Loose (injected dependencies) - Flexibility: High (easy to swap implementations) Improvement: - 90% reduction in global access points (20 → 2) - 3 new testable types created - 100% of business logic now testable ``` ## Key Lessons ### 1. Incremental is Better Than Perfect **DON'T**: ```go // ❌ Try to eliminate ALL globals at once // This is overwhelming and error-prone ``` **DO**: ```go // ✅ One type at a time, one level at a time // Step 1: Extract NATSClient (week 1) // Step 2: Extract OrderService (week 2) // Step 3: Continue gradually ``` ### 2. Bottom-Up Approach Works **Start at the leaf** (furthest from main): 1. Identify deepest usage (`messaging.PublishEvent`) 2. Extract clean type (`NATSClient`) 3. Push global up one level (`OrderService`) 4. Repeat until reaching entry points ### 3. Pragmatic Stopping Point **Acceptable to have globals at**: - `main()` function - HTTP handler setup - Application initialization - Top-level factory functions **NOT acceptable deep in**: - Business logic types - Data access types - Service layer - Utility functions ### 4. Islands of Clean Code Each extracted type is an "island": - **Fully testable** in isolation - **No global dependencies** - **Explicit dependencies** via constructor - **Can be tested in parallel** - **Easy to understand** (dependencies visible) ### 5. Real Testability **Before**: "We have 80% test coverage" (But all tests depend on global state mutation) **After**: "We have 95% test coverage" (All tests use clean dependency injection, run in parallel) Real testability means: - ✅ No global state manipulation - ✅ Tests run in parallel - ✅ Tests are isolated - ✅ Easy to test edge cases - ✅ Can use real implementations (no mocking) ## When to Apply This Pattern **Apply dependency rejection when**: - ✅ `noglobals` linter fails - ✅ Global config accessed throughout codebase - ✅ Testing requires global state mutation - ✅ Parallel tests fail due to shared state - ✅ Code has hidden dependencies **DON'T apply to**: - ❌ Loggers (`slog`, `zerolog`) - designed to be global - ❌ Constants/enums - these are fine as globals - ❌ Read-only singletons with no state ## Refactoring Progression Example ``` Iteration 1 (Week 1): ├─ Extract NATSClient ├─ Global accesses: 20 → 14 └─ Testable types: 1 Iteration 2 (Week 2): ├─ Extract OrderService ├─ Global accesses: 14 → 8 └─ Testable types: 2 Iteration 3 (Week 3): ├─ Extract UserService ├─ Global accesses: 8 → 4 └─ Testable types: 3 Iteration 4 (Week 4): ├─ Push to handlers ├─ Global accesses: 4 → 2 └─ Mission accomplished! ✅ ``` **Note**: Each iteration is a working, tested, deployable state. No "big bang" refactoring needed. ## Conclusion Dependency rejection is about **gradual improvement**, not perfection: - Start at the bottom (leaf code) - Create one clean island at a time - Push globals up one level per iteration - Stop when globals are only at entry points - Every step improves testability **The goal isn't zero globals** - it's **globals only where they belong** (entry points), with all business logic cleanly injectable and testable.