Inheritance: Subclassing, NSObject, super, and Composition vs. Inheritance
Write correct subclass declarations, explain NSObject's role as the root of every class hierarchy, override methods correctly using super, trace the initialization hierarchy through chained [super init] calls, and reason about when composition is the better design choice over deep inheritance.
Every class this course has written so far — Person, Messenger, Downloader — has quietly been part of an inheritance hierarchy, declared with a colon and a second class name: @interface Person : NSObject. That colon is not decoration; it establishes a real relationship, subclassing, through which Person automatically gains a substantial amount of behavior it never had to write itself. This lesson makes that relationship explicit: what it means for one class to be a subclass of another, what NSObject specifically provides as the ultimate ancestor of virtually every class you'll write, how super lets an overriding method still reach the version it's replacing, why initialization specifically has to chain through every level of that hierarchy, and — just as importantly — when a deep inheritance chain becomes a liability rather than a convenience, and composition serves a design better instead.
Subclassing: The Colon Syntax and What It Grants
A class declares its superclass with a colon immediately after its own name in @interface: @interface Employee : Person. This single line establishes that Employee is a subclass of Person, and it is a strong, load-bearing statement: every property Person declares, every instance method Person implements, and every ivar Person has is automatically available on Employee too, without Employee writing a single line of code to re-declare any of it. An Employee instance genuinely has a _name and a _age, and can genuinely respond to -description, purely because Person already provided them and Employee inherited them by virtue of that one colon.
Subclassing is additive by default: Employee doesn't merely inherit Person's members, it can also add entirely new ones of its own that Person never had — @property (nonatomic, copy) NSString *jobTitle; inside Employee's own @interface gives every Employee instance a jobTitle property, while a plain Person instance has no such thing at all. This is the essential shape of subclassing: specialize a general type by adding what's specific to the more particular case, while keeping everything the general type already provided.
It's worth being precise that inheritance is a one-way relationship: Employee knows about, and can use, everything Person defines, but Person has no knowledge whatsoever of Employee or any of its additions — a variable statically typed as Person * cannot access jobTitle even if it happens to actually be pointing at an Employee instance at runtime, without an explicit cast. The superclass is, and remains, oblivious to whatever subclasses might eventually be built on top of it.
• @interface Employee : Person declares Employee as a subclass of Person — the colon establishes the inheritance relationship. • A subclass automatically has every property, method, and ivar its superclass declares, with zero extra code required. • Subclassing is additive: a subclass can declare new properties/methods the superclass never had. • Inheritance is one-directional: the superclass has no knowledge of any subclass or its additions.
NSObject: The Root of the Hierarchy
NSObject is the root class that sits at the top of virtually every Objective-C class hierarchy — when Person is declared @interface Person : NSObject, Person isn't just gaining some convenient utilities; it's declaring itself as a member of the entire object system Objective-C's runtime is built around. NSObject provides the baseline behavior every object needs to function within that system: memory management machinery (the retain/release/ARC bookkeeping covered in full in the Memory Management module), the default -description method (which Class Anatomy already relied on for readable logging), -isEqual: and -hash for comparing objects, -class for asking an object what its runtime type is, +alloc for allocating new instances, and -respondsToSelector:/+instancesRespondToSelector:, the existence checks covered in the Dynamic Messaging lesson just before this one.
Because virtually every class you write ultimately descends from NSObject — directly, as Person does, or indirectly, through some other ancestor that itself descends from NSObject — all of that baseline machinery is available on essentially every object in an Objective-C program, without any class needing to implement it from scratch. This is precisely why [anyObject description], [anyObject respondsToSelector:@selector(foo)], and [anyObject isEqual:otherObject] all work reliably regardless of what specific class anyObject happens to be — they aren't things each class reimplements; they're inherited, ultimately, from NSObject sitting at the top.
It is technically possible to declare a class with no superclass at all (a root class of its own), but this is exceedingly rare in ordinary application code — doing so forfeits all of NSObject's baseline behavior and requires manually reimplementing a substantial amount of infrastructure the runtime otherwise assumes every object has. In practice, essentially every class you write in this course, and in real Objective-C code generally, ultimately traces back to NSObject at its root.
• NSObject is the root class virtually every Objective-C class hierarchy descends from, directly or indirectly. • It provides baseline behavior: memory management hooks, -description, -isEqual:/-hash, -class, +alloc, -respondsToSelector:, and more. • Because nearly every class descends from it, this baseline behavior is available on essentially any object, without each class reimplementing it. • Declaring a class with no superclass at all is rare in practice — it forfeits all of NSObject's built-in infrastructure.
Overriding and super
A subclass overrides an inherited method by declaring and defining a method with the exact same selector its superclass (or an ancestor further up the chain) already provides — as the Methods lesson noted, matching selector, matching parameter types, matching return type. When Employee declares its own -description, Employee's version is what actually runs whenever -description is sent to an Employee instance; runtime lookup, covered in Dynamic Messaging, finds Employee's implementation first (since it searches the receiver's actual class before walking further up), so Person's -description is effectively shadowed for any Employee object, even though Person's version still exists further up the chain.
Very often, an overriding method doesn't want to replace its superclass's behavior entirely — it wants to extend it, running the superclass's version first (or last) and adding something on top. super is the keyword that makes this possible: [super description] inside Employee's own -description sends the description message specifically starting the search at Person (Employee's superclass) rather than at Employee itself, deliberately skipping past Employee's own override to reach the version being extended. - (NSString *)description { return [NSString stringWithFormat:@"%@, Job: %@", [super description], self.jobTitle]; } runs Person's description logic first, then appends Employee's own addition, rather than reimplementing Person's formatting from scratch.
super is not itself a variable or a distinct object with its own identity — sending a message to super still operates on the exact same object (self) that received the original message; only where the runtime begins its lookup changes, starting one level higher in the hierarchy instead of at the receiver's own actual class. This distinction matters: [super description] and [self description] would produce different results (Employee's own override would recurse into itself with self, but correctly skip to Person's version with super), while both are still fundamentally operating on the identical Employee instance underneath.
• Overriding: a subclass provides its own implementation for a selector its superclass already defines; the subclass's version runs for its instances. • super lets an overriding method reach its superclass's version of the same method deliberately, rather than reimplementing it from scratch. • [super description] starts runtime lookup one level higher (at the superclass) instead of at the receiver's own actual class. • super is not a separate object -- it's still the same self, just a different starting point for where method lookup begins.
Inherited Properties, Inherited Methods, and the Class Hierarchy
Inheritance in Objective-C is transitive: if Employee inherits from Person, and Manager inherits from Employee, then Manager inherits everything Employee has (including everything Employee itself inherited from Person) — a Manager instance has _name and _age from Person, jobTitle from Employee, and whatever Manager adds on its own, all simultaneously, purely by virtue of the chain of superclass relationships connecting it back to the root. Each level in this chain only needs to declare what's genuinely new at that level; everything below is carried forward automatically.
This is the class hierarchy: a tree (in practice, usually closer to a chain, for any one lineage) with NSObject at the root and every other class positioned somewhere beneath it, each node's position determined entirely by its declared superclass. The specific chain this lesson uses as its running example looks like this:
Reading this diagram from top to bottom traces the path of accumulating specialization: NSObject provides the universal baseline every object needs; Person adds name and age, general to any person; Employee adds jobTitle, specific to people who work; Manager adds whatever is specific to managing others (perhaps a list of direct reports) on top of everything Employee already has. Reading it from bottom to top instead traces exactly what runtime lookup does when a message is sent to a Manager instance that Manager itself doesn't implement: check Manager, then Employee, then Person, then NSObject, stopping at the first match found.
• Inheritance is transitive: Manager (via Employee, via Person) has everything from every ancestor in its chain, simultaneously. • Each class in a hierarchy only needs to declare what's new at its own level -- everything from ancestors is carried forward automatically. • The class hierarchy is a tree rooted at NSObject; a single lineage (NSObject → Person → Employee → Manager) reads as a chain. • Runtime lookup for an unimplemented selector walks this chain from the receiver's own class upward, exactly the order the diagram shows top-down in reverse.
The Initialization Hierarchy: Chained [super init]
Because a subclass instance genuinely contains storage for every ancestor's ivars simultaneously — a Manager has Person's _name/_age, Employee's _jobTitle, and its own ivars all at once — properly constructing one requires every level of the hierarchy to get a chance to set up its own portion of that storage, in order, starting from the root. This is exactly why every custom -init... method this course has written begins by calling [super init] before doing anything else: self = [super init]; sends init up to the superclass first, giving it (and, transitively, its own superclass, all the way up to NSObject) the opportunity to fully initialize everything it's responsible for, before the current level's -init method sets up what's specific to it.
Concretely, if Manager overrides -init, its body should look like - (instancetype)init { self = [super init]; if (self) { _directReports = [NSMutableArray array]; } return self; } — the [super init] call runs Employee's initializer (which itself calls [super init] to run Person's, which itself calls [super init] to run NSObject's, at the true root of the chain), and only once that entire chain has completed and handed back a valid, non-nil self, does Manager's own -init body proceed to set up _directReports. Skipping this call would leave every ancestor's portion of the object's storage completely uninitialized — Manager's own ivar would be set up correctly, but _name, _age, and _jobTitle would all be left in whatever undefined state alloc's raw memory allocation left them in.
The if (self) { ... } guard immediately after self = [super init] exists because [super init] (or any initializer, at any level) is permitted to return nil if initialization failed for some reason — checking self before proceeding avoids doing further setup work on, or accessing ivars through, an object that failed to come into existence properly. This pattern — assign the result of [super init] back to self, guard on it being non-nil, then do this level's own setup — is the standard shape of every initializer in an Objective-C inheritance chain, at every level, all the way from a direct NSObject subclass down through arbitrarily many levels of further subclassing.
• Every -init override should call [super init] first, assigning the result back to self, before doing its own level's setup. • This chains all the way up: Manager's init calls Employee's, which calls Person's, which calls NSObject's — each level initializing its own portion of the object's storage in order. • Skipping [super init] leaves ancestor-level ivars in an undefined state, even though the current level's own ivars are set up correctly. • The if (self) { ... } guard after self = [super init]; protects against proceeding on an object whose initialization already failed somewhere up the chain.
Limitations of Deep Inheritance
A chain like NSObject → Person → Employee → Manager is short enough to stay easy to reason about, but nothing stops a codebase from growing it further — Manager → Director → VicePresident → Executive, each level adding a little more — and real problems tend to emerge well before a hierarchy gets that deep. The first is the fragile base class problem: because every subclass depends on the exact behavior of every ancestor above it, a seemingly small, reasonable change to Person (adjusting what -description returns, or how -init sets up _name) can silently break Employee, Manager, and every other descendant that was implicitly relying on the old behavior, even though none of their own code changed at all. The deeper the chain, the more classes are exposed to this risk from a single change anywhere above them.
The second is tight coupling between levels that, in the real domain the classes model, may not actually belong in a strict is-a relationship at all. Modeling 'a Manager is a kind of Employee, which is a kind of Person' is a reasonably natural fit for is-a — but it's easy to be tempted into forcing a relationship that's really more like has-a into the same inheritance shape, purely because inheritance is the mechanism already at hand, producing a hierarchy that's technically legal but conceptually strained and hard to reason about later.
The third is that deep hierarchies make behavior harder to trace: understanding what a single message send to a VicePresident instance actually does might require reading four or five separate @implementation blocks, tracing overrides and super calls through every intervening level, when a flatter design would have made the same behavior visible in one place. This isn't a reason to avoid inheritance altogether — it remains an excellent, natural tool for genuine is-a relationships with a shallow, stable hierarchy — but it is a real cost that grows, non-linearly, with depth, and it's exactly what motivates the alternative covered next.
• Fragile base class problem: a change to an ancestor can silently break distant descendants that depended on its old behavior, without their own code changing. • Forcing a has-a relationship into an is-a inheritance shape just because inheritance is available produces a technically-legal but conceptually strained hierarchy. • Deep chains make tracing actual behavior harder: understanding one message send may require reading through many @implementation blocks and super calls. • These costs grow with depth — inheritance remains a good tool for genuine, shallow, stable is-a relationships.
Composition vs. Inheritance
Composition is the alternative design: instead of a class gaining behavior by inheriting from another class (is-a), it gains behavior by holding a reference to an instance of another class as one of its own properties, and delegating to it (has-a). Where inheritance would say 'a Car is-a Engine' (which is a poor fit for reality — a car doesn't behave like an engine, it uses one), composition says 'a Car has-a Engine', declared as @property (nonatomic, strong) Engine *engine; inside Car, with Car's own methods calling into self.engine's methods wherever engine behavior is actually needed.
Composition avoids every one of the deep-inheritance costs described above, because the composed pieces are independent, separately-defined classes connected only through a property reference rather than through the compiler-enforced, rigid shape of a class hierarchy: changing Engine's internals doesn't risk silently breaking Car the way changing Person could silently break Manager, since Car only depends on Engine's own public interface, not on being built from the same inheritance chain. Composed pieces are also easier to swap: a Car holding an Engine * property can be handed a GasEngine or an ElectricEngine instance interchangeably (given a shared interface, typically a protocol), something a rigid, single-superclass inheritance chain cannot express nearly as flexibly.
The practical guidance this course recommends: reach for inheritance when the relationship is genuinely is-a, the hierarchy will stay shallow (two, maybe three levels, as in the Person → Employee → Manager example), and the behavior really is meant to be shared and specialized uniformly down that chain. Reach for composition when the relationship is really has-a or uses-a, when you want to combine independent pieces of behavior flexibly (a class that needs both logging behavior and networking behavior doesn't need to inherit from a LoggingNetworkingBase class — it can simply hold a logger and a network client as two separate composed properties), or when you find yourself designing a hierarchy purely to reuse code rather than because the types genuinely relate to each other as general-to-specific. Neither tool replaces the other entirely; well-designed Objective-C code uses shallow, deliberate inheritance for real is-a relationships and composition everywhere else.
• Composition (has-a): a class holds another class's instance as a property and delegates to it, rather than inheriting from it. • Composition avoids fragile-base-class risk: composed pieces are connected by an interface, not a rigid shared hierarchy, so changes are more isolated. • Composed pieces are easier to swap (e.g. GasEngine vs. ElectricEngine via a shared protocol) than a fixed inheritance chain allows. • Guidance: use inheritance for genuine, shallow is-a relationships; use composition for has-a/uses-a relationships and for combining independent behaviors flexibly.
Code Examples
Employee : Person establishes the subclass relationship, so Employee automatically has name and age from Person. Its own initializer calls [super initWithName:age:] first so Person's portion of the object is set up before Employee sets _jobTitle. Its -description override calls [super description] to reuse Person's formatting rather than reimplementing it, appending only what's new at Employee's level.
Manager's initializer calls [super initWithName:age:jobTitle:], which is Employee's initializer -- which itself calls [super initWithName:age:], Person's initializer -- which itself calls [super init], reaching NSObject at the true root. Only after this entire upward chain completes does Manager's own if (self) block run, setting up _directReports. Every ancestor's portion of the object is fully initialized, in order, before the subclass adds its own piece.
Car does not subclass Engine -- it holds one as a property and delegates to it. This models the real has-a relationship correctly (a car uses an engine; it isn't a kind of engine) and means Car and Engine can each change independently, and Car's engine property could just as easily be swapped for a different Engine subclass, none of which a rigid inheritance relationship would express as cleanly.
// Person.h
@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSInteger age;
- (instancetype)initWithName:(NSString *)name age:(NSInteger)age;
- (NSString *)description;
@end
// Employee.h
@interface Employee : Person
@property (nonatomic, copy) NSString *jobTitle;
- (instancetype)initWithName:(NSString *)name age:(NSInteger)age jobTitle:(NSString *)jobTitle;
@end
// Employee.m
@implementation Employee
- (instancetype)initWithName:(NSString *)name age:(NSInteger)age jobTitle:(NSString *)jobTitle {
self = [super initWithName:name age:age]; // let Person set up name/age first
if (self) {
_jobTitle = [jobTitle copy];
}
return self;
}
- (NSString *)description {
return [NSString stringWithFormat:@"%@, Job: %@", [super description], self.jobTitle];
}
@end@interface Manager : Employee
@property (nonatomic, strong) NSMutableArray<Employee *> *directReports;
- (instancetype)initWithName:(NSString *)name age:(NSInteger)age jobTitle:(NSString *)jobTitle;
@end
@implementation Manager
- (instancetype)initWithName:(NSString *)name age:(NSInteger)age jobTitle:(NSString *)jobTitle {
self = [super initWithName:name age:age jobTitle:jobTitle]; // -> Employee's init -> Person's init -> NSObject's init
if (self) {
_directReports = [NSMutableArray array];
}
return self;
}
@end
Manager *m = [[Manager alloc] initWithName:@"Dana" age:41 jobTitle:@"Engineering Manager"];
NSLog(@"%@", m.description);
// Prints: "Dana, Job: Engineering Manager" (inherited from Employee's -description, itself extending Person's)// Instead of forcing "Car is-a Engine" through inheritance:
@interface Engine : NSObject
- (void)start;
@end
@implementation Engine
- (void)start { NSLog(@"Engine starting..."); }
@end
@interface Car : NSObject
@property (nonatomic, strong) Engine *engine; // has-a Engine, not is-a Engine
- (void)start;
@end
@implementation Car
- (void)start {
[self.engine start]; // Car delegates to its composed Engine
}
@end
Car *car = [[Car alloc] init];
car.engine = [[Engine alloc] init];
[car start]; // "Engine starting..." -- Car never inherited from Engine at allThe Person / Employee / Manager Class Hierarchy
NSObject sits at the root, providing baseline behavior every object needs. Person adds name/age. Employee, a subclass of Person, adds jobTitle while keeping everything Person provided. Manager, a subclass of Employee, adds its own manager-specific state on top of everything inherited from Employee and, transitively, from Person and NSObject.
• (instancetype)initWithName:(NSString *)name age:(NSInteger)age jobTitle:(NSString *)jobTitle { _jobTitle = [jobTitle copy]; // never called [super init...] at all return self; } - (instancetype)initWithName:(NSString *)name age:(NSInteger)age jobTitle:(NSString *)jobTitle { self = [super initWithName:name age:age]; if (self) { _jobTitle = [jobTitle copy]; } return self; }
Skipping [super init...] means Person's portion of the object -- _name and _age -- is never set up at all, left in whatever undefined state alloc's raw memory left them in. Every initializer in a hierarchy must call up to its superclass's initializer first so every ancestor level gets to set up its own storage.
• (NSString *)description { return [NSString stringWithFormat:@"%@, Job: %@", self.description, self.jobTitle]; // infinite recursion } - (NSString *)description { return [NSString stringWithFormat:@"%@, Job: %@", [super description], self.jobTitle]; }
Calling self.description (or [self description]) from inside Employee's own -description override sends the message back to Employee's own class starting from the top, recursing into itself forever. [super description] deliberately starts lookup at Person instead, correctly reaching the version being extended rather than the override itself.
// forcing a has-a relationship into inheritance: @interface Car : Engine @end
@interface Car : NSObject @property (nonatomic, strong) Engine *engine; @end
A Car is not a kind of Engine -- it uses one. Modeling this as inheritance (Car : Engine) forces an is-a relationship onto something that's really has-a, producing a class hierarchy that doesn't match reality and is harder to reason about. Composition (Car holding an Engine property) models the relationship correctly.
// building a deep chain purely to reuse code: Manager : Employee : Person : Staff : Worker : NSObject
// keep the hierarchy shallow and genuinely is-a; share unrelated behavior via composition or protocols instead
Extending an inheritance chain purely to reuse code, rather than because each level is a genuine specialization of the one above, produces a fragile hierarchy where a change anywhere near the root risks breaking many distant descendants. Prefer a shallow, deliberate is-a chain and reach for composition to share behavior that doesn't fit that shape.
What does @interface Employee : Person establish? A. That Employee and Person are unrelated classes that happen to be declared near each other B. That Employee is a subclass of Person, automatically inheriting Person's properties, methods, and ivars C. That Person is a subclass of Employee D. That Employee must reimplement every method Person has
B. That Employee is a subclass of Person, automatically inheriting Person's properties, methods, and ivars — The colon syntax declares Employee as a subclass of Person: Employee automatically gains every property, instance method, and ivar Person defines, without needing to redeclare any of it, and can add its own new members on top.
What role does NSObject play in an Objective-C class hierarchy? A. It's an optional utility class most programs never use B. It's the root class virtually every class ultimately descends from, providing baseline behavior like -description, -isEqual:, and +alloc C. It's a protocol, not a class D. It only exists in unit testing code
B. It's the root class virtually every class ultimately descends from, providing baseline behavior like -description, -isEqual:, and +alloc — NSObject sits at the root of essentially every class hierarchy, providing foundational behavior (memory management hooks, -description, -isEqual:/-hash, +alloc, and more) that every descendant class inherits automatically.
What does [super description] do inside Employee's own overridden -description method? A. It causes infinite recursion into Employee's own -description B. It sends the description message starting the search at Person (Employee's superclass), reaching Person's version rather than Employee's own override C. It is a syntax error inside an override D. It deletes Employee's own description implementation
B. It sends the description message starting the search at Person (Employee's superclass), reaching Person's version rather than Employee's own override — super does not change which object receives the message (it's still the same self) -- it changes where runtime lookup begins searching, starting one level higher at the superclass, which is exactly how an override can extend rather than fully replace its superclass's behavior.
Why must every -init override call [super init] (or the appropriate super initializer) before doing its own setup? A. It's purely a stylistic convention with no functional effect B. So every ancestor level in the hierarchy gets to initialize its own portion of the object's storage before the current level adds its own -- skipping it leaves ancestor ivars undefined C. Because [super init] is required to allocate memory for the object D. Because Objective-C requires exactly one super call per program
B. So every ancestor level in the hierarchy gets to initialize its own portion of the object's storage before the current level adds its own -- skipping it leaves ancestor ivars undefined — A subclass instance contains storage for every ancestor's ivars simultaneously. Calling [super init] first lets each level up the chain (all the way to NSObject) properly initialize the portion it's responsible for, before the current level's own setup runs -- skipping it leaves ancestor-level ivars in an undefined state.
What is the fragile base class problem? A. A crash that occurs whenever a class has no superclass B. The risk that a seemingly small change to an ancestor class can silently break distant subclasses that implicitly depended on its old behavior, even though the subclasses' own code never changed C. A compiler error triggered by declaring too many properties D. The requirement that every subclass must override every inherited method
B. The risk that a seemingly small change to an ancestor class can silently break distant subclasses that implicitly depended on its old behavior, even though the subclasses' own code never changed — Because every subclass depends on the exact behavior of everything above it in the hierarchy, changing an ancestor -- even in a reasonable way -- can silently break descendants relying on its previous behavior. This risk grows the deeper and more heavily-depended-upon a hierarchy becomes.
When is composition generally preferred over inheritance? A. Always -- inheritance should never be used in Objective-C B. When the relationship between two types is really has-a/uses-a rather than genuine is-a, or when combining independent behaviors flexibly matters more than a shared hierarchy C. Only when the two classes have identical method names D. Composition and inheritance are always interchangeable with no tradeoffs
B. When the relationship between two types is really has-a/uses-a rather than genuine is-a, or when combining independent behaviors flexibly matters more than a shared hierarchy — Composition (holding another class's instance as a property and delegating to it) fits has-a/uses-a relationships better than forcing them into inheritance, avoids the fragile-base-class risk of deep hierarchies, and makes swapping composed pieces easier -- while genuine, shallow is-a relationships still remain a good fit for inheritance.
NSObject
↓
Person
↓
Employee
↓
Manager