Abstract Classes & Interfaces
Posted: May 30, 2022

Abstract Classes & Interfaces

Table of Contents:

  • Diving Into Interfaces
  • Diving Into Abstract Classes
  • Some Abstract Class Favorites
  • Wrapping Up

Following up on the Object-Oriented Basics post, this time we switch our attention to interfaces and abstract classes - when to use them, when to avoid them, and how to distinguish between them. Learn all about what makes an interface successful, how to consolidate logic in abstract classes, and more! Thanks to reader Naveen for writing in with this request.

Starting off, I always like to point people to chapter 8 of “Clean Code,” which provides an excellent introduction to what might be called “thinking in interfaces”:

There is a natural tension between the provider of an interface and the user of an interface. Providers of third-party packages and frameworks strive for broad applicability so they can work in many environments and appeal to a wide audience. Users, on the other hand, want an interface that is focused on their particular needs. This tension can cause problems at the boundaries of our systems.

I like this quote for two reasons:

  1. It establishes the importance of boundaries, a concept we’ll return to
  2. It provides an excellent use-case for interfaces — on the edges of systems

Diving Into Interfaces

Put simply, an interface is a reference type that holds abstract methods. That’s important because different programming languages have slightly different interpretations of what an interface is; it’s not always true, in every object-oriented language, for example, that the methods defined in an interface must be abstract. That can be extra confusing for people learning them, since it effectively serves to minimize the small differences between an interface and an abstract class. In Apex, an interface can:

  • define methods
  • it cannot provide default implementations for those methods
  • it cannot comment on the visibility of the methods it’s declaring
  • it cannot define properties, or anything else besides methods

Here’s a basic example of an interface (this one you may remember from Lazy Iterators if you did a deep dive on the linked-to repository where Aidan Harding’s original lazy iterator implementation lives):

public interface Function {
  Object call(Object o);
}

I like this interface as an example, because it can be used in a wide variety of applications. Note that in order to implement an interface, all methods on an interface must be declared in a class with at least public visibility; protected & private methods cannot “satisfy” the demands of an interface, as it is meant to describe the public API or surface layer of a class.

While abstract classes tend to group similar behaviors together, interfaces group what’s possible. As well, interfaces can extend other interfaces — you may have noticed that I do this within the Repository pattern; when an interface extends another interface, a class must define all methods from both interfaces in order to successfully deploy:

public interface IAggregateRepository extends IRepository {
  // by extending IRepository
  // we are promising the compiler that any class
  // implementing IAggregateRepository
  // has all of the methods defined here AND all of the methods
  // within IRepository
  Integer count();
  //  etc ... more aggregate methods
}

Here are several example implementations of the Function interface from before:

public class FunctionImplementations {
  public class FunctionLogger implements Function {
    public Object call(Object obj) {
      System.debug(obj);
      return obj;
    }
  }

  public class AccountAssignmentFunction implements Function {
    private final Schema.SObjectField fieldToken;
    private final Object value;

    public AccountAssignmentFunction(Schema.SObjectField fieldToken, Object value) {
      this.fieldToken = fieldToken;
      this.value = value;
    }

    public Object call(Object obj) {
      if (obj instanceof Account) {
        Account acc = (Account) obj;
        acc.put(this.fieldToken, this.value);
      }
      return obj;
    }
  }

  public class MultiplicationFunction implements Function {
    private final Decimal multiplicationFactor;

    public MultiplicationFunction(Decimal multiplicationFactor) {
      this.multiplicationFactor = multiplicationFactor;
    }

    public Object call(Object obj) {
      if (obj instanceof Decimal) {
        Decimal baseNumber = (Decimal) obj;
        return baseNumber * this.multiplicationFactor;
      }
    }
  }
}

Notice how using Object as the return type introduces possibilities at the expense of having to cast what’s going to be passed in. If the top two examples were being used:

// within the FunctionImplementations class ...
public void useExampleFunctions(List<Account> accounts) {
  List<Function> functions = new List<Function>{
    new FunctionLogger(),
    new AccountAssignmentFunction(Account.Name, 'Updated name')
  };

  for (Account acc : accounts) {
    for (Function func : functions) {
      func.call(acc);
    }
  }
}

This is an extremely contrived example, but something that I hope it helps to demonstrate is that Object-based interfaces have the most flexibility when it comes to usages by virtue of being able to be called with any type. The first two implementations of the interface, for example, return exactly the same object, while the MultiplicationFunction returns … a multiplied number. Hopefully this gets you thinking about when to use interfaces:

  • you have operations that are similar in naming but have vastly different underlying implementations
  • you have operations where there is no default implementation (goes hand-in-hand with the second half of the above bullet point; principally that the implementations can be quite different)

