(Another Thaura Work product. Again, haven’t checked, but lots of good ideas in here.)
New ways to use old structures to help LLMs write better code. Techniques to reduce token use, reduce context size, reduce errors.
Adapting Traditional OOP Style
Classic object-oriented design β inheritance hierarchies, polymorphic dispatch, encapsulated state β predates LLMs by decades. Much of what made OOP ergonomic for humans (deep class trees, implicit constructor injection, framework-managed lifecycles) introduces exactly the kind of implicit decisions that hurt one-shot generation. The good news: you can keep the spirit of OOP while stripping out the parts that confuse a model.
Inheritance β Composition + Interfaces
Deep extends chains force the model to track overridden methods across multiple files, guess which superclass field is shadowing which, and infer initialization order. Adaptation:
- Prefer interfaces / traits / protocols over base classes. A flat
interface PaymentProcessor { process(order: Order): Result }gives the model a complete contract in one place. No diamond problems, no super-call ordering to reason about. - Use composition for shared behavior. Mixins, embedded structs, or trait implementations (
impl Display for MyType) are self-contained blocks the model can generate without knowing the rest of the hierarchy. - If you must inherit, keep depth β€ 2. One concrete base + one leaf is manageable; three levels with abstract intermediates start to exceed what the model tracks reliably in a single pass.
Encapsulation β Explicit Data Structures
Private fields with getters/setters hide data flow behind method calls the model must predict. Adaptation:
- Expose plain data structures (structs, records, frozen objects) at boundaries. The model sees the shape directly instead of inferring it from accessor pairs.
- Reserve methods for behavior, not for field access. If a “method” just returns a field, make the field public (or readonly) and delete the getter. Fewer indirection layers means fewer places the model can invent a nonexistent accessor.
- Make mutability intentional and visible. A struct with all-public fields communicates “data container.” A class with private state and mutating methods signals “behavior owner.” Mixing both in one type forces the model to guess the invariant.
Polymorphism β Tagged Unions + Pattern Matching
Runtime dynamic dispatch (obj.handle() where the actual type depends on construction site) requires the model to enumerate every possible subclass. Adaptation:
- Replace runtime polymorphism with sum types + exhaustive match wherever the set of cases is closed.
enum Shape { Circle(f64), Rect(f64, f64) }matched in one function is unambiguous; aShapeinterface with ten subclasses scattered across modules is not. - For open-ended extensibility (plugin systems, event handlers), use an explicit registry or visitor pattern rather than relying on the model to discover all overrides. The registry is a single lookup table the model can read.
- Sealed hierarchies beat open ones. If the compiler enforces exhaustiveness (Kotlin
sealed, Scala sealed, TypeScript discriminated unions), the model knows the case list is complete. Open hierarchies invite phantom branches.
Dependency Injection β Explicit Constructor Parameters
Frameworks that inject dependencies via annotations, reflection, or container magic hide the dependency graph. The model can’t see what a class actually needs until it reads the DI configuration. Adaptation:
- Pass dependencies through constructors explicitly.
fn new(db: &Pool, cache: &Cache)is visible at the call site.@Injectannotations require reading a separate wiring file the model may not have in context. - Keep constructor arity low (β€ 4). Beyond that, bundle related deps into a config/options struct. Long parameter lists are where models drop arguments or swap types.
- Avoid builder-pattern fluency for core types. Fluent builders (
new Widget().red().big().build()) scatter construction logic across chained calls. A single constructor call with a struct literal is easier to generate correctly.
Framework Magic β Plain Entry Points
Spring beans, Rails controllers, Angular decorators, and similar frameworks interpose lifecycle hooks, serialization, and routing logic between your code and its observable behavior. Adaptation:
- Isolate framework glue in thin adapters. Keep business logic in plain functions/classes with no framework annotations. The adapter translates HTTP request β function call β response. The model generates the interesting part (logic) without needing to satisfy framework constraints simultaneously.
- Prefer functional entry points for pure computation. A controller that calls
calculate_total(items, discount_code)is testable and generatable independently of the web framework. Embedding that calculation inside a decorated class method couples two concerns the model must get right at once. - Document the framework contract in the prompt if unavoidable. If the target code must be a Spring
@Serviceor a React component, include one minimal working example in the prompt. One concrete exemplar reduces hallucination more than any amount of prose description.
State Machines β Enum-Driven Flows
Implicit state held in boolean flags (isLoading, hasError, isSubmitted) creates combinatorial explosion the model struggles to navigate. Adaptation:
- Model explicit states as enums.
enum CheckoutState { Idle, FillingCart, ValidatingPayment, Processing, Complete(Error?) }makes invalid transitions impossible and gives the model a finite, named space to work in. - Transition functions take
(state, event) β new_state. Pure transition functions are trivially verifiable and easy to generate. Mutating a stateful object’s internal booleans is not. - Guard clauses over nested conditionals.
match state { Idle => ..., Processing => ... }beats four levels ofif (flag1 && !flag2 || flag3)that the model must untangle.
Summary Checklist
| Classic OOP habit | LLM-friendly adaptation | Why it helps |
|---|---|---|
| Deep inheritance tree | Interface + composition, depth β€ 2 | Flat contracts, no shadow tracking |
| Private fields + getters | Public/frozen data structs | Visible shape, no invented accessors |
| Runtime polymorphism | Sum types + exhaustive match | Closed case set, compiler-checked |
| Annotation-based DI | Explicit constructor params | Dependency graph visible at call site |
| Builder pattern | Single constructor + options struct | One expression, not a chain |
| Framework-decorated logic | Thin adapter + pure core function | Separates concerns the model handles independently |
| Boolean-flag state | Enum-driven state machine | Finite named states, pure transitions |
The overarching principle is the same as the rest of this article: remove implicit decisions. Traditional OOP optimizes for human maintainability in large teams over years β hiding internals, deferring binding, distributing behavior across a hierarchy. LLM one-shot generation optimizes for a different constraint: everything relevant must be visible, local, and unambiguous within a single context window. You don’t abandon OOP thinking; you flatten it so the model doesn’t have to reconstruct the hidden architecture from fragments.