
Reduce, Reuse, Refactor: Interfaces & Object Inheritance
Table of Contents:
Where you are in the Reduce, Reuse, Refactor series:
- On The Relationship Between Automation & Code
- What Is An Object?
- Object Variables
- Interfaces & Object Inheritance (this post)
Object-oriented programming languages model objects as hierarchies — typically, everything starts from the Object
class and propogates downwards. As we delve into the topic of interaces and inheritance, we’re essentially learning how to perform taxonomy on code: how to recognize objects that are similar to other objects in different hierarchies, and how to model those relationships to aid in the neverending goal of translating intent and business rules to code.
When we model how objects “talk” to one another, and how we think about them, the starting place is typically the API that the object in question has. Let’s look at a simple example:
public class DML {
public Database.SaveResult doInsert(SObject record) {
return this.doInsert(new List<SObject>{ record });
}
public List<Database.SaveResult> doInsert(List<SObject> records) {
List<Database.SaveResult> results = Database.insert(records);
this.logErrors(results);
return results;
}
private void logErrors(List<Database.SaveResult> results) {
for (Database.SaveResult result : results) {
if (result.isSuccess() == false) {
Logger.error('Error creating or updating', result);
}
}
}
}
If we were to model this DML
object, I’d do something like the following:
- DML
Database.SaveResult doInsert(SObject singleRecord)
List<Database.SaveResult> doInsert(List<SObject> records)
String toString()
Object clone()
Boolean equals(Object other)
Integer hashCode()
Note that typically when modeling objects, we don’t represent the public methods that come directly from the Object superclass, but we could if we wanted to.
What we do want to show, and be aware of, are the methods (and variables) that are globally and publicly available when we make our own hierarchies. How does that work, exactly? And why would we want to use such language features? Well, the DML
class I’ve just started sketching out is actually an easy way to show this off. While insert and update share the same Database.SaveResult
class, consider the case for upserting: the standard platform returns a different Database.UpsertResult
type for this.
One might hope that there was some unifying parent type between all of the different database result objects in the standard Apex library, but there isn’t. Without a common interface, if we wanted to support logging errors for every kind of result, we’d have to re-implement the logging loop for each and every type:
public class DML {
public Database.SaveResult doInsert(SObject record) {
return this.doInsert(new List<SObject>{ record })[0];
}
public List<Database.SaveResult> doInsert(List<SObject> records) {
List<Database.SaveResult> results = Database.insert(records);
this.logErrors(results);
return results;
}
public Database.UpsertResult doUpsert(SObject record) {
return this.doUpsert(new List<SObject>{ record })[0];
}
public List<Database.UpsertResult> doUpsert(List<SObject> records) {
List<Database.UpsertResult> results = Database.upsert(records);
this.logUpsertErrors(results);
return results;
}
private void logErrors(List<Database.SaveResult> results) {
for (Database.SaveResult result : results) {
if (result.isSuccess() == false) {
Logger.error('Error creating or updating', result);
}
}
}
private void logErrors(List<Database.UpsertResult> results) {
for (Database.UpsertResult result : results) {
if (result.isSuccess() == false) {
Logger.error('Error creating or updating', result);
}
}
}
}
I’ve included an error in here on purpose — forgetting to update the string passed to Logger.error()
, but the error might come much later on, as well. This happens with shifting requirements and code duplication: the decision is made to lower the logging level to something below “error”, like “warn”, for this particular code path. Somebody goes in, updates a method or two, but misses something somewhere. Now you have two problems! Looking for more info on that hidden Logger
implementation? It’s an easter egg reference to Nebula Logger, the most popular and widely-used logging framework for Salesforce.
Interfaces — Defining API-Level Contracts
One way that problems of this sort can be cleaned up is through the use of interfaces, which define a set of methods that an object must define in order to properly implement the interface. Remember when we talked about interfaces in On The Relationship Between Automation & Code? Let’s walk through declaring one and show how using an interface might work with this example:
public class DML {
// an interface can ALSO be a top-level class
// but it's common to see them defined within classes
// as well
public interface DatabaseResult {
Boolean hasError();
List<Database.Error> getErrors();
}
public DatabaseResult doInsert(SObject record) {
return this.doInsert(new List<SObject>{ record })[0];
}
public List<DatabaseResult> doInsert(List<SObject> records) {
List<Database.SaveResult> results = Database.insert(records);
List<DatabaseResult> wrappedResults = new List<DatabaseResult>();
for (Database.SaveResult result : results) {
wrappedResults.add(this.logErrors(new SaveResult(result)));
}
return wrappedResults;
}
public DatabaseResult doUpsert(SObject record) {
return this.doUpsert(new List<SObject>{ record })[0];
}
public List<DatabaseResult> doUpsert(List<SObject> records) {
List<Database.UpsertResult> results = Database.upsert(records);
List<DatabaseResult> wrappedResults = new List<DatabaseResult>();
for (Database.UpsertResult result : results) {
wrappedResults.add(this.logErrors(new UpsertResult(result)));
}
return wrappedResults;
}
private DatabaseResult logErrors(DatabaseResult result) {
if (result.hasError()) {
// using Nebula Logger as an example logging implementation
// look it up using the link above if you're not familiar!
Logger.warn('Error performing DML', result.getErrors());
}
return result;
}
public class SaveResult implements DatabaseResult {
private final Database.SaveResult result;
public SaveResult(Database.SaveResult result) {
this.result = result;
}
public Boolean hasError() {
return this.result.isSuccess() == false;
}
public List<Database.Error> getErrors() {
return this.result.getErrors();
}
}
public class UpsertResult implements DatabaseResult {
private final Database.UpsertResult result;
public UpsertResult(Database.UpsertResult result) {
this.result = result;
}
public Boolean hasError() {
return this.result.isSuccess() == false;
}
public List<Database.Error> getErrors() {
return this.result.getErrors();
}
}
}
Now the public “contract” for our DML
class’s API returns a uniform type — a DML.DatabaseResult
class. This is really interesting! We’ve essentially hidden the Database
namespace classes, like Database.SaveResult
and Database.UpsertResult
from our callers. We’ll talk later about how to make such classes re-accessible, but for now, the contract or “shape” of our API has subtly changed: from something that the platform provides, to something that we have full control over. You’ll start to see through this post just how powerful it can be to be able to quickly iterate and make changes to the shape of an object’s API.
In order to make objects that are part of the standard library in bulk, we need to loop, but we don’t want to have to loop twice (once to create them, once to log). More loops equals more iterations equals more time (the time it takes for your code to execute). Minimizing the time it takes for code to run is a consideration while writing, rewriting, and refactoring — more on that later. In other words, it might seem like a lot to have to write 15ish lines of code per class that implements the DatabaseResult
interface, but as the use of the DML
class radiates outward through a codebase, the value you get out of having a way to consistently refer to all different kinds of Database namespace result classes increases exponentially.
Let’s walk through an example where we add a few methods to the DML.DatabaseResult
interface:
- a
System.Type getType()
method - a
Object getUnderlyingResult()
method (and notes on why onlyObject
will do here)
First, let’s implement the getType()
method:
public class DML {
// an interface can ALSO be a top-level class
// but it's common to see them defined within classes
// as well
public interface DatabaseResult {
Boolean hasError();
List<Database.Error> getErrors();
Type getType();
}
// ... etc
public class SaveResult implements DatabaseResult {
private final Database.SaveResult result;
public SaveResult(Database.SaveResult result) {
this.result = result;
}
public Boolean hasError() {
return this.result.isSuccess() == false;
}
public List<Database.Error> getErrors() {
return this.result.getErrors();
}
public System.Type getType() {
return SaveResult.class;
}
}
public class UpsertResult implements DatabaseResult {
private final Database.UpsertResult result;
public UpsertResult(Database.UpsertResult result) {
this.result = result;
}
public Boolean hasError() {
return this.result.isSuccess() == false;
}
public List<Database.Error> getErrors() {
return this.result.getErrors();
}
public System.Type getType() {
return UpsertResult.class;
}
}
}
There are two different ways to get a System.Type
instance — one is to strongly type the name of the class, as in the above examples, and to use the .class
property on it. The other is to use the Type.forName()
method in the standard Apex library. This differs from Java, where you always have the option of using the .getClass()
method. In Apex, without the use of proper generics, the System.Type
instance for a class and something like an Object
class for getUnderlyingResult()
end up playing nicely together. Let’s implement getUnderlyingResult()
to show why:
public class DML {
// an interface can ALSO be a top-level class
// but it's common to see them defined within classes
// as well
public interface DatabaseResult {
Boolean hasError();
List<Database.Error> getErrors();
Type getType();
Object getUnderlyingResult();
}
// ... etc
public class SaveResult implements DatabaseResult {
private final Database.SaveResult result;
public SaveResult(Database.SaveResult result) {
this.result = result;
}
public Boolean hasError() {
return this.result.isSuccess() == false;
}
public List<Database.Error> getErrors() {
return this.result.getErrors();
}
public System.Type getType() {
return SaveResult.class;
}
public Object getUnderlyingResult() {
return this.result;
}
}
public class UpsertResult implements DatabaseResult {
private final Database.UpsertResult result;
public UpsertResult(Database.UpsertResult result) {
this.result = result;
}
public Boolean hasError() {
return this.result.isSuccess() == false;
}
public List<Database.Error> getErrors() {
return this.result.getErrors();
}
public System.Type getType() {
return UpsertResult.class;
}
public Object getUnderlyingResult() {
return this.result;
}
}
}
When it comes to upserting, the Database.UpsertResult
class has another method on it — isCreated()
returns true if an SObject was inserted, and false if it already existed and was only updated. Now, if we were looking at some calling code for the DML
class, there are two different options for checking that Boolean:
public class SomeOtherClass {
private final DML dml = new DML();
public void someUpsertMethod(List<SObject> recordsToUpsert) {
List<DML.DatabaseResult> upsertResults = this.dml.doUpsert(recordsToUpsert);
List<SObject> insertedRecords = new List<SObject>();
List<SObject> updatedRecords = new List<SObject>();
for (Integer index = 0; index < recordsToUpsert.size(); index++) {
// database results are always returned in the same order
// as the list of records that were passed in
// though there are other, easier options for doing this
// this is the simplest way
SObject upsertedRecord = recordsToUpsert[index];
DML.DatabaseResult result = upsertResults[index];
// option 1: blindly trust
Database.UpsertResult underlyingResult = (Database.UpsertResult) result.getUnderlyingResult();
List<SObject> records = underlyingResult.isCreated() ? insertedRecords : updatedRecords();
records.add(upsertedRecord);
// option 2: trust, but verify
if (underlyingResult.getType() == Database.UpsertResult.class) {
Database.UpsertResult underlyingResult = (Database.UpsertResult) result.getUnderlyingResult();
List<SObject> records = underlyingResult.isCreated() ? insertedRecords : updatedRecords();
records.add(upsertedRecord);
}
}
}
}
Option 1 is more performant — there’s technically no need to check the underlying type, but that “technically” is doing a bit of heavy lifting: so long as the implementation is correct, the if statement is redundant. Defensively programming is filled with guard clauses like the one in Option 2, though, and Option 2 prevents a simple mistake from causing a runtime exception. Learning to balance when to use guard clauses versus how to eliminate the need for them with solid testing is another subject we’ll be spending time on later. For now, I want to do a brief aside, because the casting (the parentheses in the above example) is only necessary because Apex doesn’t currently support generics.
A Brief Foray Into Generics
What are generics, you say? Let’s use an example to show off what generics look like:
public class DML {
// note how we now pass a T argument
// which also represents the return type for
// "getUnderlyingResult()"
public interface DatabaseResult<T> {
Boolean hasError();
List<Database.Error> getErrors();
T getUnderlyingResult();
Type getType();
}
// here, within the "implements" keyword, we pass an inner Type to DatabaseResult
// this should look familiar - we already do this with Lists, Sets, Maps, and for a few other generic
// types in Apex
public class SaveResult implements DatabaseResult<Database.SaveResult> {
private final Database.SaveResult result;
public SaveResult(Database.SaveResult result) {
this.result = result;
}
public Boolean hasError() {
return this.result.isSuccess() == false;
}
public List<Database.Error> getErrors() {
return this.result.getErrors();
}
public System.Type getType() {
return SaveResult.class;
}
public Database.SaveResult getUnderlyingResult() {
return this.result;
}
}
public class UpsertResult implements DatabaseResult<Database.UpsertResult> {
private final Database.UpsertResult result;
public UpsertResult(Database.UpsertResult result) {
this.result = result;
}
public Boolean hasError() {
return this.result.isSuccess() == false;
}
public List<Database.Error> getErrors() {
return this.result.getErrors();
}
public System.Type getType() {
return UpsertResult.class;
}
public Database.UnderlyingResult getUnderlyingResult() {
return this.result;
}
}
}
Which would make the calling code look a lot cleaner:
public class SomeOtherClass {
private final DML dml = new DML();
public void someUpsertMethod(List<SObject> recordsToUpsert) {
List<DML.DatabaseResult> upsertResults = this.dml.doUpsert(recordsToUpsert);
List<SObject> insertedRecords = new List<SObject>();
List<SObject> updatedRecords = new List<SObject>();
for (Integer index = 0; index < recordsToUpsert.size(); index++) {
SObject upsertedRecord = recordsToUpsert[index];
DML.UpsertResult result = upsertResults[index];
// now there's only one option
List<SObject> records = result.getUnderlyingResult().isCreated() ? insertedRecords : updatedRecords();
records.add(upsertedRecord);
}
}
}
If and when Apex ends up getting generics, it will empower two things immediately:
- move an entire class of bugs from runtime errors to compile time errors (because there’s no need to use guard clauses or type-checking when you know the definitive type being returned by something)
- the ability to remove a lot of ugly casting in existing codebases
In the meantime, it’s good to know, at least, what the syntax for generics might look like should the feature ever make its way to the base platform.
Inheritance, Polymorphism & Object Hierarchies
Interfaces define the behavior that objects must have in order to satisfy the compile-time contract necessary for an object to be represented by a specific type. Sometimes interfaces serve to unify objects from vastly different hierarchies by showing you, their user, the methods they have in common. Ideally, you don’t even need to know much about how a specific object is implemented when working with its interface. Inheritance, on the other hand, provides you with a similar set of tools for defining behavior but does so by explicitly relating one object to another.
Consider the simplest possible example from On The Relationship Between Automation & Code:
public class Person {
public String name { get; set; }
public String identifier { get; set; }
// by using the override keyword, we're saying:
// "don't use the Object parent class's implementation,
// instead use this new implementation instead!"
public override String toString() {
return Person.class.getName() + ':[name=' + this.name +']';
}
}
But now lets take things a step further by enhancing the Person
class example:
public abstract class Person {
private final SObject record;
public Person(SObject record) {
this.record = record;
}
public abstract String getEmail();
public abstract String getName();
public Id getId() {
return this.record.Id;
}
}
public class ContactPerson extends Person {
private final Contact con;
// remember, the call to the super class
// MUST come first in a subclass constructor
public ContactPerson(Contact con) {
super(con);
this.con = con;
}
public override String getEmail() {
return this.con.Email;
}
public override String getName() {
return this.con.Name;
}
}
public class LeadPerson extends Person {
private final Lead lead;
public LeadPerson(Lead lead) {
super(lead);
this.lead = lead;
}
public override String getEmail() {
return this.lead.Email;
}
public override String getName() {
return this.lead.Name;
}
}
Now we’ve established a hierarchy:
person
│
└─── ContactPerson
│
│
└─── LeadPerson
Where the parent class, the abstract Person
class, has some default behavior implemented - things like getId()
, since that’s a generic method all SObject
classes have without needing to know their type, but it also specifies methods that its subclasses will need to implement — methods like getEmail()
and getName()
. In some elementary programming classes, you’ll hear talk about the “is a” phrase (LeadPerson
is a Person
) versus “has a” (LeadPerson
has a Lead
). _Is a_
relationships are a means of classification; has a relationships are compositional and don’t imply a direct relationship between objects. In this example, both LeadPerson
and ContactPerson
are instances of the parent class Person
, and as such can be used in calling code without specifically knowing which instance is being used.
Which leads us to the concept of polymorphism (which I’ll work carefully to only define later). Technically, you’ve already seen polymorphism at play multiple times in the examples in this series, but it’s now time to talk explicitly about what it means and why it’s one of the most powerful concepts in our Object-Oriented toolbox. Let’s back up for a second, and show off a bit of code that uses our Person
class as-is:
public class Booking {
// this is set elsewhere
public Person person;
}
public class BookingEngine {
public void book(Person person) {
// do a bunch of confirmation steps and then:
this.sendBookingConfirmationEmail(person);
}
private void sendBookingConfirmationEmail(Person person) {
Messaging.SingleEmailMessage email = new Messaging.SingleEmailMessage();
email.setOrgWideEmailAddressId(UserInfo.getOrganizationId());
email.setPlainTextBody('Congrats! You\'re booked, and in real life you\'d get a much fancier confirmation email!!');
email.setSubject('Your order confirmation');
// polymorphism ahead -> 👇👇👇👇👇👇👇
email.setTargetObjectId(person.getId());
Messaging.sendEmail(new List<Messaging.Email>{ email });
}
}
To be clear — this is already an example of polymorphism. Here, the BookingEngine
class is acting on a Person
object, which itself is encapsulating either a Contact
or a Lead
. But we can go deeper, as they say.
Let’s say that a common customer complaint within this totally hypothetical system is that customers that make an order the very first time they visit our site are confused about how to login. That’s because they’re Leads, and they don’t have a pre-existing account. Even in this relatively “simple” situation, it’s easy to imagine a layered set of requirements that gets handed down to make things better:
- for net-new customers, whose email addresses don’t match anything else in our system, send two emails: the existing booking confirmation one, and a new one dedicated to explaining how web account setup can be done
- for customers checking out as a guest, if their email matches an existing account, only send the booking confirmation
- if there’s already a non-converted Lead whose email matches the one being submitted, merge the new Lead and the existing one
These requirements aren’t meant to be exhaustive, but rather to show how even “simple” requests carry nuance and complexity worth thinking about.
It’s tempting to “simply” alter our BookingEngine
method and move on with our lives, and that’s the kind of “small change” that proliferate throughout codebases globally:
public class Booking {
// this is set elsewhere
public Person person;
}
public class BookingEngine {
public void book(Person person) {
// do a bunch of confirmation steps and then:
this.sendBookingConfirmationEmail(person);
}
private void sendBookingConfirmationEmail(Person person) {
List<Messaging.Email> emails = new List<Messaging.Email>();
Messaging.SingleEmailMessage newBookingEmail = this.getEmail(person);
newBookingEmail.setPlainTextBody('Congrats! You\'re booked, and in real life you\'d get a much fancier confirmation newBookingEmail!!');
newBookingEmail.setSubject('Your order confirmation');
emails.add(newBookingEmail);
// 👇👇👇 procedural new code here 👇👇👇
if (person instanceof LeadPerson && this.doesNotExist(person.getEmail())) {
Messaging.SingleEmailMessage newAccountEmail = this.getEmail(person);
newAccountEmail.setPlainTextBody('Let\'s walk you through how to set up your new account...');
newAccountEmail.setSubject('Your login information');
emails.add(newAccountEmail);
}
Messaging.sendEmail(emails);
}
private Messaging.SingleEmailMessage getEmail(Person person) {
Messaging.SingleEmailMessage email = new Messaging.SingleEmailMessage();
email.setOrgWideEmailAddressId(UserInfo.getOrganizationId());
email.setTargetObjectId(person.getId());
return email;
}
private Boolean doesNotExist(String email) {
return [SELECT COUNT() FROM Contact WHERE Email = :email] == 0;
}
}
There are a number of reasons to not do that, though — procedural code, like the above, adds complexity to the existing method, even though we’ve refactored (like good citizens do) while adding functionality. But in pure programming terms, it’s not just that the sendBookingConfirmationEmail()
method has some undocumented side-effect; it’s that it knows too much about LeadPerson
. Let’s take a look at a more polymorphic approach to this exact same code:
public abstract class Person {
private final SObject record;
public Person(SObject record) {
this.record = record;
}
// new! 👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇
public abstract List<Messaging.SingleEmailMessage> getBookingEmails();
public abstract String getEmail();
public abstract String getName();
public Id getId() {
return this.record.Id;
}
// getEmail has now been "promoted"
protected Messaging.SingleEmailMessage getEmail() {
Messaging.SingleEmailMessage email = new Messaging.SingleEmailMessage();
email.setOrgWideEmailAddressId(UserInfo.getOrganizationId());
email.setTargetObjectId(this.getId());
return email;
}
public virtual override List<Messaging.SingleEmailMessage> getBookingEmails() {
Messaging.SingleEmailMessage newBookingEmail = this.getEmail();
newBookingEmail.setPlainTextBody('Congrats! You\'re booked, and in real life you\'d get a much fancier confirmation newBookingEmail!!');
newBookingEmail.setSubject('Your order confirmation');
return new List<Messaging.SingleEmailMessage>{ newBookingEmail };
}
}
public class ContactPerson extends Person {
private final Contact con;
public ContactPerson(Contact con) {
super(con);
this.con = con;
}
public override String getEmail() {
return this.con.Email;
}
public override String getName() {
return this.con.Name;
}
}
public class LeadPerson extends Person {
private final Lead lead;
public LeadPerson(Lead lead) {
super(lead);
this.lead = lead;
}
// calling "super" on a method uses the implementation from the parent class.
// while this isn't always the cleanest approach - does a subclass end up knowing
// too much about its parent? - it sometimes is the easiest way to avoid having to
// use the same subject line in two different places. As with all things,
// being aware of the tradeoffs (cleanliness? knowing too much?) can help you
// make the best decision when the way forward isn't clear
public override List<Messaging.SingleEmailMessage> getBookingEmails() {
List<Messaging.SingleEmailMessage> bookingEmails = super.getBookingEmails();
if (this.doesNotExist()) {
Messaging.SingleEmailMessage newAccountEmail = this.getEmail(person);
newAccountEmail.setPlainTextBody('Let\'s walk you through how to set up your new account...');
newAccountEmail.setSubject('Your login information');
bookingEmails.add(newAccountEmail);
}
return bookingEmails;
}
public override String getEmail() {
return this.lead.Email;
}
public override String getName() {
return this.lead.Name;
}
// not bulk safe, to be clear
private Boolean doesNotExist() {
return [SELECT COUNT() FROM Contact WHERE Email = :this.getEmail()] == 0;
}
}
Then, in the booking service, our code just looks like:
public class BookingEngine {
public void book(Person person) {
// do a bunch of confirmation steps and then:
Messaging.sendEmail(person.getBookingEmails());
}
}
Polymorphic code tends to make calling code cleaner, but it also has the effect of “hiding” away the various implementations of getBookingEmails()
. While this example is purely to show how polymorphism can aid in readability, it’s important to note that sometimes procedural code is easier to read; that, sometimes, the idea of a Person
“knowing” about which emails it should be sent after booking may not be the right abstraction.
Abstraction
What is abstraction, anyway? Colloquially, you’ll find that the word “abstraction” tends to get thrown around in a similar vein to properties on objects; that is, people tend to use the word to refer to a broader array of subjects than the meaning of the word itself. And that’s OK — much like when people use the word “property” when referring to an object’s field(s), people tend to overuse the word “abstraction” because it’s a really useful concept that can be applied broadly.
If I were to define abstraction in my own words, here’s what I would say:
Abstraction is the process of “lifting up” concepts such that the concept in question is represented at a higher level — be it with types, or with functions
So what does that mean, exactly? Let’s take the Person
class example from above. We can generally say a few things about Person
:
- it abstracts away the concept of which Salesforce object is being used when in a booking context
- it has an opinion about which emails are going to be sent to a person when they’re checking out
The “lifting up” of state from SObject representations to a single type is an example of working with an abstraction. Is every example of polymorphism an example of an abstraction? Yes. Perhaps the most oft-quoted passage on the subject serves perfectly to describe what is and isn’t an abstraction:
The essence of abstraction is preserving information that is relevant in a given context, and forgetting information that is irrelevant in that context. - John Guttag
This is why interface usage is almost always an abstraction, but there are cases when a virtual or abstract class is not being used as an abstraction: code reuse for the sake of cutting down on lines of code does not an abstraction make. For example:
public virtual class Utils {
public virtual String getFormattedString(String input) {
// replace new lines
return input.replace('\n', ' ');
}
}
public class UtilsOveride extends Utils {
public override String getFormattedString(String input) {
// replace tabs
return input.replace('\t', ' ');
}
}
What can we take away from such an implementation? Not much. I’ve seen classes like this out in the wild, often while consulting; this isn’t really abstraction, but an attempt at code reuse. Learning to recognize the difference is an important part of being able to ascribe intent within your code. Sometimes a comment goes a long way, especially when the code itself is cryptic. I’d hope for a comment more explanatory in nature than the peremptory ones I’ve left within this example if I were to find Utils
out in the wild. Generating the ability to easily re-use code is not necessarily an abstraction. There are plenty of great Utils
-type classes out there, and in particular I’ve long been a fan of collection-type utilities that allow you more easily work with Sets, Maps, and List classes, but utilities are not an abstraction — on the other hand, LazyIterator is a great example of an abstraction that sits on top of collections themselves without “just” being a toolbox. It’s also a great example of the throughline between functional and Object-oriented programming, in that it’s possible to do both within Apex.
Remember — code encapsulates concepts. Abstractions are higher ordered concepts that share the relevant parts of a concept itself as an object’s “shape”.
Conclusion
This wraps up the four part series on Object-oriented programming. The fundamentals are an incredibly important part of understanding how to model objects — their APIs, their structure, and their consistency. Modeling intent as code increases in importance as audience size scales up. The more people that have the potential to put eyes on code, the more paramount it is that intent is the singular focusing lens through which information is transmitted. Consistency also increases in importance linearly with audience size. This is one of the biggest reasons why I advocate for automatic code formatting using tools such as Prettier — while I don’t always agree with the way Prettier chooses to format code, I’d much rather spend my time reading and writing code than formatting code and finding formatting issues within my code and other peoples’ code. Auto formatting tools turn what was previously a set of stylistic opinions into something enforceable at the code editor level.
In truth, this is just a diving board for beginners — a foundation which will, in turn, empower you to approach problems as something waiting to be solved. It will also, I hope, make you wary of solutions in search of problems — another thing that’s prevalent throughout hustle culture, in my experience. I’ve learned a fair deal myself, while writing this, having to re-acquiant myself with things like initializers, the differences between properties and fields, the best way to approach interfaces and classes, etc…
It’s my hope that there was something for everybody in these posts: a real chance to learn something new, regardless of your experience level. As always, thanks for reading the Joys of Apex, and if this was helpful for you, remember that there’s a ton of other always-free content here that extends across the last six years of my own programming journey. Till next time!