afv-library/skills/generating-apex/references/solid-principles.md

402 lines
10 KiB
Markdown

# SOLID Principles in Apex
## Overview
SOLID principles guide object-oriented design for maintainable, flexible code.
| Principle | Summary |
|-----------|---------|
| **S**ingle Responsibility | One reason to change |
| **O**pen/Closed | Open for extension, closed for modification |
| **L**iskov Substitution | Subtypes must be substitutable |
| **I**nterface Segregation | Small, specific interfaces |
| **D**ependency Inversion | Depend on abstractions |
---
## S - Single Responsibility Principle
> "A module should have one, and only one, reason to change."
### Problem: Multiple Responsibilities
```apex
// BAD: Class has multiple reasons to change
public class OrderProcessor {
public void processOrder(Order__c order) {
// Validate order (reason 1: validation rules change)
if (order.Total__c <= 0) {
throw new ValidationException('Invalid total');
}
// Calculate tax (reason 2: tax rules change)
Decimal tax = order.Total__c * 0.08;
order.Tax__c = tax;
// Send email (reason 3: notification requirements change)
Messaging.SingleEmailMessage email = new Messaging.SingleEmailMessage();
email.setToAddresses(new List<String>{order.Customer_Email__c});
Messaging.sendEmail(new List<Messaging.Email>{email});
// Save to database (reason 4: persistence logic changes)
update order;
}
}
```
### Solution: Separate Responsibilities
```apex
// GOOD: Each class has single responsibility
public class OrderValidator {
public void validate(Order__c order) {
if (order.Total__c <= 0) {
throw new ValidationException('Invalid total');
}
}
}
public class TaxCalculator {
public Decimal calculate(Decimal amount) {
return amount * 0.08;
}
}
public class OrderNotificationService {
public void sendConfirmation(Order__c order) {
// Email logic
}
}
public class OrderService {
private OrderValidator validator;
private TaxCalculator taxCalc;
private OrderNotificationService notifier;
public void processOrder(Order__c order) {
validator.validate(order);
order.Tax__c = taxCalc.calculate(order.Total__c);
update order;
notifier.sendConfirmation(order);
}
}
```
---
## O - Open/Closed Principle
> "Software entities should be open for extension, but closed for modification."
### Problem: Modifying Existing Code
```apex
// BAD: Must modify class to add new discount type
public class DiscountCalculator {
public Decimal calculate(String discountType, Decimal amount) {
if (discountType == 'PERCENTAGE') {
return amount * 0.1;
} else if (discountType == 'FIXED') {
return 50;
} else if (discountType == 'VIP') { // Added later
return amount * 0.2;
}
// Keep adding else-if for each new type...
return 0;
}
}
```
### Solution: Extend Without Modifying
```apex
// GOOD: Add new discount types without changing existing code
public interface DiscountStrategy {
Decimal calculate(Decimal amount);
}
public class PercentageDiscount implements DiscountStrategy {
private Decimal rate;
public PercentageDiscount(Decimal rate) {
this.rate = rate;
}
public Decimal calculate(Decimal amount) {
return amount * rate;
}
}
public class FixedDiscount implements DiscountStrategy {
private Decimal fixedAmount;
public FixedDiscount(Decimal fixedAmount) {
this.fixedAmount = fixedAmount;
}
public Decimal calculate(Decimal amount) {
return fixedAmount;
}
}
// To add VIP discount: create new class, no modification needed
public class VIPDiscount implements DiscountStrategy {
public Decimal calculate(Decimal amount) {
return amount * 0.2;
}
}
public class DiscountCalculator {
private Map<String, DiscountStrategy> strategies;
public Decimal calculate(String type, Decimal amount) {
DiscountStrategy strategy = strategies.get(type);
return strategy?.calculate(amount) ?? 0;
}
}
```
### Real-World Example: Trigger Actions Framework
TAF follows OCP - add new behaviors via metadata configuration without modifying the trigger or handler.
---
## L - Liskov Substitution Principle
> "Subtypes must be substitutable for their base types."
### Problem: Subtype Breaks Contract
```apex
// BAD: Lead violates SObject update contract when converted
public class RecordUpdater {
public void updateRecord(SObject record) {
// This fails for converted Leads!
if (record instanceof Lead) {
Lead l = (Lead)record;
if ([SELECT IsConverted FROM Lead WHERE Id = :l.Id].IsConverted) {
return; // Can't update converted lead
}
}
update record;
}
}
```
### Solution: Design for Substitutability
```apex
// GOOD: Interface defines clear contract
public interface Updatable {
Boolean canUpdate();
void performUpdate();
}
public class AccountUpdater implements Updatable {
private Account record;
public Boolean canUpdate() {
return true; // Accounts can always be updated
}
public void performUpdate() {
update record;
}
}
public class LeadUpdater implements Updatable {
private Lead record;
public Boolean canUpdate() {
return ![SELECT IsConverted FROM Lead WHERE Id = :record.Id].IsConverted;
}
public void performUpdate() {
if (canUpdate()) {
update record;
}
}
}
// Consumer doesn't need type checking
public class RecordService {
public void updateRecord(Updatable record) {
if (record.canUpdate()) {
record.performUpdate();
}
}
}
```
---
## I - Interface Segregation Principle
> "Clients should not be forced to depend on interfaces they don't use."
### Problem: Fat Interface
```apex
// BAD: Interface forces unnecessary implementations
public interface RecordProcessor {
void validate(SObject record);
void calculate(SObject record);
void sendNotification(SObject record);
void createAuditLog(SObject record);
void syncToExternal(SObject record);
}
// Simple processor forced to implement everything
public class SimpleProcessor implements RecordProcessor {
public void validate(SObject record) { /* actual logic */ }
public void calculate(SObject record) { /* actual logic */ }
// Forced to implement these even though not needed
public void sendNotification(SObject record) { }
public void createAuditLog(SObject record) { }
public void syncToExternal(SObject record) { }
}
```
### Solution: Small, Focused Interfaces
```apex
// GOOD: Segregated interfaces
public interface Validatable {
void validate(SObject record);
}
public interface Calculable {
void calculate(SObject record);
}
public interface Notifiable {
void sendNotification(SObject record);
}
public interface Auditable {
void createAuditLog(SObject record);
}
// Implement only what you need
public class SimpleProcessor implements Validatable, Calculable {
public void validate(SObject record) { /* logic */ }
public void calculate(SObject record) { /* logic */ }
}
public class FullProcessor implements Validatable, Calculable, Notifiable, Auditable {
public void validate(SObject record) { /* logic */ }
public void calculate(SObject record) { /* logic */ }
public void sendNotification(SObject record) { /* logic */ }
public void createAuditLog(SObject record) { /* logic */ }
}
```
### Salesforce Example: Database.Batchable Options
```apex
// Implement only what you need
public class SimpleBatch implements Database.Batchable<SObject> {
// Just the required interface
}
public class StatefulBatch implements Database.Batchable<SObject>, Database.Stateful {
// Add stateful when needed
}
public class CalloutBatch implements Database.Batchable<SObject>, Database.AllowsCallouts {
// Add callouts when needed
}
```
---
## D - Dependency Inversion Principle
> "High-level modules should not depend on low-level modules. Both should depend on abstractions."
### Problem: Direct Dependencies
```apex
// BAD: High-level class depends on concrete implementation
public class OrderService {
private EmailService emailService; // Concrete class
private StripePaymentGateway gateway; // Concrete class
public OrderService() {
this.emailService = new EmailService();
this.gateway = new StripePaymentGateway();
}
public void processOrder(Order__c order) {
gateway.charge(order.Total__c);
emailService.send(order.Customer_Email__c);
}
}
```
### Solution: Depend on Abstractions
```apex
// GOOD: Depend on interfaces, inject implementations
public interface PaymentGateway {
PaymentResult charge(Decimal amount);
}
public interface NotificationService {
void send(String recipient, String message);
}
public class StripeGateway implements PaymentGateway {
public PaymentResult charge(Decimal amount) {
// Stripe-specific logic
}
}
public class EmailNotification implements NotificationService {
public void send(String recipient, String message) {
// Email-specific logic
}
}
// High-level class depends on abstractions
public class OrderService {
private PaymentGateway gateway;
private NotificationService notifier;
// Constructor injection
public OrderService(PaymentGateway gateway, NotificationService notifier) {
this.gateway = gateway;
this.notifier = notifier;
}
public void processOrder(Order__c order) {
gateway.charge(order.Total__c);
notifier.send(order.Customer_Email__c, 'Order confirmed');
}
}
// Easy to test with mocks
@isTest
static void testOrderService() {
PaymentGateway mockGateway = new MockPaymentGateway();
NotificationService mockNotifier = new MockNotificationService();
OrderService service = new OrderService(mockGateway, mockNotifier);
// Test without real payment or email
}
```
---
## Summary
| Principle | Violation Sign | Solution |
|-----------|---------------|----------|
| SRP | Class has multiple reasons to change | Split into focused classes |
| OCP | Adding features requires modifying existing code | Use strategy pattern, interfaces |
| LSP | Type checking before using base type | Redesign hierarchy, use composition |
| ISP | Empty method implementations | Split into smaller interfaces |
| DIP | Creating concrete dependencies in constructor | Inject dependencies via constructor |