www.dennisokeeffe.com
Composition Over Inheritance
We will be covering a few topics where each is ramping up into the next: 1. The prerequisites - Code volume - Control flow - State management 2. The principles - Composition over inheritance - Parse, don't validate - Never throw errors - Metadata - Define your source of truth - Let controllers tell you everything - Don't emulate network infrastructure - Don't let AI take the driver's seat - Generate as much code as possible - Write to refactor programmatically - Don't go overkill on abstraction layers … Over-bloat and unnecessary abstractions may be easier to imagine. If you're working in the codebase where you need to make jumps to ten different definitions in order to understand the inheritance chain or follow the path of the business logic, then you have probably over-engineered the shit out of it. Principles like "composition over inheritance" and "parse, don't validate" can help mitigate volume creep (which I touch on in their own section), but there are some general guiding principles that I recommend to get around this: … 1. Unpredictable Behavior: When state can be modified from multiple places without clear patterns, applications become unpredictable. Developers can't easily reason about what will happen when code executes. 2. Debugging Nightmares: Without clear state flows, finding the root cause of bugs becomes extremely difficult. A bug might manifest in one component but originate from state modifications elsewhere. 3. Technical Debt Accumulation: Poor state management compounds over time through things like duplicated state, stale state and side-effects. 4. Readability and Maintainability Issues: New developers struggle to understand applications. 5. Performance Problems: Unnecessary re-renders, Memory leaks, Network request redundancy. Although this post won't spend too much time on state management, it is also partly related to these topics: … In the above case, we are throwing errors as stand-ins for what could be handled as expected errors. A non-exhaustive list of problems with this: … . A developer cannot grok from our types what can go wrong in an expected way. In my experience as well, this approach also doesn't really happen in practice. Not all thrown errors are caught and managed correctly, so you end up with hard-to-follow try-catch behavior littered throughout implementation. **Do**: … . 2. There are no try-catch clauses. In the case where an error is thrown from something **unexpected**, we consider this a**defect** and should have systems in place to capture that error and inform the developers (not shown here). 3. Our controller can have an easier time managing responses at the boundary, while our developers working on this can learn a lot about this endpoint and possible responses without diving into the business logic. If you look at the `Data` and expected error classes, you'll known the `_tag` property (which I've adopted from EffectTS). I'll talk more to this on the metadata section. I should finish here by saying that "never" here is a bit strong. I've recently heard an engineering manager use the quote "use exceptions for exceptional circumstances", and I find that to be a useful quote around throwing errors in TypeScript. Do so sparingly and with good reason.
Related Pain Points3件
Unpredictable behavior from uncontrolled state modifications
7When application state can be modified from multiple places without clear patterns, developers cannot easily reason about code execution. Bugs manifest in unexpected components, making debugging extremely difficult and technical debt accumulates through duplicated and stale state.
Over-engineering and excessive abstraction layers in codebases
6Developers create unnecessarily complex inheritance chains and abstraction layers that make code difficult to understand. Following a single business logic path requires jumping between ten or more different definitions, making the codebase hard to maintain and reason about.
Error handling complexity with thrown exceptions as control flow
5Using thrown errors as stand-ins for expected error handling makes types non-exhaustive and unclear about what can go wrong. Try-catch blocks are scattered throughout implementations and not all thrown errors are caught correctly, leading to hard-to-follow error management patterns.