As well, if you are creating a library that will be packaged and consumed externally, using an interface makes even more sense. What I’m about to show you is a common pattern when building libraries. If you’re working on a team that shares a Salesforce org with other teams and are collaborating across packages, consuming another team’s code through interfaces (and vice versa: them using your services through interfaces you expose) is ideal, as it minimizes each team’s dependency on each other to only the meaningful methods necessary. As an example, I was recently asked within Apex Rollup whether or not it would be possible to bypass a custom trigger handler framework when updates are committed to the database. If there’s existing code that does this:

// in some class, within some method
Database.DMLOptions dmlOptions = new Database.DMLOptions();
// updates to the dmlOptions as necessary prior to calling:
Database.update(recordsToUpdate, dmlOptions);

An interface (coupled with Custom Metadata, in this case, to pass in the alternative implementation) becomes the ideal way to bifurcate the expected behavior while still allowing for sensible defaults:

public interface DatabaseUpdater {
  // update is a reserved keyword in Apex ...
  void doUpdate(List<SObject> recordsToUpdate);
}

// using CMDT records and taking advantage of .getInstance(String developerName):
// assumes there is a "Value" field on the CMDT ...
RollupPlugin__mdt alternativeDmlImplementation = RollupPlugin__mdt.getInstance('DatabaseUpdaterImplementation');
if (alternativeDmlImplementation != null) {
  // spin up the custom-supplied implementation:
  DatabaseUpdater customUpdater = (DatabaseUpdater) Type.forName(alternativeDmlImplementation.Value__c).newInstance();
  customUpdater.doUpdate(recordsToUpdate);
} else {
  // the default implementation; business as usual
  Database.DMLOptions dmlOptions = new Database.DMLOptions();
  // etc ...
  Database.update(recordsToUpdate, dmlOptions);
}

Note that we can get a lot fancier — the default implementation can become an instance of the DatabaseUpdater interface:

private class DefaultUpdater implements DatabaseUpdater {
  public void doUpdate(List<SOBject> records) {
    Database.DMLOptions dmlOptions = new Database.DMLOptions();
    // etc ...
    Database.update(recordsToUpdate, dmlOptions);
  }
}
// which then allows us to clean up the DML call site considerably:
MyCustomMetadata__mdt possibleImplementation = MyCustomMetadata__mdt.getInstance('DatabaseUpdaterImplementation');
Type updaterType = possibleImplementation != null ? Type.forName(possibleImplementation.Value__c) : DefaultUpdater.class;
((DatabaseUpdater) updaterType.newInstance()).doUpdate(recordsToUpdate);

So — you’ve seen several examples of interfaces being used. A class can implement multiple interfaces (this is a crucial difference, actually, between interfaces and abstract/virtual classes), which is a nice segueway into … the one other usage of interfaces that is common within Salesforce (though you’ll typically only see this as the consumer) — marker interfaces. Marker interfaces are “empty”, in the sense that you need not implement any methods in order to fulfill the bounds of the interface’s contract. Here’s a commonly used example from within Salesforce projects:

global class Database {

  // Stateful is a marker interface!
  global interface Stateful {
  }

  global interface Batchable<List<T>> {
    QueryLocator start(BatchableContext bc);
    void execute (BatchableContext bc, List<T> scope);
    void finish (BatchableContext bc);
  }

  global class QueryLocator {
    // implementation omitted
  }

  global class BatchableContext {
    // implementation omitted
  }
}

Look familiar? The Database.Stateful interface is a marker interface — it requires no methods to fulfill its contract, but allows consumers to persist instance variable data between batch jobs. Marker interfaces will (probably) have a more usable future within Apex in coming releases; for now, the Custom Metadata approach (shown above) to initializing interface implementors dynamically is the most idiomatic way to do so.

That leaves us with … when not to use interfaces, which I believe is a subject also worth talking about. Unfortunately, in my experience interfaces are often introduced without cause or reason, so let’s review when using interfaces is an anti-pattern:

  • you only have one concrete implementation of a class. Having an interface for this is absolutely over-engineering.
  • you are grouping behavior without improving the readability of the system. Usage of interfaces should help to make code more readable by promising the reader that the actual implementation details behind a method call will be satisfied in an unimportant way; that there’s no need to examine the concrete type of the class implementing the interface. If you frequently have to peer into the concrete internals of a class while looking at usages of that interface, it’s likely you’re pinning to the wrong abstraction
  • you’re using an interface but you cast to a concrete class in order to make use of additional methods

