Loading learning content...
The inheritance vs composition debate is often framed as a binary choice. In reality, sophisticated designs frequently combine both techniques, using each where it excels. The question isn't always "inheritance OR composition?" but rather "how can we use inheritance AND composition together effectively?"\n\nHybrid approaches leverage inheritance for type relationships and shared implementation while using composition for flexible behavior configuration. Understanding these patterns enables you to craft designs that are simultaneously expressive, flexible, and maintainable.
By the end of this page, you will understand five hybrid approaches that combine inheritance and composition effectively. You'll see how these patterns appear in real frameworks and learn to recognize when a hybrid approach provides the optimal design.
The most common hybrid pattern uses inheritance to define type structure (what an object IS) while using composition to define behavior (what an object DOES). This separates stable identity from variable behavior.\n\nThe Pattern:\n\n- A class hierarchy defines the fundamental types and their relationships\n- Each type contains composed components that provide configurable behavior\n- The hierarchy is stable; behavior variation happens through component swapping\n- Inheritance provides polymorphism; composition provides flexibility
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136
// Hybrid: Inheritance for Structure, Composition for Behavior // STRUCTURE: Class hierarchy defines what things ARE// This hierarchy is stable - the type relationships are permanent abstract class Vehicle { protected engine: Engine; // Composed behavior protected transmission: Transmission; protected fuelSystem: FuelSystem; constructor( public readonly vin: string, public readonly make: string, public readonly model: string ) {} // Template method using composed components start(): void { this.fuelSystem.prime(); this.engine.start(); this.transmission.engage(); } // Abstract - subclasses define structural specifics abstract getAxleCount(): number; abstract getPassengerCapacity(): number; // Behavior delegation accelerate(): void { this.engine.increaseThrottle(); this.transmission.optimizeGear(); } // Configure composed behavior upgradeEngine(engine: Engine): void { this.engine = engine; }} class Car extends Vehicle { constructor( vin: string, make: string, model: string, engine: Engine, transmission: Transmission, fuelSystem: FuelSystem ) { super(vin, make, model); this.engine = engine; this.transmission = transmission; this.fuelSystem = fuelSystem; } getAxleCount(): number { return 2; } getPassengerCapacity(): number { return 5; }} class Motorcycle extends Vehicle { getAxleCount(): number { return 1; } getPassengerCapacity(): number { return 2; }} class Truck extends Vehicle { constructor( vin: string, make: string, model: string, engine: Engine, transmission: Transmission, fuelSystem: FuelSystem, private readonly towingCapacity: number ) { super(vin, make, model); this.engine = engine; this.transmission = transmission; this.fuelSystem = fuelSystem; } getAxleCount(): number { return 2; } // Or more for big rigs getPassengerCapacity(): number { return 3; } getTowingCapacity(): number { return this.towingCapacity; }} // BEHAVIOR: Composed components define what things DO// These can vary independently and be swapped interface Engine { start(): void; stop(): void; increaseThrottle(): void; decreaseThrottle(): void; getHorsepower(): number;} class GasolineEngine implements Engine { constructor(private horsepower: number) {} start() { console.log('Gasoline engine starting...'); } stop() { console.log('Gasoline engine stopping...'); } increaseThrottle() { /* ... */ } decreaseThrottle() { /* ... */ } getHorsepower() { return this.horsepower; }} class ElectricMotor implements Engine { constructor(private horsepower: number) {} start() { console.log('Electric motor ready instantly'); } stop() { console.log('Electric motor off'); } increaseThrottle() { /* instant torque */ } decreaseThrottle() { /* regenerative braking */ } getHorsepower() { return this.horsepower; }} class HybridEngine implements Engine { constructor( private gasEngine: GasolineEngine, private electricMotor: ElectricMotor ) {} // Delegates to both, managing transition start() { this.electricMotor.start(); } // ...} // Usage: Combine structure (inheritance) with behavior (composition)const sportsCar = new Car( 'VIN123', 'Ferrari', 'F8', new GasolineEngine(710), // Powerful gas engine new AutomaticTransmission(8), // 8-speed auto new DirectInjectionFuel()); const commuter = new Car( 'VIN456', 'Tesla', 'Model 3', new ElectricMotor(450), // Electric motor new SingleSpeedTransmission(), // EVs don't need multi-gear new NullFuelSystem() // No fuel system); // Same Car type, completely different behavior through composition!Why This Hybrid Works:\n\n1. Stable structure: The Vehicle → Car/Motorcycle/Truck hierarchy captures genuine type relationships. A Car IS-A Vehicle—this won't change.\n\n2. Variable behavior: How a vehicle accelerates, sounds, or handles depends on components. Gas vs electric, manual vs automatic—these vary independently of vehicle type.\n\n3. Polymorphism preserved: Code can work with any Vehicle. Fleet management sees Vehicles; maintenance sees component types.\n\n4. Configuration flexibility: The same Car class can represent a sports car or an economy sedan through different component combinations.\n\n5. Evolution path: Add new engine types without touching the Vehicle hierarchy. Add new vehicle types that use existing engines.
Use this hybrid when you have genuine type categories (IS-A relationships that pass the Liskov test) but behavioral variations within those categories. The hierarchy captures WHAT things are; composition captures HOW they behave.
This pattern uses an abstract base class that requires strategy components to be injected. The base class provides shared infrastructure while strategies provide the variable behavior.\n\nThe Pattern:\n\n- Abstract base class defines the algorithm skeleton and shared code\n- Constructor requires strategy interfaces to be provided\n- Concrete subclasses may provide default strategies or require them\n- Strategies can be swapped even after subclass instantiation
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158
// Hybrid: Abstract Base Class with Composed Strategies // Strategy interfaces for variable behaviorpublic interface ContentParser { Document parse(InputStream input) throws ParseException; String getContentType();} public interface ContentValidator { ValidationResult validate(Document document);} public interface ContentTransformer { Document transform(Document document);} public interface ContentWriter { void write(Document document, OutputStream output) throws IOException;} // Abstract base class requires strategies via constructorpublic abstract class DocumentProcessor { protected final ContentParser parser; protected final ContentValidator validator; protected final ContentTransformer transformer; protected final ContentWriter writer; protected final Logger logger; // Constructor REQUIRES strategy implementations protected DocumentProcessor( ContentParser parser, ContentValidator validator, ContentTransformer transformer, ContentWriter writer) { this.parser = Objects.requireNonNull(parser); this.validator = Objects.requireNonNull(validator); this.transformer = Objects.requireNonNull(transformer); this.writer = Objects.requireNonNull(writer); this.logger = LoggerFactory.getLogger(getClass()); } // Template method using strategies public final void process(InputStream input, OutputStream output) throws ProcessingException { try { logger.info("Starting document processing"); // Parse (delegated to strategy) Document document = parser.parse(input); logger.debug("Parsed document: {}", document.getId()); // Validate (delegated to strategy) ValidationResult validation = validator.validate(document); if (!validation.isValid()) { handleValidationFailure(validation); // Extension point return; } // Pre-processing hook (subclass extension point) preProcess(document); // Transform (delegated to strategy) Document transformed = transformer.transform(document); // Post-processing hook (subclass extension point) postProcess(transformed); // Write (delegated to strategy) writer.write(transformed, output); logger.info("Document processing complete"); } catch (Exception e) { logger.error("Processing failed", e); throw new ProcessingException("Document processing failed", e); } } // Hooks for subclass customization (optional override) protected void handleValidationFailure(ValidationResult result) { throw new ValidationException(result.getErrors()); } protected void preProcess(Document document) { // Default: no-op } protected void postProcess(Document document) { // Default: no-op } // Abstract - subclasses must define public abstract String getProcessorName();} // Concrete subclass provides defaults and customizationpublic class XmlToJsonProcessor extends DocumentProcessor { public XmlToJsonProcessor() { // Provides sensible defaults for this processor type super( new XmlParser(), new XmlSchemaValidator(), new XmlToJsonTransformer(), new JsonWriter() ); } // Custom constructor for specific strategies public XmlToJsonProcessor(ContentValidator customValidator) { super( new XmlParser(), customValidator, // Custom validation new XmlToJsonTransformer(), new JsonWriter() ); } @Override public String getProcessorName() { return "XML-to-JSON Processor"; } @Override protected void preProcess(Document document) { // XML-specific pre-processing removeXmlDeclaration(document); normalizeNamespaces(document); }} // Another subclass with different defaultspublic class CsvToExcelProcessor extends DocumentProcessor { public CsvToExcelProcessor(ContentValidator validator) { super( new CsvParser(), validator, new CsvToExcelTransformer(), new ExcelWriter() ); } @Override public String getProcessorName() { return "CSV-to-Excel Processor"; } @Override protected void postProcess(Document document) { // Apply Excel-specific formatting autoSizeColumns(document); applyHeaderStyles(document); }}Benefits of This Hybrid:\n\n1. Shared infrastructure: The base class provides logging, error handling, the processing pipeline—code that would otherwise be duplicated.\n\n2. Forced composition: By requiring strategies in the constructor, the pattern ensures flexibility and testability from the start.\n\n3. Subclass specialization: Subclasses can provide sensible defaults, add hooks, and customize for their domain.\n\n4. Open for extension: Add new processors (subclasses) and new strategies independently. Neither depends on the other's hierarchy.\n\n5. Testing: Test the base class with mock strategies. Test strategies independently. Test subclasses with mock strategies.
This is a common pattern in framework design. The framework provides abstract base classes with strategy injection points. Framework users extend the base classes and provide strategies appropriate for their domain.
This pattern uses inheritance only at the interface level to establish type contracts, while all implementations use pure composition. This provides polymorphism without implementation coupling.\n\nThe Pattern:\n\n- Define interface hierarchies that express type relationships\n- Concrete classes implement interfaces but don't extend other classes\n- Implementation reuse happens through composition and delegation\n- Interface inheritance provides type polymorphism; composition provides code reuse
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176
// Hybrid: Interface Inheritance + Implementation Composition // INTERFACE HIERARCHY - Inheritance at the type level// Establishes contracts and enables polymorphism interface Readable { read(): Buffer; isReadable(): boolean;} interface Writable { write(data: Buffer): void; isWritable(): boolean;} // Interface inheritance - extends the contractinterface ReadWritable extends Readable, Writable { // Combines both contracts} interface Seekable { seek(position: number): void; getPosition(): number;} // More specific interface through inheritanceinterface RandomAccessFile extends ReadWritable, Seekable { getLength(): number; truncate(length: number): void;} // IMPLEMENTATION - Pure composition, no class inheritance // Reusable components for implementationclass ReadBuffer { private buffer: Buffer; private position: number = 0; constructor(size: number) { this.buffer = Buffer.alloc(size); } read(count: number): Buffer { const data = this.buffer.slice(this.position, this.position + count); this.position += count; return data; } hasData(): boolean { return this.position < this.buffer.length; }} class WriteBuffer { private buffer: Buffer; private position: number = 0; constructor(size: number) { this.buffer = Buffer.alloc(size); } write(data: Buffer): void { data.copy(this.buffer, this.position); this.position += data.length; } hasSpace(): boolean { return this.position < this.buffer.length; } flush(): Buffer { return this.buffer.slice(0, this.position); }} class FileHandle { constructor(private fd: number) {} readAt(position: number, length: number): Buffer { /* ... */ } writeAt(position: number, data: Buffer): void { /* ... */ } getLength(): number { /* ... */ } close(): void { /* ... */ }} // Concrete implementations compose the pieces class BufferedFileReader implements Readable { private readBuffer: ReadBuffer; private fileHandle: FileHandle; constructor(path: string, bufferSize: number = 8192) { this.fileHandle = new FileHandle(/* open file */); this.readBuffer = new ReadBuffer(bufferSize); } read(): Buffer { return this.readBuffer.read(1024); } isReadable(): boolean { return this.readBuffer.hasData(); }} class BufferedFileWriter implements Writable { private writeBuffer: WriteBuffer; private fileHandle: FileHandle; constructor(path: string, bufferSize: number = 8192) { this.fileHandle = new FileHandle(/* open file */); this.writeBuffer = new WriteBuffer(bufferSize); } write(data: Buffer): void { this.writeBuffer.write(data); } isWritable(): boolean { return this.writeBuffer.hasSpace(); }} // Implements complex interface through composition of simpler partsclass RandomAccessFileImpl implements RandomAccessFile { private fileHandle: FileHandle; private position: number = 0; private readBuffer: ReadBuffer; private writeBuffer: WriteBuffer; constructor(path: string) { this.fileHandle = new FileHandle(/* open file */); this.readBuffer = new ReadBuffer(4096); this.writeBuffer = new WriteBuffer(4096); } // Readable implementation read(): Buffer { const data = this.fileHandle.readAt(this.position, 1024); this.position += data.length; return data; } isReadable(): boolean { return this.position < this.getLength(); } // Writable implementation write(data: Buffer): void { this.fileHandle.writeAt(this.position, data); this.position += data.length; } isWritable(): boolean { return true; } // Seekable implementation seek(position: number): void { this.position = position; } getPosition(): number { return this.position; } // RandomAccessFile specific getLength(): number { return this.fileHandle.getLength(); } truncate(length: number): void { /* ... */ }} // Client code uses interface types - polymorphic!function copyStream(source: Readable, destination: Writable): void { while (source.isReadable()) { const data = source.read(); destination.write(data); }} // Works with any Readable/Writable combinationconst file = new RandomAccessFileImpl("/path/to/file");const memory = new MemoryBuffer(); // Also implements ReadWritablecopyStream(file, memory);Key Characteristics:\n\n1. Type relationships via interfaces: The RandomAccessFile extends ReadWritable, Seekable expresses an IS-A relationship at the type level, enabling polymorphism.\n\n2. No implementation inheritance: Classes implement interfaces but don't extend other classes. RandomAccessFileImpl doesn't inherit from BufferedFileReader.\n\n3. Reuse via composition: ReadBuffer, WriteBuffer, and FileHandle are composed into implementations. They're shared through HAS-A, not IS-A.\n\n4. Maximum flexibility: Any class can implement any combination of interfaces. Implementation details are fully encapsulated.\n\n5. Go-style philosophy: This is essentially how Go works—interface types for polymorphism, struct composition for code reuse.
This pattern is natural in languages like Go and Rust. In Java/C#/TypeScript, it requires discipline—the language allows class inheritance, but you choose interface-only inheritance for flexibility. Consider making this a team convention for new code.
Mixins and traits provide a middle ground between inheritance and composition. They allow sharing implementation across unrelated classes without creating inheritance hierarchies.\n\nThe Concept:\n\n- Mixins/traits are reusable chunks of behavior\n- They can be "mixed into" classes without inheritance relationships\n- Multiple mixins can be combined in a single class\n- They provide implementation sharing without IS-A semantics
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140
// Hybrid: Mixin Composition in TypeScript // Type for constructor functionstype Constructor<T = {}> = new (...args: any[]) => T; // ============ MIXINS ============ // Mixin: Adds timestamping capabilityfunction Timestamped<TBase extends Constructor>(Base: TBase) { return class extends Base { createdAt = new Date(); updatedAt = new Date(); touch() { this.updatedAt = new Date(); } getAge(): number { return Date.now() - this.createdAt.getTime(); } };} // Mixin: Adds activation capabilityfunction Activatable<TBase extends Constructor>(Base: TBase) { return class extends Base { isActive = false; activate() { this.isActive = true; console.log(`${(this as any).constructor.name} activated`); } deactivate() { this.isActive = false; console.log(`${(this as any).constructor.name} deactivated`); } };} // Mixin: Adds tagging capabilityfunction Taggable<TBase extends Constructor>(Base: TBase) { return class extends Base { private tags: Set<string> = new Set(); addTag(tag: string) { this.tags.add(tag); } removeTag(tag: string) { this.tags.delete(tag); } hasTag(tag: string): boolean { return this.tags.has(tag); } getTags(): string[] { return Array.from(this.tags); } };} // Mixin: Adds serialization capabilityfunction Serializable<TBase extends Constructor>(Base: TBase) { return class extends Base { toJSON(): object { const proto = Object.getPrototypeOf(this); const jsonObj: any = {}; Object.getOwnPropertyNames(this).forEach(key => { jsonObj[key] = (this as any)[key]; }); return jsonObj; } toString(): string { return JSON.stringify(this.toJSON(), null, 2); } };} // ============ USAGE ============ // Base class with core identityclass Entity { constructor(public id: string, public name: string) {}} // Compose mixins as needed - no inheritance hierarchy! // User: Timestamped + Activatable + Serializableclass User extends Serializable(Activatable(Timestamped(Entity))) { constructor(id: string, name: string, public email: string) { super(id, name); }} // Document: Timestamped + Taggable + Serializable (different mix)class Document extends Serializable(Taggable(Timestamped(Entity))) { constructor(id: string, name: string, public content: string) { super(id, name); }} // Product: Timestamped + Taggable + Activatable (all three)class Product extends Activatable(Taggable(Timestamped(Entity))) { constructor(id: string, name: string, public price: number) { super(id, name); }} // Each class has exactly the capabilities it needsconst user = new User('u1', 'Alice', 'alice@example.com');user.activate(); // From Activatableuser.touch(); // From Timestampedconsole.log(user.toJSON()); // From Serializable const doc = new Document('d1', 'Report', 'Content here');doc.addTag('important'); // From Taggabledoc.addTag('q4-2024');console.log(doc.getAge()); // From Timestamped const product = new Product('p1', 'Widget', 99.99);product.addTag('electronics'); // From Taggableproduct.activate(); // From Activatableproduct.touch(); // From Timestamped // Type system understands the composition!function processActivatable(item: { activate(): void; deactivate(): void }) { item.activate();} processActivatable(user); // Works - User has ActivatableprocessActivatable(product); // Works - Product has Activatable// processActivatable(doc); // Error - Document doesn't have ActivatableMixin Benefits:\n\n1. A la carte composition: Each class gets exactly the capabilities it needs. No inheriting unwanted behavior.\n\n2. No diamond problem: Mixins compose linearly. Each only extends the previous, avoiding multiple inheritance complexity.\n\n3. Reusable across hierarchies: The same Timestamped mixin works for User, Document, Product—completely unrelated types.\n\n4. Type-safe: TypeScript (and similar languages) understand the composed type. IDE autocomplete, type checking, all work correctly.\n\n5. Orthogonal concerns: Each mixin handles one capability. They compose without interference.
Different languages implement this pattern differently: Scala has traits, Rust has traits, Ruby has modules, Python has multiple inheritance (use carefully). TypeScript uses the function-based mixin pattern shown here. The concept is the same—reusable behavior chunks that compose.
This pattern combines a sealed/closed type hierarchy (for exhaustive handling) with composed strategies (for behavioral flexibility). The hierarchy is fixed, but behavior within each type is configurable.\n\nThe Pattern:\n\n- A sealed type hierarchy represents fixed categories\n- Each case in the hierarchy contains composed strategy components\n- Pattern matching handles the sealed hierarchy exhaustively\n- Strategies provide flexibility within each case
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126
// Hybrid: Sealed Hierarchy with Composed Strategies (Kotlin) // Strategy interfaces for composed behaviorinterface PricingStrategy { fun calculatePrice(basePrice: Double, context: PricingContext): Double} interface FulfillmentStrategy { fun fulfill(order: Order): FulfillmentResult} interface NotificationStrategy { fun notify(customer: Customer, message: String)} // Sealed hierarchy - exhaustive, closed set of order typessealed class OrderType( val pricing: PricingStrategy, val fulfillment: FulfillmentStrategy, val notification: NotificationStrategy) { // Each subtype is a case with composed strategies class Standard( pricing: PricingStrategy = StandardPricing(), fulfillment: FulfillmentStrategy = WarehouseFulfillment(), notification: NotificationStrategy = EmailNotification() ) : OrderType(pricing, fulfillment, notification) class Express( pricing: PricingStrategy = ExpressPricing(), // Adds rush fee fulfillment: FulfillmentStrategy = PriorityFulfillment(), notification: NotificationStrategy = SmsNotification() ) : OrderType(pricing, fulfillment, notification) class Subscription( val billingCycle: BillingCycle, pricing: PricingStrategy = SubscriptionPricing(), fulfillment: FulfillmentStrategy = AutoFulfillment(), notification: NotificationStrategy = EmailNotification() ) : OrderType(pricing, fulfillment, notification) class DigitalDownload( pricing: PricingStrategy = DigitalPricing(), fulfillment: FulfillmentStrategy = DownloadFulfillment(), notification: NotificationStrategy = EmailNotification() ) : OrderType(pricing, fulfillment, notification) class Wholesale( val discountTier: DiscountTier, pricing: PricingStrategy = WholesalePricing(), fulfillment: FulfillmentStrategy = BulkFulfillment(), notification: NotificationStrategy = B2BNotification() ) : OrderType(pricing, fulfillment, notification)} // Order uses the sealed typedata class Order( val id: String, val customer: Customer, val items: List<OrderItem>, val type: OrderType // Sealed - exhaustive handling possible) // Processing with exhaustive matching + strategy delegationclass OrderProcessor { fun process(order: Order): ProcessingResult { // Type-specific handling with exhaustive matching val typeSpecificData = when (order.type) { is OrderType.Standard -> processStandard(order) is OrderType.Express -> processExpress(order) is OrderType.Subscription -> processSubscription(order, order.type.billingCycle) is OrderType.DigitalDownload -> processDigital(order) is OrderType.Wholesale -> processWholesale(order, order.type.discountTier) // No else needed - compiler verifies exhaustiveness! } // Strategy-based behavior val price = order.type.pricing.calculatePrice( order.items.sumOf { it.basePrice }, PricingContext(order.customer) ) val fulfillmentResult = order.type.fulfillment.fulfill(order) order.type.notification.notify( order.customer, "Order ${order.id} processed. Total: $price" ) return ProcessingResult(order.id, price, fulfillmentResult, typeSpecificData) } private fun processStandard(order: Order): Map<String, Any> = mapOf( "estimatedDelivery" to calculateStandardDelivery(order) ) private fun processExpress(order: Order): Map<String, Any> = mapOf( "guaranteedDelivery" to calculateExpressDelivery(order), "trackingPriority" to "HIGH" ) private fun processSubscription(order: Order, cycle: BillingCycle): Map<String, Any> = mapOf( "nextBillingDate" to cycle.nextDate(), "autoRenew" to true ) private fun processDigital(order: Order): Map<String, Any> = mapOf( "downloadLinks" to generateDownloadLinks(order), "expiresAt" to Instant.now().plus(Duration.ofDays(7)) ) private fun processWholesale(order: Order, tier: DiscountTier): Map<String, Any> = mapOf( "discountApplied" to tier.discountPercent, "invoiceTerms" to "NET30" )} // Adding a new order type requires:// 1. Add to sealed class (compile errors everywhere it's not handled)// 2. Add handling in when expressions (compiler enforces)// 3. Strategy implementations are reusable or new as neededBenefits of This Hybrid:\n\n1. Exhaustive handling: The sealed hierarchy ensures all order types are handled. Adding a new type creates compile errors until handled everywhere.\n\n2. Type-specific data: Each sealed case can have unique properties (billingCycle for subscriptions, discountTier for wholesale).\n\n3. Flexible behavior: Strategies within each type can be customized. Express orders could use different pricing during promotions.\n\n4. Pattern matching: Clean when expressions that the compiler verifies are complete.\n\n5. Safe evolution: New types are explicit additions; you can't accidentally add a type that bypasses handling.
This pattern works well for domain concepts with a fixed set of categories (order types, payment methods, state machine states) where each category has configurable behavior. The categories are stable; the behaviors within them evolve.
Each hybrid pattern suits different design contexts. Use this guide to select the appropriate approach:
| Pattern | Best When | Example Domains |
|---|---|---|
| Inheritance for Structure, Composition for Behavior | Stable type hierarchy + variable behavior within types | Vehicles, UI Components, Documents |
| Abstract Base + Strategy Composition | Shared algorithm skeleton + pluggable steps | File Processors, Request Handlers, Pipelines |
| Interface Inheritance + Implementation Composition | Maximum flexibility, multiple interfaces per class | IO Streams, Service Layers, Plugins |
| Mixin/Trait Composition | Orthogonal capabilities across unrelated types | Serialization, Logging, Validation |
| Sealed Hierarchy + Strategies | Fixed categories with exhaustive handling + variable behavior | State Machines, Message Types, Commands |
We've explored five hybrid patterns that combine inheritance and composition effectively. Let's consolidate the key insights:
Module Conclusion:\n\nYou now have a complete framework for choosing between inheritance and composition—and combining them. You understand:\n\n- The decision framework with five evaluation dimensions\n- When inheritance is appropriate in six distinct scenarios\n- When composition is clearly better across seven contexts\n- How to combine both with five hybrid patterns\n\nThis knowledge transforms the inheritance vs composition question from a confused debate into a structured design decision based on your specific context, requirements, and constraints.
Congratulations! You've mastered the critical choice between inheritance and composition. You can now apply a systematic decision framework, recognize scenarios favoring each approach, and leverage hybrid patterns that combine both techniques effectively. This knowledge is fundamental to creating flexible, maintainable object-oriented designs.