Concepts / Designing Class Hierarchies

Designing Class Hierarchies

Polymorphism allows a subtype to be treated as an instance of its parent type, enabling flexible code that works with multiple subtypes without type checking.

  • Programming

One Collection, Many Roles

Imagine counting everyone in a school building. You could write separate logic for teachers, students, and administrators. Polymorphism offers a different design: treat each person as a SchoolMember while allowing the object to retain the behavior of its specific subtype. The shared code does not need to check whether it received a Teacher or a Student.

can refer tocan refer toselectsselectsSchoolMemberparent-type referenceTeachersubtype objectTeacher.tell()subtype behaviorStudentsubtype objectStudent.tell()subtype behavior
How can objects from different subtypes be treated as the same parent type while still using each subtype's behavior?

Polymorphism allows a subtype to be treated as an instance of its parent type. A Teacher or Student can therefore be handled as a SchoolMember while still using the behavior belonging to the actual subtype.

Runtime Method Selection

The parent-type view of an object does not erase the object's subtype. When a method such as tell() is called, dynamic dispatch examines the object's actual runtime type and chooses the matching method. If a SchoolMember reference holds a Teacher object, the Teacher version of tell() runs. If that reference holds a Student object, the Student version runs. The choice is based on the real object, not merely on the parent type used to refer to it.

What do you think happens?

A variable is treated as a SchoolMember, but the object it refers to is a Teacher. Which version of tell() executes?

  • Only the generic SchoolMember version
  • The Teacher version
  • No version, because the reference is not a Teacher
Reveal answer

Answer: The Teacher version

Dynamic dispatch uses the actual runtime type of the object. The parent-type reference allows the object to be handled as a SchoolMember, but the Teacher object's behavior remains available.

each objectactual type is Teacheractual type is StudentSchoolMemberobjectsTeacher and Student objectscall tell()same operationTeacher.tell()Teacher behaviorStudent.tell()Student behavior
How does one piece of code process different subtype objects without checking their specific types?

Parent Features Everywhere

Inheritance creates a downward flow of functionality. When a new attribute or method is added to SchoolMember, every subtype inherits it automatically. For example, adding an ID card field to SchoolMember makes that field available to both Teacher and Student without changing either subtype.

inheritsinheritsSchoolMemberID card field addedTeacherinherits ID cardStudentinherits ID card
What happens to each subtype when a new capability is added to the parent class?

A parent-class change is shared by all subtypes because subtypes inherit the parent's attributes and methods. This makes shared functionality easier to add and maintain.

Contained Subtype Changes

Inheritance also provides isolation in the opposite direction. If a method is added only to Teacher, that method does not appear in Student and does not alter SchoolMember. Changing one subtype changes the behavior of that subtype without accidentally changing its siblings or its parent.

parent ofparent ofparent ofparent ofSchoolMemberno new Teacher-only methodTeacherexisting behaviorSchoolMemberunchangedTeachernew methodStudentexisting behaviorStudentunchanged
What changes when functionality is added to one subtype, and what remains unchanged in the parent and sibling subtype?

A Flexible Design Boundary

Polymorphism lets one operation work with multiple subtypes through their shared parent type. Instead of writing separate functions for Teacher and Student, code can work with SchoolMember objects and call the shared operation. Dynamic dispatch supplies the appropriate subtype behavior at runtime.

This design supports the Open-Closed Principle. Existing code can remain closed for modification while the class hierarchy stays open for extension: a new subtype can be added, and existing code that works with the parent type can continue to work without being rewritten for every subtype.

Design actionEffect
Add functionality to SchoolMemberThe functionality becomes available to Teacher and Student
Add functionality only to TeacherTeacher changes; Student and SchoolMember remain unaffected
Process objects as SchoolMember instancesThe same code can work with multiple subtypes
Call tell() through a parent-type referenceDynamic dispatch selects the actual subtype's version

Mistakes in Hierarchy Design

  • Assuming the parent-type reference determines the method that runs

    Dynamic dispatch selects the method using the object's actual runtime type.

    Fix: Determine which subtype object is actually being held; its matching method is selected at runtime.

  • Expecting a subtype-only change to spread to sibling subtypes

    Changes made within a subtype remain isolated from its siblings and parent.

    Fix: Add shared functionality to SchoolMember when all subtypes should receive it.

  • Writing separate logic for every known subtype

    This gives up the flexibility of polymorphism and makes the shared operation depend on subtype checks.

    Fix: Use the shared parent type when the operation applies uniformly to multiple subtypes.

  • Forgetting that inherited functionality is available automatically

    Subtypes inherit the parent's attributes and methods.

    Fix: Place shared functionality in the parent and allow inheritance to propagate it.

Practice the Reasoning

MEDIUM

A SchoolMember hierarchy contains Teacher and Student. First, predict the result of these changes: an ID card field is added to SchoolMember; a new method is added only to Teacher; and tell() is called on a SchoolMember reference that holds a Student object. State which objects receive each change and which tell() implementation executes.

Hints
  • Changes in the parent class propagate downward to its subtypes.
  • Changes in one subtype do not propagate sideways to sibling subtypes.
  • Dynamic dispatch uses the actual runtime type of the object.

Tracing Three Hierarchy Changes

Determine the effect of three operations in a SchoolMember hierarchy: add an ID card field to SchoolMember, add a method only to Teacher, and call tell() through a SchoolMember reference holding a Student object.

Parent change: The ID card field is added to SchoolMember, so both Teacher and Student inherit access to it.

Subtype change: The new method is added only to Teacher, so it remains isolated to Teacher and does not alter Student or SchoolMember.

Runtime call: The reference is parent-typed, but the actual object is Student. Dynamic dispatch therefore selects Student's version of tell().

The ID card field is available to Teacher and Student; the new method exists only in Teacher; and Student.tell() executes for the Student object.

Key Takeaways

  1. Polymorphism allows a subtype such as Teacher or Student to be treated as its parent type, SchoolMember.
  2. Dynamic dispatch chooses the correct subtype method at runtime based on the actual object type.
  3. Functionality added to the parent class automatically becomes available to its subtypes.
  4. Functionality added only to one subtype remains isolated from the parent and sibling subtypes.
  5. Polymorphism supports flexible code that can be extended with new subtypes without modifying existing shared logic.

Key Takeaways

  • Polymorphism lets code treat different subtype objects uniformly through a shared parent type.
  • Dynamic dispatch preserves subtype-specific behavior when a method is called.
  • Parent-class functionality propagates to all subtypes through inheritance.
  • Subtype-specific changes remain isolated from the parent and sibling subtypes.
  • This combination supports extensible code under the Open-Closed Principle.