There are plenty of reasons to use interfaces — in particular, when it comes to dependency injection, modeling dependencies with interfaces (typically) means that creating mocks/stubs for those dependencies when it comes time to unit tests your objects is relatively easy. To quote Ralph Waldo Emerson, “a foolish consistency is the hobgoblin of little minds,” though, and if you’re experiencing any of the code smells from the above list, I would encourage you to think critically about whether using interfaces is helping your use-case, or simply adding indirection without the need for it. To see other examples of where that quote comes into play, check out A Tale Of Two Scope Creeps and setTimeout() & Implementing Delays.

This is my opinion — there was an interesting thread on the programming subreddit recently about the article Building an interface (even if there's only one implementation) is unquestionably right. I would encourage readers to think critically when … comparing opinions … between strangers online (though hopefully the Ralph Waldo Emerson quote gives you enough context about how I feel about dogma), and when considering when to use interfaces.

Diving Into Abstract Classes

If interfaces define contracts for our classes to fulfill, what makes an abstract class so special? In Apex, the principal difference between interfaces and abstract classes lies in what’s allowed to be defined — an abstract class can define default implementations for methods, and an abstract class can define properties (both instance and static properties). Like interfaces, abstract classes can’t be initialized directly; they require non-abstract classes to extend them:

public abstract class MyAbstractClass {
  public static final String CONSTANT_TEXT = 'I can define properties';

  // classes extending MyAbstractClass HAVE to provide
  // an implemetation for "doSomething"
  public abstract void doSomething();

  public virtual void optionallyProvideAnotherImplementation() {
    System.debug('By default we will log this, but subclasses can provide a different implementation');
  }
}

Trying to initialize MyAbstractClass would lead to the error: Error: Abstract classes cannot be constructed: MyAbstractClass. Extending the class ends up looking like:

public class AbstractSubclass extends MyAbstractClass {

  // We HAVE to declare an implementation for doSomething:
  public void doSomething() {
    System.debug('fulfills the abstract class contract');
  }

  // We DO NOT have to override a virtual method
  // but I do so here to show you how it's done
  public override void optionallyProvideAnotherImplementation() {
    System.debug('Overridden!');
  }
}

Something that’s interesting about virtual methods (ones that provide a default implementation) is that their overrides can also be declared as virtual (only of use if the subclass itself is abstract or virtual, which means that the subclass itself can also be overridden):

public virtual class AbstractSubclass extends MyAbstractClass {

  public void doSomething() {
    System.debug('fulfills the abstract class contract');
  }

  // By declaring the override as virtual, and the class itself as virtual
  // we allow the implementation HERE to be overriden in subclasses
  public virtual override void optionallyProvideAnotherImplementation() {
    System.debug('Overridden!');
  }
}

Again, abstract classes excel at grouping shared logic between subclasses. Within Apex Rollup, for instance, I use a class called RollupCalculator to define methods and properties that will be shared between subclasses, and specifically the method:

public abstract without sharing class RollupCalculator {
  // top-level properties
  public virtual void performRollup(List<SObject> calcItems, Map<Id, SObject> oldCalcItems) {
    // note the usage of the virtual keyword here
    // logic for the default means of rolling values up follows
  }
}

Subclasses then either get the chance to take advantage of the default logic for rolling values up (which is followed by all rollup operations where the individual values can be reduced meaningfully to numbers: Decimal and all of its derivatives, as well as Datetime, Date and Time classes — which, again, can all be represented by numbers), or they can opt-out of the default logic by supplying their own:

// an example overridden implementation of the default behavior
private without sharing class AverageRollupCalculator extends RollupCalculator {
  // constructor omitted for brevity
  public override void performRollup(List<SObject> calcItems, Map<Id, SObject> oldCalcItems) {
    List<SObject> applicableCalcItems = this.op == Rollup.Op.DELETE_AVERAGE ? new List<SObject>() : calcItems;
    applicableCalcItems = this.winnowItems(applicableCalcItems, oldCalcItems);
    Decimal oldSum;
    Decimal denominator = Decimal.valueOf(applicableCalcItems.size());
    if (this.isFullRecalc == false) {
      // apex rollup calculates many rollup operations using only values in memory
      // but for average, if we don't already HAVE all of the pre-existing values in memory,
      // we don't have enough information to update and need to retrieve (at the very least)
      // the count and sum of the other values - this part is also omitted from the example
      // though it's only a few lines of code
    }

    if (oldSum == null) {
      oldSum = 0.00;
    }
    Decimal newSum = 0.00;
    for (SObject calcItem : applicableCalcItems) {
      // this.opFieldOnCalcItem is a shared property on the parent abstract class
      Object potentialDecimal = calcItem.get(this.opFieldOnCalcItem);
      newSum += potentialDecimal == null ? 0.00 : (Decimal) potentialDecimal;
    }

    Decimal average = null;
    // We can't do the division if the denominator is 0
    if (denominator != 0) {
      Decimal numerator = oldSum + newSum;
      average = numerator / denominator;
    }
    // another shared property at the parent level
    this.returnVal = average;
  }
}

Being able to use subclasses as a way of customizing logic is a cookie-cutter case for employing abstract classes to allow for code re-use. Ideally, however, we separate the ability for code re-use from the intended purpose of abstract (and, by … extension 😎 … virtual classes), which is to group like behaviors.

Don’t Repeat Yourself (DRY) is one of the first concepts that gets heavily covered by newcomers to object oriented programming, but there’s been a growing backlash surrounding that particular practice because blanket statements such as DRY needlessly reduce the practicalities of building working software into a process too rigid to deviate from. In other words: it doesn’t always make sense to not duplicate code when it’s hard to further reduce the concept you’re duplicating. Many times, DRY makes sense because we can say: “oh, this concept deserves a name of its own”:

public abstract class RollupComparer {
  protected final Integer moveTowardFrontOfList = -1;
  protected final Integer moveTowardBackOfList = 1;

  public abstract Integer compare(Object o1, Object o2);

  public void sort(Object[] values) {
    ComparableItem[] wrappedItems = new List<ComparableItem>();

    for (Object value : values) {
      wrappedItems.add(new ComparableItem(this, value));
    }

    wrappedItems.sort();

    values.clear();
    for (ComparableItem item : wrappedItems) {
      values.add(item.value);
    }
  }

  private class ComparableItem implements System.Comparable {
    private final RollupComparer comparer;
    private final Object value;

    public ComparableItem(RollupComparer comparer, Object value) {
      this.comparer = comparer;
      this.value = value;
    }

    public Integer compareTo(Object o) {
      return this.comparer.compare(this.value, ((ComparableItem) o).value);
    }
  }
}

I’ve used this example before (in the post Random Shuffle For Lists & The Importance Of Revisiting Old Code as well as in Sorting & Performance In Apex), but it’s a good one because it shows off the value of a good abstraction: here, the abstract class allows subclasses to easily implement sorting without having to go through any additional ceremony:

// sorts picklist fields according to their "rank" in the picklist
private class PicklistSorter extends RollupComparer {
  private final PicklistController picklistController;
  public PicklistSorter(PicklistController picklistController) {
    this.picklistController = picklistController;
  }
  public override Integer compare(Object o1, Object o2) {
    String firstVal = (String) o1;
    String secondVal = (String) o2;

    Integer sortValue = 0;
    Integer firstRank = this.picklistController.getRank(firstVal, Rollup.Op.FIRST);
    Integer secondRank = this.picklistController.getRank(secondVal, Rollup.Op.FIRST);
    if (firstRank == this.picklistController.minimumIntegerValue || secondRank == this.picklistController.minimumIntegerValue) {
      // fall back to standard sorting if sentinel value is returned
      return firstVal.compareTo(secondVal);
    } else if (firstRank > secondRank) {
      sortValue = this.moveTowardBackOfList;
    } else if (firstRank < secondRank) {
      sortValue = this.moveTowardFrontOfList;
    }
    return sortValue;
  }
}

// sorts records according to their SObject types to prevent chunk errors on updates
private class SObjectSorter extends RollupComparer {
  public override Integer compare(Object first, Object second) {
    Integer returnVal = 0;
    if (first instanceof SObject && second instanceof SObject) {
      String firstSObjectType = String.valueOf(((SObject) first).getSObjectType());
      String secondSObjectType = String.valueOf(((SObject) second).getSObjectType());
      if (firstSObjectType > secondSObjectType) {
        returnVal = this.moveTowardBackOfList;
      } else if (secondSObjectType > firstSObjectType) {
        returnVal = this.moveTowardFrontOfList;
      }
    }
    return returnVal;
  }
}

Note that more recent versions of Salesforce (API version 59+) have introduced the generic System.Comparator<T> interface, which as further distilled the abstraction for sorting. Sorting & Performance In Apex has the full story on that.

If, by introducing an abstract class, you can codify a concept and distill it down to something that makes the rest of your work manageable, or increases the readability of the downstream code, you’ve done yourself (and the people you work with, if applicable), an enormous favor. Sorting, for example, typically involves a lot of ceremony — using an abstract class to consolidate some of that ceremony so that sorting a list becomes as simple as new SObjectSorter().sort(list) is amazing. Just don’t take it too far! One of the most common mistakes I see when people start using abstract and virtual classes can be represented with the following example:

public abstract class BusinessLogic {
  public abstract void assign(List<SObject> recordsWithoutOwners);
  public abstract void createTasks(List<SObject> recordsThatNeedTasks);
}

There’s never going to be a case where a class like BusinessLogic makes sense. It’s grouping two pieces of behavior that don’t belong together. Now, in this hypothetical situation, it could be the case that complex assignment rules that differ between departments is needed, and that every department also needs Tasks created. That’s not out of the question. If that is the case, though, BusinessLogic becomes a terrible name for the concept that you’re trying to consolidate. In general, (lest we return to Naming Matters in Apex) a class’s name should describe its responsibilities, whereas a method’s name should outline the action being performed.

Some Abstract Class Favorites

There are a few areas within Salesforce that benefit tremendously from the usage of abstract classes to consolidate behavior — and sometimes this involves a fascinating duality between interfaces and abstract classes. For example, I highly prefer establishing a single source of truth for queueables within a codebase:

public abstract class AbstractQueuer implements System.Queueable {
  public abstract void execute(System.QueueableContext context);

  protected virtual AbstractQueuer getQueuer() {
    return this;
  }

  protected void runAgain() {
    if(this.shouldRunAgain()) {
      System.enqueueJob(this.getQueuer());
    }
  }

  protected virtual Boolean shouldRunAgain() {
    Integer queuedJobsThisTransaction = Limits.getQueueableJobs();
    Integer maxQueueableJobs = Limits.getLimitQueueableJobs();

    return Test.isRunningTest() == false && queuedJobsThisTransaction <= maxQueueableJobs;
  }
}

This allows for rather effortless recursion in queueables when processing records (which can make them a nice alternative to batches when you have a way to flag processed records):

public class AccountFlagUpdater extends AbstractQueuer {
  private static final Integer REASONABLE_UPDATE_AMOUNT = 2000;

  public void execute(System.QueueableContext qc) {
    List<Account> accountsToFlag = [SELECT Id, Flag__c FROM Account WHERE Flag__c = FALSE LIMIT :REASONABLE_UPDATE_AMOUNT];
    for (Account acc : accountsToFlag) {
      acc.Flag__c = true;
    }
    update accountsToFlag;

    this.runAgain();
  }

  protected override Boolean shouldRunAgain () {
    return super.shouldRunAgain() &&
      [SELECT COUNT() FROM Account WHERE Flag__c = FALSE] > 0;
  }
}

This pattern — encapsulating system specific interfaces with common-sense extras — is really nice because it allows us to keep our business-specific classes small. It also provides a nice developer experience by separating concerns (business logic and re-queuing logic) into method-based partitions that increase readability & decrease the amount of time it takes to deliver value. That’s a huge win. This has the added benefit of making testing easy — consolidating where we can create a stub being the preamble to any good dependency-injection strategy — but I want to take pains to stress that ease in testability is frequently paired with good architecture. The latter begets the former, in other words.

Wrapping Up

Think critically about where you’ve seen interfaces used, and why you’ve seen them used. When were they successful? Remember that because a class can implement multiple interfaces, you can easily add abstraction points to a class where its behavior converges on behavior common to the broader patterns in your application; an interface with a single method but a good name can help to consolidate the concepts behind our business logic, particularly when the underlying implementations differ drastically.

When it comes to abstract and virtual classes, remember that Don’t Repeat Yourself (DRY) is important when behaviors belong together, and help with further development. Every class sharing a parent class, however, isn’t a great usage for subclassing, and carries with it a performance overhead. Consolidate and group behavior into shared methods when appropriate.

As always, thanks for reading the Joys of Apex. I appreciate that so many people have written in with requests, with thoughts, and feedback. It really means a lot to me!

In the past three years, hundreds of thousands of you have come to read & enjoy the Joys Of Apex. Over that time period, I've remained staunchly opposed to advertising on the site, but I've made a Patreon account in the event that you'd like to show your support there. Know that the content here will always remain free. Thanks again for reading — see you next time!