Inheritance, Composition, and ReuseIntroductionObject-oriented programming (OOP) supports software reuse by allowing programmers to define new types in terms of existing types. Inheritance and composition are the two most important mechanisms for expressing reuse and relationships among objects. Inheritance creates a relationship in which one class specializes another; composition constructs a more complex object from other objects. These mechanisms are powerful, but neither should be selected automatically. Poorly designed inheritance hierarchies can produce rigid, fragile software, while excessive composition can create unnecessary complexity. For AI and machine-learning applications, these decisions arise frequently. A system may contain different kinds of models, data sources, optimizers, sensors, or evaluation strategies. A sound design must represent genuine conceptual relationships, preserve substitutability, and remain understandable as the system evolves. This chapter uses Python-like examples. The principles apply to languages such as Java, C++, C#, and TypeScript, although syntax and visibility rules differ. Learning ObjectivesAfter completing this chapter, you should be able to: - explain inheritance, specialization, generalization, and method overriding;
- distinguish an is-a relationship from a has-a relationship;
- design and use abstract classes and abstract methods;
- implement composition and delegation;
- apply substitutability and the Liskov Substitution Principle;
- compare inheritance and composition using maintainability criteria;
- identify common design errors in class hierarchies; and
- select an appropriate reuse strategy in an AI/ML application.
1. Reuse and Relationships in Object-Oriented DesignReuse means employing existing software assets rather than implementing equivalent behavior repeatedly. Reuse may occur through: - implementation reuse, in which existing code is executed again;
- interface reuse, in which different classes follow a common contract;
- conceptual reuse, in which a domain abstraction is shared; and
- behavioral reuse, in which a class delegates work to another object.
A relationship between two classes should communicate domain meaning, not merely reduce typing. Two broad relationships are central: - is-a: a specialized object is also an instance of a more general concept. A `ConvolutionalNetwork` is a `Model`.
- has-a: an object contains, owns, or uses another object. A `Trainer` has an `Optimizer`.
The distinction is useful but not mechanical. A sentence such as “a car has an engine” suggests composition; “a car is a vehicle” suggests inheritance. Nevertheless, a domain statement alone is insufficient. The proposed subtype must also obey the expectations established by its base type. 2. Inheritance: Generalization and SpecializationInheritance allows a subclass, or derived class, to acquire accessible attributes and methods from a superclass, or base class. The subclass can add new capabilities or override inherited operations. ```python class Model: def predict(self, features): raise NotImplementedError class LinearModel(Model): def predict(self, features): return self.weights @ features + self.bias ``` `LinearModel` specializes `Model`. It inherits the conceptual role of a model while providing a particular prediction implementation. Inheritance can support: 1. interface inheritance: the subclass conforms to operations promised by the parent; 2. implementation inheritance: the subclass reuses existing method or attribute implementations; and 3. polymorphism: code can operate on a general type while receiving subclass-specific behavior. 2.1 Single and Multilevel InheritanceIn single inheritance, a class has one direct parent. Multilevel inheritance forms a chain, such as `Model` → `NeuralNetwork` → `ConvolutionalNetwork`. Such chains can be useful when each level represents a stable and meaningful abstraction. They become risky when behavior is distributed across many levels, making it difficult to determine which implementation is active. Some languages support multiple inheritance, where a class has more than one parent. Python permits it, while Java generally supports multiple interface inheritance but only single class inheritance. Multiple inheritance may model legitimate combinations, but it can introduce name conflicts and the diamond problem: a class inherits the same ancestor through two paths. Language-specific method-resolution rules can address the conflict, but complexity remains a design concern. 2.2 Visibility and EncapsulationA subclass should not depend unnecessarily on the internal representation of its parent. Inheritance does not eliminate encapsulation. If a subclass directly manipulates fragile internal data structures, changes to the parent may break the subclass. Prefer protected extension points, documented methods, and stable interfaces over access to implementation details. 3. Method Overriding and PolymorphismMethod overriding occurs when a subclass supplies a method with the same conceptual contract as an inherited method. The replacement should preserve the meaning of the operation. Overriding is different from overloading. Overloading uses the same operation name with different parameter lists, where supported; overriding replaces inherited behavior in a subtype. ```python class DataLoader: def load(self, path): raise NotImplementedError class CSVLoader(DataLoader): def load(self, path): Read and validate comma-separated datareturn read_csv(path) class JSONLoader(DataLoader): def load(self, path): return read_json(path) def prepare_dataset(loader, path): records = loader.load(path) return clean(records) ``` `prepare_dataset` is polymorphic: it depends on the `DataLoader` abstraction and works with either concrete loader. The caller does not need conditional logic such as “if the file is CSV, do this; otherwise do that.” An overriding method should normally preserve: - the expected input meaning;
- the promised output meaning;
- relevant error behavior;
- important side-effect constraints; and
- performance or resource assumptions when these are part of the contract.
A subclass may internally implement an operation differently, but a caller should not need to know which subtype it received merely to use the inherited interface. When a subclass extends rather than replaces behavior, it may call the parent implementation. This is appropriate only when the parent operation remains semantically valid. Calling `super()` mechanically can be incorrect if the parent performs work that conflicts with the subclass’s invariants. 4. Abstract Classes and ContractsAn abstract class defines a common concept but is not intended to be instantiated directly. It may provide shared state or concrete methods while leaving some operations to subclasses. An abstract method declares an operation that concrete subclasses must implement. ```python from abc import ABC, abstractmethod class Model(ABC): def __init__(self, name): self.name = name @abstractmethod def predict(self, features): pass def describe(self): return f"Model: {self.name}" ``` An abstract class establishes a contract. It is useful when several related classes share both an interface and genuine implementation or invariants. An interface or protocol is often preferable when the design requires only a capability and not shared state or implementation. For example, a forecasting service may depend on anything that supplies `forecast(horizon)`. Requiring every implementation to inherit from one particular class can create unnecessary coupling. A protocol-based design can allow unrelated classes to participate if they provide the required operation. Abstract designs should specify preconditions, postconditions, and invariants. A precondition describes what must be true before an operation; a postcondition describes what the operation guarantees afterward; an invariant must remain true for every valid object of the class. 5. Substitutability and the Liskov Substitution PrincipleThe Liskov Substitution Principle (LSP) states that objects of a subtype should be usable wherever objects of the supertype are expected without invalidating the correctness of the program. It is not enough that a subclass compiles or shares a method name. A practical interpretation is: - a subtype should not require stronger inputs than the supertype contract requires;
- a subtype should provide results consistent with the promised output;
- a subtype should not unexpectedly remove guarantees;
- a subtype should preserve important invariants; and
- clients should not need repeated type checks to use ordinary subtype behavior.
Consider this problematic design: ```python class Rectangle: def set_width(self, value): self.width = value def set_height(self, value): self.height = value class Square(Rectangle): def set_width(self, value): self.width = self.height = value def set_height(self, value): self.width = self.height = value ``` Mathematically, a square is a rectangle, but this mutable API gives clients independent width and height operations. A client expecting to set width without changing height will fail when given a `Square`. The issue is not the mathematical classification; it is the behavioral contract of the software abstraction. Separate immutable shapes or a common `Shape` abstraction may be safer. LSP is therefore a design test. Ask: “What assumptions can a client make about the base type, and does every proposed subtype honor them?” 6. Composition and DelegationComposition creates an object that contains references to other objects and delegates responsibilities to them. It expresses a has-a or uses-a relationship. ```python class Optimizer: def step(self, parameters, gradients): raise NotImplementedError class Trainer: def __init__(self, model, optimizer): self.model = model self.optimizer = optimizer def train_batch(self, features, labels): predictions = self.model.predict(features) gradients = compute_gradients(predictions, labels) self.optimizer.step(self.model.parameters(), gradients) ``` `Trainer` does not become an `Optimizer`; it has one. This design separates responsibilities and permits an optimizer to be replaced without changing the trainer. Composition supports: - low coupling;
- replacement at runtime;
- focused classes;
- easier unit testing through test doubles; and
- combinations of behaviors that would be awkward in a rigid inheritance tree.
Delegation is the act of forwarding a responsibility to a composed object. A `ReportService` may delegate formatting to a `Formatter`, and an `ImagePipeline` may delegate resizing to a `Resizer`. Delegation is not a loss of abstraction: the containing object can coordinate several collaborators while preserving a simple public interface. 6.1 Aggregation and OwnershipComposition is often discussed with aggregation. In aggregation, a contained object may exist independently and may be shared. In stronger composition, the containing object controls the contained object’s lifecycle or conceptual identity. The distinction is language-dependent and should not be treated as an absolute runtime rule. The important question is ownership: who creates, replaces, configures, and disposes of the collaborator? 7. Inheritance Versus CompositionInheritance is appropriate when all of the following are substantially true: - the subtype genuinely represents a specialized form of the parent;
- the parent contract is stable and meaningful;
- subtype instances can replace parent instances;
- the hierarchy is not being used merely to share utility code; and
- the subclass does not need to disable or reinterpret much of the parent behavior.
Composition is usually preferable when: - behavior may vary independently;
- collaborators should be replaceable at runtime;
- the relationship is has-a, uses-a, or depends-on;
- the proposed subtype would violate the parent contract; or
- future combinations are likely to outnumber the current categories.
A common rule is “favor composition over inheritance.” This is a heuristic, not a prohibition. Inheritance can communicate domain structure and provide useful polymorphism. The correct decision depends on contracts, variability, coupling, and expected change. 7.1 A Decision FrameworkBefore choosing inheritance, evaluate five questions: 1. Semantic validity: Is the proposed subtype genuinely a specialization? 2. Substitutability: Can every client of the parent use the subtype safely? 3. Responsibility: Does the subclass have a focused responsibility rather than a collection of unrelated additions? 4. Change coupling: Will changes to the parent force unnecessary changes in the child? 5. Variability: Are behaviors likely to change independently or be combined in new ways? If the answers to the first two are weak, inheritance should normally be rejected. If the last two indicate high variability, composition is often more maintainable. 8. Worked Design Example: An ML Training FrameworkSuppose a university laboratory is building a training framework. The initial design uses inheritance: ```python class Trainer: def train(self, data): model = self.create_model() optimizer = self.create_optimizer() return run_training(model, optimizer, data) class ImageTrainer(Trainer): def create_model(self): return CNN() def create_optimizer(self): return Adam() ``` This is a template-method design. It may be suitable when every trainer follows the same lifecycle and subclasses select stable construction decisions. However, requirements soon add language models, distributed optimizers, early stopping, checkpointing, and different data-validation policies. A deep hierarchy could result: `Trainer` → `ImageTrainer` → `DistributedImageTrainer` → `CheckpointedDistributedImageTrainer`. The classes represent combinations of independent features, so inheritance begins to encode a product of choices. A compositional redesign is more flexible: ```python class TrainingPipeline: def __init__(self, model_factory, optimizer, validator, stopper): self.model_factory = model_factory self.optimizer = optimizer self.validator = validator self.stopper = stopper def train(self, data): data = self.validator.validate(data) model = self.model_factory.create() return run_training(model, self.optimizer, data, self.stopper) ``` Now a CNN factory can be combined with Adam or SGD, and the same stopping policy can be reused across models. Each collaborator can be tested independently. The framework still may use inheritance internally—for example, several model factories may implement a common `create` protocol—but the variable policies are composed. AnalysisThe inheritance design reuses a training algorithm and is reasonable while variation is limited to a few stable creation steps. The compositional design better represents independent policies. It reduces subclass proliferation and makes runtime configuration possible. Its cost is that the system contains more collaborating objects and requires clear interfaces and dependency configuration. 9. Illustrative Case: Clinical Imaging PipelineThe following is a fictional illustrative case. A hospital research team develops a pipeline that receives medical images, preprocesses them, invokes a model, and records results. Background and ProblemThe first version defines `MRIImage`, `CTImage`, and `UltrasoundImage` as subclasses of `MedicalImage`. Later, the team needs compressed images, encrypted storage, GPU preprocessing, multiple normalization methods, and audit logging. Developers begin creating classes such as `EncryptedGPU MRIImage` and `LoggedNormalizedCTImage`. The hierarchy becomes difficult to understand and test. AnalysisImage modality is a genuine classification, but encryption, logging, normalization, and hardware execution are behaviors that vary independently. They are not necessarily kinds of images. Treating them as inheritance dimensions creates a combinatorial hierarchy. The team also discovers that some image subclasses override `load()` to require a network connection, violating client expectations that a local image can be loaded from a path. Approach and SolutionThe team retains a modest abstraction: ```python class ImageSource: def read(self): raise NotImplementedError ``` A `Pipeline` is composed with an `ImageSource`, `Preprocessor`, `Model`, `Storage`, and `AuditLogger`. Decorator-like wrappers add encryption and logging around selected components. Each component has a narrow contract, and dependency injection supplies test implementations. OutcomeNew preprocessing methods can be introduced without adding image subclasses. A model can be replaced independently of storage. The team also documents whether each component may mutate data, whether failures are retriable, and what information must be logged. Tests verify both individual components and the pipeline contract. Learning PointsThe case demonstrates that domain inheritance and behavioral composition can coexist. It also shows that maintainability depends on contract clarity, not simply on the number of classes. In a clinical setting, traceability and predictable failure behavior are particularly important because software outputs may influence human decisions. 10. Maintainability, Testing, and Professional PracticeReuse is valuable only when it reduces total cost over the software lifecycle. Relevant maintainability factors include: - coupling: how strongly one class depends on another’s details;
- cohesion: how closely the responsibilities within a class belong together;
- change impact: the number of components likely to change after a requirement changes;
- testability: how easily behavior can be isolated and verified;
- discoverability: how easily developers can understand available operations; and
- extensibility: how safely new behavior can be added.
Inheritance can reduce duplication but increase coupling to base-class implementation. Composition may require more delegation code but allows collaborators to be replaced. Neither approach automatically produces high cohesion. Unit tests should verify contracts, not only implementation details. For a base abstraction, tests can specify common behavior that every valid implementation must satisfy. Subtype-specific tests then verify specialized guarantees. Composition makes test isolation easier because a fake optimizer, model, or logger can be supplied. Documentation should state whether methods may be overridden, whether constructors must be called, what exceptions mean, and which attributes are stable. A class intended for extension should provide deliberate extension points rather than accidentally exposing every internal method. 11. Common Design ErrorsMisusing inheritance for code sharingA utility method does not create an is-a relationship. If a class inherits from another only to reuse one helper, composition, a standalone function, or a helper object is usually clearer. Overriding to disable behaviorA subclass that raises “not supported” for an inherited operation may indicate that the parent abstraction is too broad. Split the abstraction or use smaller interfaces. Type-checking everywhereRepeated `if isinstance(...)` statements often signal that polymorphism or composition has not been used effectively. Some type checks are legitimate at system boundaries, but they should not replace a coherent contract. Deep inheritance treesDeep trees make behavior harder to trace and increase the chance that a change at the top affects distant subclasses. Prefer shallow hierarchies with clear responsibilities. Fragile base classesA base-class change can silently alter subclass behavior, especially when subclasses depend on protected state or call-order assumptions. Use stable contracts and minimize hidden dependencies. Excessive compositionComposition can also be overused. A design with many tiny objects, excessive indirection, or unclear ownership may be harder to understand than a simple class. Interfaces should be meaningful, and collaborators should represent real variation or responsibility. 12. Ethics, Governance, and AI RelevanceIn AI systems, reuse decisions can affect governance. A shared base class may encode common validation or audit behavior, but developers must verify that every subtype actually satisfies those controls. For example, a model subclass that bypasses input validation can undermine safety assumptions. Composed logging and monitoring components can support accountability, but logging sensitive data may create privacy risks. In clinical, financial, or educational applications, designers should minimize collected information, control access, preserve data provenance, and document model and preprocessing versions. Reusable components should not silently carry inappropriate assumptions from one context to another. A sound architecture makes safety-relevant behavior explicit. Validation, authorization, reproducibility, and error reporting should be part of documented contracts where applicable, rather than accidental side effects of a parent class. 13. Contemporary DevelopmentsModern software ecosystems increasingly use interfaces, protocols, dependency injection, component systems, mixins, decorators, and functional composition alongside traditional inheritance. Python’s abstract base classes and protocols, Java interfaces and sealed classes, and C# interfaces and records all support controlled polymorphism in different ways. Sealed or restricted hierarchies can be useful when a domain has a deliberately closed set of variants, such as a compiler’s syntax nodes or a fixed command set. Open extension points are more appropriate for plugin architectures. Data-oriented and component-based designs, common in high-performance and simulation software, often favor assembling capabilities rather than building deep class trees. For AI/ML engineering, frameworks commonly separate models, datasets, optimizers, metrics, callbacks, and deployment services as replaceable components. This reflects the practical need to experiment with combinations while preserving reproducibility and testing. Best-Practice Checklist- Model genuine is-a relationships with inheritance.
- Use composition for has-a, uses-a, and independently variable behavior.
- Define contracts before creating subclasses.
- Check substitutability, not merely shared names or attributes.
- Keep inheritance hierarchies shallow and cohesive.
- Prefer interfaces or protocols when shared implementation is unnecessary.
- Inject collaborators to improve testing and replacement.
- Document ownership, lifecycle, side effects, and failure behavior.
- Avoid subclassing solely for code reuse.
- Review reuse decisions as requirements and system risks evolve.
Chapter SummaryInheritance expresses specialization and can provide interface inheritance, implementation reuse, and polymorphism. Method overriding is safe only when the replacement preserves the inherited contract. Abstract classes and interfaces make shared expectations explicit, while the Liskov Substitution Principle provides a practical test for whether a subtype can replace its parent. Composition expresses has-a and uses-a relationships by assembling objects and delegating responsibilities. It is particularly effective when behaviors vary independently, must be replaced at runtime, or would create a combinatorial inheritance hierarchy. Inheritance and composition are not competing dogmas: well-designed systems often use both. The central decision criteria are semantic validity, substitutability, coupling, cohesion, testability, expected change, and governance requirements. In AI and ML systems, these principles support flexible experimentation while helping preserve reproducibility, safety, privacy, and accountability. Key TermsInheritance: Mechanism by which a subclass acquires or specializes behavior from a superclass. Superclass: A general class from which another class inherits. Subclass: A class that specializes or extends another class. Generalization: Modeling common features in a more general abstraction. Specialization: Defining a more specific type based on a general type. Method overriding: Replacing inherited behavior with a subtype implementation that follows the same contract. Polymorphism: Using objects of different concrete types through a common abstraction. Abstract class: A class intended to define common structure or behavior without necessarily being instantiated. Abstract method: A method whose required implementation is deferred to concrete subclasses. Interface: A specification of operations and behavioral expectations without requiring shared implementation. Composition: Building an object from other objects that perform delegated responsibilities. Delegation: Forwarding work from one object to a collaborator. Aggregation: A relationship in which contained objects may exist independently of the container. Substitutability: The ability to use a subtype wherever its supertype is expected. Liskov Substitution Principle: The principle that subtypes must preserve the correctness expectations of their supertypes. Coupling: The degree of dependency between software components. Cohesion: The degree to which responsibilities within a component belong together. Dependency injection: Supplying collaborators to an object rather than having it construct them internally. Contract: A documented set of preconditions, postconditions, invariants, and behavioral expectations. Template method: A design in which a base method defines an algorithmic structure and subclasses provide selected steps. Review QuestionsShort Answer Questions1. What is the difference between an is-a relationship and a has-a relationship? 2. Define method overriding and explain how it differs from overloading. 3. What purpose does an abstract class serve? 4. State the central idea of the Liskov Substitution Principle. 5. Give two advantages of composition. Long Answer Questions1. Explain inheritance, polymorphism, and abstract classes using an AI/ML example. 2. Compare inheritance and composition in terms of coupling, testing, extensibility, and maintainability. 3. Discuss why a mathematically valid subtype may still violate software substitutability. 4. Explain delegation and dependency injection, including their effects on testability. 5. Describe the risks of deep inheritance hierarchies and propose design practices to reduce those risks. Analytical / Application Questions1. Design an architecture for a machine-learning training system supporting multiple models, optimizers, stopping policies, and logging strategies. Justify which relationships should use inheritance and which should use composition. 2. A subclass overrides a parent method by accepting fewer valid input forms and raising an exception for some inputs previously accepted by the parent. Analyze the design using the Liskov Substitution Principle and propose a correction. 3. A software team has created twelve subclasses representing combinations of image type, storage format, encryption, and preprocessing method. Evaluate the design and recommend a maintainable alternative. Multiple Choice Questions with AnswersQuestion 1Which relationship is best represented by “a Trainer has an Optimizer”? A. Inheritance B. Composition C. Method overriding D. Generalization Correct Answer: B. Composition Question 2What is the primary requirement of method overriding? A. The subclass must use more parameters than the parent B. The subclass must preserve the meaning of the inherited contract C. The subclass must duplicate all parent code D. The parent must be abstract Correct Answer: B. The subclass must preserve the meaning of the inherited contract Question 3What does polymorphism allow a client to do? A. Use different concrete objects through a common abstraction B. Prevent all method replacement C. Access every private attribute D. Eliminate the need for object creation Correct Answer: A. Use different concrete objects through a common abstraction Question 4Which design most directly supports replacing an optimizer at runtime? A. Hard-coding optimizer logic in the Trainer superclass B. Making every optimizer a private method of Trainer C. Composing Trainer with an Optimizer object D. Duplicating Trainer for each optimizer Correct Answer: C. Composing Trainer with an Optimizer object Question 5What does the Liskov Substitution Principle require? A. Every class must have multiple parents B. Subtypes must be usable wherever their supertypes are expected C. Subclasses must never add methods D. All objects must be immutable Correct Answer: B. Subtypes must be usable wherever their supertypes are expected Question 6Why can a deep inheritance hierarchy be difficult to maintain? A. It prevents all reuse B. Behavior and dependencies become difficult to trace C. It guarantees high cohesion D. It removes polymorphism Correct Answer: B. Behavior and dependencies become difficult to trace Question 7Which is the strongest reason to use an abstract class? A. To prevent any form of reuse B. To define a shared contract and possibly shared implementation C. To guarantee that all methods are private D. To avoid defining subclasses Correct Answer: B. To define a shared contract and possibly shared implementation Question 8A class inherits from another only to reuse one helper function. Which alternative is usually clearer? A. A deeper inheritance chain B. A standalone function or helper object C. Overriding every parent method D. Making the helper abstract Correct Answer: B. A standalone function or helper object Question 9In the clinical imaging case, why were encryption and logging better modeled as composed behaviors? A. They were independent concerns that could vary separately from image modality B. They were mathematical shapes C. They could not be tested D. They required multiple inheritance by definition Correct Answer: A. They were independent concerns that could vary separately from image modality Question 10Which property describes how closely responsibilities within a class belong together? A. Coupling B. Substitutability C. Cohesion D. Delegation Correct Answer: C. Cohesion
|