Object-Oriented Apex · Lesson 12 of 41
Inheritance, Interfaces and Polymorphism in Apex
Once a codebase grows past a few classes, you start seeing the same shapes repeat: several kinds of discount, several notification channels, several integrations that all "send" something. Inheritance and interfaces let you write the shared part once and swap the specific part – and they're the foundation of trigger frameworks and enterprise patterns you'll meet later.
In this lesson
- virtual, abstract and override
- super and protected
- Interfaces and polymorphism
- Choosing an implementation at runtime with Type.forName
- Comparable for sorting your own classes
Classes are closed by default
In Java any class can be extended. In Apex a class must be declared virtual or abstract to allow it, and a method must be virtual or abstract to be overridden – with the override keyword on the child. It's deliberate: you decide what can change.
// Notification.cls
public abstract class Notification {
protected String recipient; // visible to subclasses
public Notification(String recipient) {
this.recipient = recipient;
}
public abstract String channel(); // subclasses MUST implement
public virtual String format(String message) { // subclasses MAY override
return '[' + channel() + '] to ' + recipient + ': ' + message;
}
public String send(String message) { // not virtual: same for everyone
return 'Sent ' + format(message);
}
}
// EmailNotification.cls
public class EmailNotification extends Notification {
public EmailNotification(String recipient) {
super(recipient); // call the parent constructor
}
public override String channel() {
return 'EMAIL';
}
}
// SmsNotification.cls
public class SmsNotification extends Notification {
public SmsNotification(String recipient) {
super(recipient);
}
public override String channel() {
return 'SMS';
}
public override String format(String message) {
String text = super.format(message); // reuse the parent version
return text.length() > 50 ? text.left(47) + '...' : text;
}
}
List<Notification> outbox = new List<Notification>{
new EmailNotification('asha@acme.com'),
new SmsNotification('+91-98765-43210')
};
for (Notification n : outbox) {
System.debug(n.send('Your case 00001042 has been resolved. Thank you!'));
}
Sent [EMAIL] to asha@acme.com: Your case 00001042 has been resolved. Thank you! Sent [SMS] to +91-98765-43210: Your case 00001042 ha...
The loop doesn't know or care which kind of notification it holds – each object runs its own version of channel() and format(). That's polymorphism.
| Keyword | Meaning |
|---|---|
virtual (class) | can be extended and instantiated |
abstract (class) | can be extended, can't be instantiated; may have abstract methods |
virtual (method) | has a body; children may override it |
abstract (method) | no body; children must implement it |
override | required on the child's version |
super | the parent's constructor or method |
Interfaces
An interface is a contract: a list of method signatures with no implementation. A class can extend only one class but implement many interfaces, which makes interfaces the more flexible tool for "things that can do X":
// DiscountRule.cls
public interface DiscountRule {
Decimal discountFor(Opportunity opp);
String name();
}
// VolumeDiscount.cls
public class VolumeDiscount implements DiscountRule {
public Decimal discountFor(Opportunity opp) {
return opp.Amount >= 100000 ? opp.Amount * 0.05 : 0;
}
public String name() { return 'Volume'; }
}
// LoyaltyDiscount.cls
public class LoyaltyDiscount implements DiscountRule {
public Decimal discountFor(Opportunity opp) {
return opp.Type == 'Existing Customer - Upgrade' ? 2500 : 0;
}
public String name() { return 'Loyalty'; }
}
Opportunity opp = new Opportunity(Name = 'Acme upgrade', Amount = 150000, Type = 'Existing Customer - Upgrade');
List<DiscountRule> rules = new List<DiscountRule>{ new VolumeDiscount(), new LoyaltyDiscount() };
Decimal totalDiscount = 0;
for (DiscountRule rule : rules) {
Decimal d = rule.discountFor(opp);
System.debug(rule.name() + ': ' + d);
totalDiscount += d;
}
System.debug('Total discount: ' + totalDiscount);
Volume: 7500.00 Loyalty: 2500 Total discount: 10000.00
Adding a new discount means adding a new class – the calculating loop never changes. This "open for extension, closed for modification" idea is what makes large codebases maintainable.
Choosing the implementation at runtime
Type.forName creates an object from a class name stored as text – in custom metadata, a custom setting or a field. Admins can switch behaviour without a deployment:
// imagine these names come from Custom Metadata records
List<String> configuredRules = new List<String>{ 'LoyaltyDiscount', 'VolumeDiscount', 'NoSuchRule' };
for (String className : configuredRules) {
Type t = Type.forName(className);
if (t == null) {
System.debug('Skipping unknown rule ' + className);
continue;
}
DiscountRule rule = (DiscountRule) t.newInstance();
System.debug('Loaded ' + rule.name() + ' rule');
}
Loaded Loyalty rule Loaded Volume rule Skipping unknown rule NoSuchRule
Comparable: sorting your own classes
// LeadScore.cls
public class LeadScore implements Comparable {
public String name;
public Integer score;
public LeadScore(String name, Integer score) {
this.name = name;
this.score = score;
}
public Integer compareTo(Object other) { // highest score first
LeadScore that = (LeadScore) other;
return that.score - this.score;
}
}
List<LeadScore> leads = new List<LeadScore>{
new LeadScore('Globex', 62), new LeadScore('Acme', 91), new LeadScore('Initech', 75)
};
leads.sort();
for (LeadScore l : leads) System.debug(l.name + ' ' + l.score);
Acme 91 Initech 75 Globex 62
Implement Comparable for a class's one natural order; use a separate Comparator (previous chapter) when you need several orders.
Practice
Try it yourself
Add a
SeasonalDiscountrule that gives 3% in December. What changes in the calculating loop?Show answer
Nothing. Write
public class SeasonalDiscount implements DiscountRule { … }and add an instance to the rules list (or its name to the configuration).Why does
new Notification('x')fail to compile?Show answer
Notification is abstract – it can't be instantiated, only extended.
Quick check
Can you extend an Apex class that isn't virtual or abstract?
No.
How many interfaces can a class implement?
Many – but it can extend only one class.
What does Type.forName return for an unknown class name?
null.
Summary
- Mark classes and methods virtual or abstract to allow extension; use override and super.
- Interfaces define contracts; code against the interface, not the class.
- Polymorphism lets one loop handle many implementations.
- Type.forName enables configuration-driven behaviour; Comparable gives a natural sort order.
Finished this lesson?
Mark it complete to track your progress, then carry on with the next lesson.