6.4 Classes, Interfaces & OOP Patterns
Key Takeaways
- Apex classes group methods and state; access modifiers include private, public, global, and protected (with inheritance nuances)
- static members belong to the type; instance members belong to an object—triggers and utilities often use static service methods
- Interfaces define method contracts that implementing classes must provide—useful for polymorphism and test doubles
- abstract classes provide partial implementation; inner classes nest helper types; sharing keywords control record access enforcement
- Service-layer and trigger-handler patterns keep triggers thin: one-line trigger delegates to a handler/service class
6.4 Classes, Interfaces & OOP Patterns
Quick Answer: Production Apex is organized into classes with clear access modifiers, static vs instance members, and often interfaces for contracts. Triggers should stay thin and call handler/service classes. with sharing / without sharing / inherited sharing control whether record sharing rules apply to Apex—details deepen in the security chapter; here you need the vocabulary and default intent.
Platform Developer I expects more than syntax: you must choose structures that stay bulk-safe, testable, and maintainable under Salesforce packaging and security rules.
Classes and Methods
public with sharing class AccountService {
public static void renumber(List<Account> accounts) {
// bulk logic
}
public Account findPrimary(Id accountId) {
return [SELECT Id, Name FROM Account WHERE Id = :accountId LIMIT 1];
}
}
A class encapsulates related behavior. Methods can return values or be void. Overloading (same method name, different parameter lists) is supported. Constructors initialize instance state:
public class PricingEngine {
private final Decimal taxRate;
public PricingEngine(Decimal taxRate) {
this.taxRate = taxRate;
}
public Decimal totalWithTax(Decimal net) {
return net * (1 + taxRate);
}
}
Access Modifiers
| Modifier | Visibility |
|---|---|
| private | Only the containing class (default for methods/variables if unspecified in many contexts—know class vs member defaults for the exam) |
| public | Any Apex in the application (same namespace) |
| global | Any Apex that can see the class, including across namespaces/packages—use sparingly for managed package APIs |
| protected | Containing class and subclasses (inner/subclass patterns) |
Top-level classes you save as Apex class metadata are typically public or global. Inner classes can use additional nesting visibility rules.
Exam mindset: Prefer the narrowest modifier that still works. global is for published APIs in packages, not everyday org code. Service methods used only inside the org are usually public (or @TestVisible private for tests).
static vs Instance
| static | instance | |
|---|---|---|
| Belongs to | The class itself | A specific object |
| Memory | One shared copy per transaction type usage | Per constructed instance |
| Call style | AccountService.renumber(list) | engine.totalWithTax(100) |
| Typical use | Utilities, trigger entry points, caches for the transaction | Engines with configuration state |
Static variables persist for the duration of the Apex transaction and are a common way to implement recursion guards in triggers:
public class TriggerGuard {
public static Boolean hasRun = false;
}
Use static flags carefully—tests and bulk multiple operations can leave unexpected state if you do not reset in test setup or design clearer handler entry points.
Interfaces and Implementation
An interface defines method signatures without bodies. Classes implement the contract:
public interface IDiscountPolicy {
Decimal apply(Decimal amount);
}
public class PercentOffPolicy implements IDiscountPolicy {
private final Decimal percent;
public PercentOffPolicy(Decimal percent) { this.percent = percent; }
public Decimal apply(Decimal amount) {
return amount * (1 - percent);
}
}
public class NoDiscountPolicy implements IDiscountPolicy {
public Decimal apply(Decimal amount) { return amount; }
}
Callers depend on IDiscountPolicy, not a concrete class—swap implementations for regions, products, or tests. Interfaces are also how many platform features plug in (for example, batchable/queueable/schedulable style contracts you will meet in async chapters).
Abstract Classes (High Level)
An abstract class can mix implemented methods with abstract methods subclasses must provide. Use when implementations share substantial code but differ in one or more steps (template-style design). Prefer an interface when you only need a pure contract with no shared code; prefer abstract class when shared helpers justify a base type.
You cannot construct an abstract class directly—only concrete subclasses.
Inner Classes
Classes can nest helper types:
public class OrderService {
public class LineRequest {
public Id productId;
public Decimal quantity;
}
public static void place(List<LineRequest> lines) { /* ... */ }
}
Inner classes organize DTOs and small helpers next to the outer service without proliferating top-level metadata files. They are still subject to governor limits—nesting is structural, not a sandbox escape.
with sharing / without sharing / inherited sharing
These class-level keywords control record-level sharing enforcement for SOQL/DML in that class (field- and object-level security are separate topics).
| Keyword | Meaning (intro level) |
|---|---|
| with sharing | Enforce the current user’s sharing rules |
| without sharing | Bypass sharing rules (still not a full “system mode” for FLS/CRUD unless you also handle those) |
| inherited sharing | Inherit the caller’s sharing context—preferred default for many reusable services |
public with sharing class SecureAccountSelector { /* ... */ }
public without sharing class NightlyCleanupService { /* elevated data access by design */ }
public inherited sharing class AccountService { /* respects caller */ }
Exam caution: without sharing is not a shortcut for “ignore all security.” It is intentional elevation for system-style processes and must be justified. Deeper CRUD/FLS, stripInaccessible, and user-mode DB operations appear in the security chapter—here, remember which keyword enforces sharing vs bypasses it.
If a class omits a sharing declaration, historical default behavior is effectively without sharing for the class—modern best practice is to declare inherited sharing or with sharing explicitly so intent is obvious.
Service Layer and Trigger-Handler Patterns (Overview)
Problem: Fat triggers mix bulkification, business rules, and recursion control in one untestable file.
Pattern:
trigger ContactTrigger on Contact (before insert, before update) {
ContactTriggerHandler.run();
}
public with sharing class ContactTriggerHandler {
public static void run() {
if (Trigger.isBefore && Trigger.isInsert) {
ContactService.applyDefaults(Trigger.new);
}
if (Trigger.isBefore && Trigger.isUpdate) {
ContactService.validateChanges(Trigger.new, Trigger.oldMap);
}
}
}
public inherited sharing class ContactService {
public static void applyDefaults(List<Contact> contacts) { /* bulk logic */ }
public static void validateChanges(List<Contact> contacts, Map<Id, Contact> oldMap) { /* ... */ }
}
Roles:
- Trigger — routing only (context checks, one call)
- Handler — maps trigger context to service methods; may hold recursion guards
- Service — pure-ish business operations that accept Lists/Maps and are unit-testable without full trigger setup (though integration tests still use
Test.startTestpatterns)
This separation appears conceptually on the exam even when exact class names differ. Choose answers that keep bulk lists, avoid logic duplication across events, and centralize rules in classes rather than copy-paste in multiple triggers.
Putting OOP Choices on the Exam
Ask for each scenario:
- Does this need shared state for the call (instance) or a stateless utility (static)?
- Is a contract needed for multiple implementations (interface)?
- Must the code respect the running user’s sharing (with/inherited sharing)?
- Is logic living in a trigger body that should move to a handler/service?
Mastering these patterns prepares you for triggers, async Apex, and testing chapters that assume class-structured code rather than anonymous-style scripts.
A managed package must expose an Apex API that subscriber orgs can call from their own Apex. Which access modifier is required on the class/methods intended for that cross-namespace use?
Which statement best describes the with sharing keyword on an Apex class?
What is the primary benefit of a thin trigger that delegates to a handler/service class?