O Open Closed Principle OCP · Quick recall Q&A
2 min readQuick recall Q&A
Use composition, custom hooks with dependency injection, and strategy patterns. Pass dependencies as props or via context so components depend on abstractions, not concrete implementations.
Large switch statements or if-else chains that grow with every new feature. For example, a form validator with a giant switch for field types, or a component that checks if (type === 'stripe') vs if (type === 'paypal') instead of using polymorphism.
Feature flags can select between implementations without modifying code. Use dependency injection to register both implementations and toggle via configuration:
const processor = useFeatureFlag('crypto-payments')
? new CryptoProcessor()
: new StripeProcessor();Don't over-abstract. Only introduce interfaces when you have 2+ implementations or anticipate variation. A single validator doesn't need an interface until you add a second one.
Export component composition APIs that allow extension. For example, a <Table> component that accepts custom <Column> renderers allows users to add new column types without modifying the library.
Version APIs instead of changing contracts. Add new endpoints (/api/v2/users) or optional fields while keeping existing behavior untouched to avoid breaking frontend clients.
TypeScript interfaces enforce contracts, ESLint rules can warn on large switch statements, and architectural tests can verify dependencies. Code reviews should catch growing conditionals.
Custom hooks encapsulate behavior behind interfaces. Create usePayment(processor: PaymentProcessor) instead of useStripePayment(). New processors extend via new implementations, not code changes.
Define theme interfaces and inject implementations:
interface Theme {
colors: ColorPalette;
spacing: SpacingScale;
}
function Button({ theme }: { theme: Theme }) {
return <button style={{ color: theme.colors.primary }}>Click</button>;
}
Add new themes by implementing the interface without touching Button.
Yes, if behavior is data-driven. For example, a form builder that reads field configurations from JSON—adding new field types is configuration, not code modification. Ensure validation guards config changes.
Plugins implement known interfaces (e.g., Monaco editor extensions, VS Code extensions, WordPress blocks). The host application never changes; you register new plugins via a plugin API.
Use middleware for cross-cutting concerns (logging, analytics, error tracking). Each middleware is independent and can be added without modifying existing middleware or reducers:
const store = createStore(
reducer,
applyMiddleware(logger, analytics, errorTracker, newMiddleware)
);Use render props, children as function, or slots pattern:
<DataTable
columns={columns}
renderRow={(row) => <CustomRow data={row} />} // Extend rendering
/>
Users extend behavior by providing custom renderers without modifying DataTable.