DEV Community

Cover image for The Art of Abstraction: When to Use Interfaces and Abstract Classes
Anas Hussain
Anas Hussain

Posted on

The Art of Abstraction: When to Use Interfaces and Abstract Classes

1. Hook & Problem Statement

You're building a Laravel application. You type-hint PaymentGatewayInterface in your controller constructor. It works. You move on.

Later, you're reading through a senior developer's code and see:

abstract class PaymentGateway implements PaymentGatewayInterface { ... }
class StripeGateway extends PaymentGateway { ... }
Enter fullscreen mode Exit fullscreen mode

You pause. "Wait," you think. "What's the difference between an interface and an abstract class? Why use one over the other? When should I use both?"

You've used interfaces before. You've seen abstract classes in Laravel (Model, ServiceProvider, Request). But you've never been able to clearly articulate when and why to use each.

This confusion is common. Most developers use interfaces and abstract classes without understanding the fundamental distinction. They're just "things that make my code more OOP." But they serve entirely different purposes.

The difference is subtle but profound.

An interface is a contract. An abstract class is a partial implementation. One tells you what to do. The other helps you how to do it.

Understanding this distinction is the difference between writing code that works and designing systems that scale. It's the difference between a junior developer who follows patterns and a senior architect who creates them.


2. Why This Pattern/Concept Exists

The Software Engineering Problem It Solves

Abstraction solves the problem of managing complexity by hiding implementation details. It allows you to define what a system does without specifying how it does it.

In large systems, you need to:

  1. Define contracts that multiple implementations must follow.
  2. Provide base functionality that implementations can reuse.
  3. Enforce structure while allowing flexibility.

The Pain That Existed Before

Before interfaces and abstract classes (or in codebases that don't use them), developers faced:

  1. Tight Coupling to Concretions: Code depended on specific classes, making it impossible to swap implementations.
  2. Code Duplication: Similar logic was copy-pasted across multiple classes because there was no way to share base functionality.
  3. No Contract Enforcement: There was no way to guarantee that different implementations had the same methods.
  4. Fragile Inheritance: When inheritance was used for everything, changes cascaded unpredictably. #### Why Large Applications Need It

As applications grow, so does the need for structured abstraction:

  • Contracts (Interfaces): Allow teams to define a clear contract that multiple implementations must follow. This enables polymorphism and dependency inversion.
  • Base Functionality (Abstract Classes): Allow teams to share common logic across multiple implementations, reducing duplication and ensuring consistency.
  • Evolution: Abstractions allow you to evolve a system without breaking existing code. You can add new implementations without changing the code that uses them.

The core insight: Interfaces define what can be done. Abstract classes define how something is done. One is about specification, the other about implementation.


3. Real World Analogy

The Restaurant Menu

Imagine a restaurant chain with multiple locations.

Interfaces (The Menu):
Every restaurant in the chain has a menu (interface). The menu defines what dishes are available (methods): burger, pizza, salad. Customers (client code) know they can order these items at any location.

  • Menu: The interface (defines what is available).
  • Burger, Pizza, Salad: The methods (contract).
  • Customer: The client code that depends on the interface.

Abstract Classes (The Recipe Book):
The chain has a recipe book (abstract class) that provides how to make the dishes. It includes:

  • Common techniques for all dishes.
  • Specific ingredients (properties).
  • Standardized procedures (methods).

The recipes define:

  • A prepareMeal() method with standard steps (base functionality).
  • Abstract methods like cookMainComponent() that each location implements differently.

Concrete Classes (The Actual Restaurants):
Each location (Stripe, PayPal, etc.):

  • Follows the menu (implements the interface).
  • Uses the recipe (extends the abstract class).
  • Customizes the details (implements abstract methods).

Analogy Mapping:

  • Menu: Interface (what you can do).
  • Recipe Book: Abstract class (how to do it with shared logic).
  • Actual Restaurant: Concrete class (specific implementation).
  • Customer: Client code.
  • Chef's Special Technique: Private/protected methods (implementation details).

Why it works:

  • The interface guarantees every location has the same offerings.
  • The abstract class ensures consistency in preparation.
  • Each location can customize the specifics.

4. The Pain (Bad Design)

Let's look at a common scenario: an e-commerce application with different payment gateways, implemented without proper abstraction.

// No abstraction - everything is concrete
class StripePayment
{
    private string $secretKey;
    private string $currency;
    private array $transactionLog = [];

    public function __construct(string $secretKey, string $currency = 'USD')
    {
        $this->secretKey = $secretKey;
        $this->currency = $currency;
    }

    public function charge(float $amount, array $customer): array
    {
        // 30 lines of Stripe-specific logic
        $stripe = new \Stripe\StripeClient($this->secretKey);

        try {
            $intent = $stripe->paymentIntents->create([
                'amount' => $amount * 100,
                'currency' => $this->currency,
                'customer' => $customer['stripe_id'],
                'payment_method' => $customer['payment_method_id'],
            ]);

            $this->logTransaction('charge', $intent->id, $amount);

            return [
                'success' => true,
                'transaction_id' => $intent->id,
                'status' => $intent->status,
            ];
        } catch (\Exception $e) {
            $this->logTransaction('charge_failed', null, $amount, $e->getMessage());
            throw $e;
        }
    }

    public function refund(string $transactionId, float $amount): array
    {
        // 15 lines of Stripe-specific refund logic
        $stripe = new \Stripe\StripeClient($this->secretKey);
        $refund = $stripe->refunds->create([
            'payment_intent' => $transactionId,
            'amount' => $amount * 100,
        ]);

        $this->logTransaction('refund', $refund->id, $amount);

        return ['success' => true, 'refund_id' => $refund->id];
    }

    private function logTransaction(string $type, ?string $id, float $amount, ?string $error = null): void
    {
        $this->transactionLog[] = [
            'type' => $type,
            'id' => $id,
            'amount' => $amount,
            'error' => $error,
            'timestamp' => date('Y-m-d H:i:s'),
        ];
    }

    public function getTransactionLog(): array
    {
        return $this->transactionLog;
    }
}

class PayPalPayment
{
    private string $clientId;
    private string $clientSecret;
    private array $transactionLog = [];

    public function __construct(string $clientId, string $clientSecret)
    {
        $this->clientId = $clientId;
        $this->clientSecret = $clientSecret;
    }

    public function charge(float $amount, array $customer): array
    {
        // 30 lines of PayPal-specific logic
        $apiContext = new \PayPal\Rest\ApiContext(
            new \PayPal\Auth\OAuthTokenCredential($this->clientId, $this->clientSecret)
        );

        $payment = new \PayPal\Api\Payment();
        $payment->setIntent('sale')
            ->setPayer(['payment_method' => 'paypal'])
            ->setTransactions([[
                'amount' => [
                    'total' => $amount,
                    'currency' => 'USD',
                ],
                'description' => 'Payment',
            ]]);

        try {
            $payment->create($apiContext);

            $this->logTransaction('charge', $payment->getId(), $amount);

            return [
                'success' => true,
                'transaction_id' => $payment->getId(),
                'approval_url' => $this->getApprovalUrl($payment),
            ];
        } catch (\Exception $e) {
            $this->logTransaction('charge_failed', null, $amount, $e->getMessage());
            throw $e;
        }
    }

    public function refund(string $transactionId, float $amount): array
    {
        // 15 lines of PayPal-specific refund logic
        // Different API calls, different parameters
        return ['success' => true];
    }

    private function getApprovalUrl($payment): ?string
    {
        foreach ($payment->getLinks() as $link) {
            if ($link->getRel() === 'approval_url') {
                return $link->getHref();
            }
        }
        return null;
    }

    private function logTransaction(string $type, ?string $id, float $amount, ?string $error = null): void
    {
        $this->transactionLog[] = [
            'type' => $type,
            'id' => $id,
            'amount' => $amount,
            'error' => $error,
            'timestamp' => date('Y-m-d H:i:s'),
        ];
    }

    public function getTransactionLog(): array
    {
        return $this->transactionLog;
    }
}

// Client code
class PaymentController
{
    public function charge(Request $request)
    {
        $paymentMethod = $request->input('payment_method');

        if ($paymentMethod === 'stripe') {
            $payment = new StripePayment(config('services.stripe.secret'));
            $result = $payment->charge($request->amount, $request->customer);
        } elseif ($paymentMethod === 'paypal') {
            $payment = new PayPalPayment(
                config('services.paypal.client_id'),
                config('services.paypal.secret')
            );
            $result = $payment->charge($request->amount, $request->customer);
        } else {
            throw new \Exception('Unsupported payment method');
        }

        return response()->json($result);
    }
}
Enter fullscreen mode Exit fullscreen mode

Why This Is Terrible

  1. No Contract: No guarantee that both classes have the same methods.
  2. Duplicated Code: Logging logic is identical but repeated.
  3. Tight Coupling: The controller must know about every payment class.
  4. No Extensibility: Adding a new method requires finding all implementations.
  5. Violates SOLID:
    • OCP: Adding a new payment method requires modifying the controller.
    • DIP: Controller depends on concrete classes, not abstractions.
    • DRY: Logging logic is duplicated.

Why Developers Write Code Like This

  • They think "I'll just add one more payment method."
  • They don't plan for abstraction.
  • They don't understand the power of interfaces and abstract classes.
  • They're afraid of "over-engineering."

5. Solution Overview

Abstraction is the process of hiding implementation details and exposing only essential features. In PHP, abstraction is achieved through:

  • Interfaces: Pure contracts (no implementation).
  • Abstract Classes: Partial implementations (some concrete, some abstract).

Core Idea

  • Interfaces define what an object can do. They are contracts.
  • Abstract classes define how an object does it, providing shared logic.

Main Participants

  1. Interface: The contract that all implementations must follow.
  2. Abstract Class: The base implementation with shared logic.
  3. Concrete Classes: The specific implementations.
  4. Client: The code that depends on the abstraction.

How Objects Collaborate

Interface (Contract)
    ↓
Abstract Class (Base Implementation)
    ↓
Concrete Class (Specific Implementation)
    ↓
Client (Uses the abstraction)
Enter fullscreen mode Exit fullscreen mode

Mental Model

Interfaces are like job descriptions. They define what a person (object) needs to be able to do, not how they do it.

Abstract classes are like employee handbooks. They provide standard procedures, guidelines, and shared resources that all employees (objects) can use, while still allowing each employee to perform their specific duties.

Benefits

  • Flexibility: Swap implementations easily.
  • Testability: Mock dependencies using interfaces.
  • Reusability: Share common logic via abstract classes.
  • Maintainability: Changes are isolated.

Trade-offs

  • Complexity: More files and abstractions.
  • Learning Curve: Developers must understand abstraction hierarchies.
  • Over-engineering: Unnecessary abstractions can complicate simple code.

6. UML Diagram

Laravel Service Container Architecture

Dependency Injection Diagram

Diagram Explanation

  1. PaymentInterface defines the contract (what methods exist).
  2. AbstractPayment provides shared implementation (currency, logging).
  3. Concrete classes extend the abstract class and implement specific logic.
  4. PaymentController depends only on the interface.
  5. Abstract methods (calculateFee, formatCurrency) force concrete classes to implement payment-specific logic.

7. Vanilla PHP Example

Let's refactor the payment system using proper abstraction.

Before Refactoring (No Abstraction)

(The terrible code shown above)

After Refactoring (Interfaces + Abstract Classes)

Step 1: Define the Interface (The Contract)
interface PaymentInterface
{
    public function charge(float $amount, array $customer): array;
    public function refund(string $transactionId, float $amount): array;
    public function getTransactionLog(): array;
    public function calculateFee(float $amount): float;
    public function formatCurrency(float $amount): string;
}
Enter fullscreen mode Exit fullscreen mode
Step 2: Define the Abstract Class (Shared Logic)
abstract class AbstractPayment implements PaymentInterface
{
    protected string $currency;
    protected array $transactionLog = [];

    public function __construct(string $currency = 'USD')
    {
        $this->currency = $currency;
    }

    // Shared implementation
    protected function logTransaction(string $type, ?string $id, float $amount, ?string $error = null): void
    {
        $this->transactionLog[] = [
            'type' => $type,
            'id' => $id,
            'amount' => $amount,
            'error' => $error,
            'timestamp' => date('Y-m-d H:i:s'),
            'currency' => $this->currency,
        ];
    }

    // Concrete implementation (shared by all)
    public function getTransactionLog(): array
    {
        return $this->transactionLog;
    }

    // Abstract methods (each implementation must define)
    abstract public function charge(float $amount, array $customer): array;
    abstract public function refund(string $transactionId, float $amount): array;
    abstract public function calculateFee(float $amount): float;
    abstract public function formatCurrency(float $amount): string;
}
Enter fullscreen mode Exit fullscreen mode
Step 3: Implement Concrete Classes
class StripePayment extends AbstractPayment
{
    private \Stripe\StripeClient $stripe;

    public function __construct(string $secretKey, string $currency = 'USD')
    {
        parent::__construct($currency);
        $this->stripe = new \Stripe\StripeClient($secretKey);
    }

    public function charge(float $amount, array $customer): array
    {
        try {
            $intent = $this->stripe->paymentIntents->create([
                'amount' => $this->formatCurrency($amount),
                'currency' => $this->currency,
                'customer' => $customer['stripe_id'],
                'payment_method' => $customer['payment_method_id'],
                'confirm' => true,
            ]);

            $this->logTransaction('charge', $intent->id, $amount);

            return [
                'success' => true,
                'transaction_id' => $intent->id,
                'status' => $intent->status,
                'fee' => $this->calculateFee($amount),
                'currency' => $this->currency,
            ];
        } catch (\Exception $e) {
            $this->logTransaction('charge_failed', null, $amount, $e->getMessage());
            throw new \Exception('Stripe charge failed: ' . $e->getMessage());
        }
    }

    public function refund(string $transactionId, float $amount): array
    {
        try {
            $refund = $this->stripe->refunds->create([
                'payment_intent' => $transactionId,
                'amount' => $this->formatCurrency($amount),
            ]);

            $this->logTransaction('refund', $refund->id, $amount);

            return [
                'success' => true,
                'refund_id' => $refund->id,
                'status' => $refund->status,
                'currency' => $this->currency,
            ];
        } catch (\Exception $e) {
            $this->logTransaction('refund_failed', $transactionId, $amount, $e->getMessage());
            throw new \Exception('Stripe refund failed: ' . $e->getMessage());
        }
    }

    public function calculateFee(float $amount): float
    {
        // Stripe fee: 2.9% + $0.30
        return round(($amount * 0.029) + 0.30, 2);
    }

    public function formatCurrency(float $amount): int
    {
        // Stripe uses cents
        return (int)($amount * 100);
    }
}

class PayPalPayment extends AbstractPayment
{
    private \PayPal\Rest\ApiContext $apiContext;

    public function __construct(string $clientId, string $clientSecret, string $currency = 'USD')
    {
        parent::__construct($currency);
        $this->apiContext = new \PayPal\Rest\ApiContext(
            new \PayPal\Auth\OAuthTokenCredential($clientId, $clientSecret)
        );
    }

    public function charge(float $amount, array $customer): array
    {
        try {
            $payment = new \PayPal\Api\Payment();
            $payment->setIntent('sale')
                ->setPayer(['payment_method' => 'paypal'])
                ->setTransactions([[
                    'amount' => [
                        'total' => $this->formatCurrency($amount),
                        'currency' => $this->currency,
                    ],
                    'description' => 'Payment',
                ]])
                ->setRedirectUrls([
                    'return_url' => $customer['return_url'],
                    'cancel_url' => $customer['cancel_url'],
                ]);

            $payment->create($this->apiContext);

            $this->logTransaction('charge', $payment->getId(), $amount);

            return [
                'success' => true,
                'transaction_id' => $payment->getId(),
                'approval_url' => $this->getApprovalUrl($payment),
                'fee' => $this->calculateFee($amount),
                'currency' => $this->currency,
            ];
        } catch (\Exception $e) {
            $this->logTransaction('charge_failed', null, $amount, $e->getMessage());
            throw new \Exception('PayPal charge failed: ' . $e->getMessage());
        }
    }

    public function refund(string $transactionId, float $amount): array
    {
        try {
            $sale = new \PayPal\Api\Sale();
            $sale->setId($transactionId);

            $refund = new \PayPal\Api\Refund();
            $refund->setAmount([
                'total' => $this->formatCurrency($amount),
                'currency' => $this->currency,
            ]);

            $refunded = $sale->refund($refund, $this->apiContext);

            $this->logTransaction('refund', $refunded->getId(), $amount);

            return [
                'success' => true,
                'refund_id' => $refunded->getId(),
                'status' => $refunded->getState(),
                'currency' => $this->currency,
            ];
        } catch (\Exception $e) {
            $this->logTransaction('refund_failed', $transactionId, $amount, $e->getMessage());
            throw new \Exception('PayPal refund failed: ' . $e->getMessage());
        }
    }

    public function calculateFee(float $amount): float
    {
        // PayPal fee: 3.4% + $0.30
        return round(($amount * 0.034) + 0.30, 2);
    }

    public function formatCurrency(float $amount): string
    {
        // PayPal expects string with 2 decimals
        return number_format($amount, 2, '.', '');
    }

    private function getApprovalUrl($payment): ?string
    {
        foreach ($payment->getLinks() as $link) {
            if ($link->getRel() === 'approval_url') {
                return $link->getHref();
            }
        }
        return null;
    }
}
Enter fullscreen mode Exit fullscreen mode
Step 4: Client Code (Using the Abstraction)
class PaymentController
{
    private PaymentInterface $payment;

    public function __construct(PaymentInterface $payment)
    {
        $this->payment = $payment;
    }

    public function charge(Request $request): JsonResponse
    {
        try {
            $result = $this->payment->charge(
                $request->input('amount'),
                $request->input('customer')
            );

            return response()->json($result);
        } catch (\Exception $e) {
            return response()->json([
                'success' => false,
                'error' => $e->getMessage(),
            ], 400);
        }
    }

    public function refund(Request $request): JsonResponse
    {
        try {
            $result = $this->payment->refund(
                $request->input('transaction_id'),
                $request->input('amount')
            );

            return response()->json($result);
        } catch (\Exception $e) {
            return response()->json([
                'success' => false,
                'error' => $e->getMessage(),
            ], 400);
        }
    }

    public function log(): JsonResponse
    {
        return response()->json($this->payment->getTransactionLog());
    }
}
Enter fullscreen mode Exit fullscreen mode
Step 5: Factory (or Dependency Injection)
class PaymentFactory
{
    public static function create(string $type): PaymentInterface
    {
        return match($type) {
            'stripe' => new StripePayment(
                config('services.stripe.secret'),
                config('services.stripe.currency', 'USD')
            ),
            'paypal' => new PayPalPayment(
                config('services.paypal.client_id'),
                config('services.paypal.secret'),
                config('services.paypal.currency', 'USD')
            ),
            default => throw new \Exception("Unknown payment type: {$type}"),
        };
    }
}

// Usage in controller (or via Laravel's container)
$payment = PaymentFactory::create($request->input('payment_method'));
$controller = new PaymentController($payment);
Enter fullscreen mode Exit fullscreen mode

What We Improved

  1. Clear Contract: Interface defines what all payments must do.
  2. Shared Logic: Abstract class provides logging and currency handling.
  3. Specific Logic: Each concrete class implements its own charge/refund logic.
  4. Flexibility: Swap payments by changing the factory or binding.
  5. Testability: Mock the interface for testing.
  6. SOLID Compliance: OCP (add new payments), DIP (depend on interface).

8. Laravel Internal Example

Laravel is built on layers of abstraction. Let's look at how it uses interfaces and abstract classes.

Eloquent Model (Abstract Class + Interfaces)

// Illuminate\Database\Eloquent\Model
abstract class Model implements Arrayable, ArrayAccess, Jsonable, JsonSerializable, QueueableEntity, UrlRoutable
{
    // Abstract: Provides base functionality
    protected $connection;
    protected $table;
    protected $primaryKey = 'id';

    // Concrete methods (shared by all models)
    public function save(array $options = [])
    {
        // 100+ lines of shared save logic
    }

    public function delete()
    {
        // Shared delete logic
    }

    // Abstract methods (must be implemented by child classes)
    abstract public function getConnectionName();
    abstract public function getTable();
}
Enter fullscreen mode Exit fullscreen mode

Why This Is Elegant:

  • Abstract Class: Provides all the shared Eloquent functionality.
  • Interfaces: Ensure the model can be used in different contexts (Arrayable, Jsonable, Queueable).
  • Your Model: Extends the abstract class and optionally implements additional interfaces.

Service Provider (Abstract Class)

// Illuminate\Support\ServiceProvider
abstract class ServiceProvider
{
    // Abstract: Provides the registration structure
    protected $app;
    protected $defer = false;

    // Concrete method (shared)
    public function register()
    {
        // Default implementation
    }

    // Abstract method (must be implemented)
    abstract public function register();

    // Optional boot method (concrete)
    public function boot()
    {
        // Can be overridden
    }
}
Enter fullscreen mode Exit fullscreen mode

Why This Is Elegant:

  • The abstract class provides the registration infrastructure.
  • Your provider only needs to implement register().
  • boot() is optional, allowing providers to be simple or complex.

Queueable Interface (Pure Interface)

// Illuminate\Contracts\Queue\QueueableEntity
interface QueueableEntity
{
    public function getQueueableId();
    public function getQueueableConnection();
    public function getQueueableRelations();
}

// Implemented by Model
class Model implements QueueableEntity
{
    public function getQueueableId()
    {
        return $this->getKey();
    }
    // ...
}
Enter fullscreen mode Exit fullscreen mode

Why This Is Elegant:

  • The interface defines what's needed to queue an entity.
  • Model implements it, but other classes could too.
  • The queue system only depends on the interface.

Authentication (Multiple Interfaces)

// Illuminate\Contracts\Auth\Guard
interface Guard
{
    public function check(): bool;
    public function user(): ?Authenticatable;
    public function validate(array $credentials = []): bool;
}

// Illuminate\Contracts\Auth\Authenticatable
interface Authenticatable
{
    public function getAuthIdentifierName();
    public function getAuthIdentifier();
    public function getAuthPassword();
    public function getRememberToken();
    public function setRememberToken($value);
    public function getRememberTokenName();
}

// Abstract base guard
abstract class Guard implements Guard
{
    // Shared logic for all guards
    protected $name;
    protected $user;
    protected $lastAttempted;

    // Abstract methods for specific guards
    abstract public function attempt(array $credentials = [], $remember = false);
    abstract public function once(array $credentials = []);
}
Enter fullscreen mode Exit fullscreen mode

Why This Is Elegant:

  • Interfaces define clear contracts.
  • Abstract class provides shared logic.
  • Concrete guards (SessionGuard, TokenGuard) handle specifics.

Filesystem (Interface + Abstract Class)

// Illuminate\Contracts\Filesystem\Filesystem
interface Filesystem
{
    public function exists(string $path): bool;
    public function get(string $path): string;
    public function put(string $path, $contents, $options = []): bool;
    public function delete(string|array $paths): bool;
    // ... more methods
}

// Abstract FilesystemAdapter (Laravel-specific)
abstract class FilesystemAdapter implements Filesystem
{
    // Shared logic for all filesystems
    protected $driver;
    protected $config;

    public function get($path)
    {
        // Shared get logic with error handling
    }

    // Abstract methods
    abstract public function write($path, $contents, $config);
    abstract public function read($path);
}

// Concrete implementations
class LocalFilesystem extends FilesystemAdapter { /* ... */ }
class S3Filesystem extends FilesystemAdapter { /* ... */ }
Enter fullscreen mode Exit fullscreen mode

Why This Is Elegant:

  • The interface defines the contract.
  • The abstract class provides error handling and shared logic.
  • Each driver implements the specific operations.

9. Real Laravel Application Example

Let's build a Report Generation System using proper abstraction.

Scenario

Your application needs to generate reports in different formats (PDF, Excel, CSV, JSON). Each format has:

  • Different rendering logic.
  • Different file generation.
  • Different meta-requirements.

But they all share:

  • Data fetching.
  • Header/footer formatting.
  • Logging.
  • File storage.

Implementation

Step 1: Define the Interface (Contract)
// app/Contracts/ReportGeneratorInterface.php
namespace App\Contracts;

use App\Models\Report;

interface ReportGeneratorInterface
{
    public function generate(Report $report, array $options = []): ReportResult;
    public function getFormat(): string;
    public function getMimeType(): string;
    public function getFileExtension(): string;
    public function supports(Report $report): bool;
}
Enter fullscreen mode Exit fullscreen mode
Step 2: Create Result DTO
// app/DTO/ReportResult.php
namespace App\DTO;

class ReportResult
{
    public function __construct(
        public readonly string $content,
        public readonly string $filename,
        public readonly string $mimeType,
        public readonly int $size,
        public readonly ?string $tempPath = null,
        public readonly array $metadata = []
    ) {}
}
Enter fullscreen mode Exit fullscreen mode
Step 3: Abstract Base Class (Shared Logic)
// app/Services/Report/AbstractReportGenerator.php
namespace App\Services\Report;

use App\Contracts\ReportGeneratorInterface;
use App\DTO\ReportResult;
use App\Models\Report;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;

abstract class AbstractReportGenerator implements ReportGeneratorInterface
{
    protected array $headers = [];
    protected array $footers = [];
    protected array $metadata = [];

    public function __construct(
        protected readonly string $storageDisk = 'local',
        protected readonly bool $debug = false
    ) {}

    public function generate(Report $report, array $options = []): ReportResult
    {
        $startTime = microtime(true);

        try {
            // Step 1: Fetch data (shared)
            $data = $this->fetchData($report, $options);

            // Step 2: Prepare headers (shared)
            $this->prepareHeaders($report);

            // Step 3: Format the content (specific)
            $content = $this->formatContent($data, $report, $options);

            // Step 4: Apply template (shared)
            $finalContent = $this->applyTemplate($content, $report);

            // Step 5: Generate file (specific)
            $filePath = $this->generateFile($finalContent, $report, $options);

            // Step 6: Log (shared)
            $this->logGeneration($report, $startTime);

            return new ReportResult(
                content: file_get_contents($filePath) ?: '',
                filename: $this->generateFilename($report) . '.' . $this->getFileExtension(),
                mimeType: $this->getMimeType(),
                size: filesize($filePath),
                tempPath: $filePath,
                metadata: array_merge($this->metadata, [
                    'format' => $this->getFormat(),
                    'generated_at' => now()->toIso8601String(),
                    'generation_time' => round(microtime(true) - $startTime, 2),
                ])
            );
        } catch (\Exception $e) {
            Log::error('Report generation failed', [
                'report_id' => $report->id,
                'format' => $this->getFormat(),
                'error' => $e->getMessage(),
            ]);
            throw $e;
        }
    }

    // Shared implementation
    protected function fetchData(Report $report, array $options): array
    {
        // Cache the data
        $cacheKey = "report_data_{$report->id}";

        if ($options['use_cache'] ?? true) {
            return cache()->remember($cacheKey, 3600, function () use ($report) {
                return $this->getReportData($report);
            });
        }

        return $this->getReportData($report);
    }

    protected function prepareHeaders(Report $report): void
    {
        $this->headers = [
            'title' => $report->title,
            'generated_at' => now()->format('Y-m-d H:i:s'),
            'company_name' => config('app.name'),
            'report_type' => $report->type,
        ];
    }

    protected function applyTemplate(string $content, Report $report): string
    {
        // Shared template wrapping
        $template = view('reports.template', [
            'content' => $content,
            'headers' => $this->headers,
            'footers' => $this->footers,
            'report' => $report,
        ]);

        return $template->render();
    }

    protected function logGeneration(Report $report, float $startTime): void
    {
        Log::info('Report generated', [
            'report_id' => $report->id,
            'format' => $this->getFormat(),
            'time' => round(microtime(true) - $startTime, 2),
        ]);
    }

    protected function generateFilename(Report $report): string
    {
        $safeTitle = strtolower(str_replace(' ', '_', $report->title));
        return "{$safeTitle}_{$report->id}_" . date('Ymd_His');
    }

    // Abstract methods (must be implemented by concrete classes)
    abstract protected function getReportData(Report $report): array;
    abstract protected function formatContent(array $data, Report $report, array $options): string;
    abstract protected function generateFile(string $content, Report $report, array $options): string;
    abstract public function getFormat(): string;
    abstract public function getMimeType(): string;
    abstract public function getFileExtension(): string;
    abstract public function supports(Report $report): bool;
}
Enter fullscreen mode Exit fullscreen mode
Step 4: Concrete Implementations
// app/Services/Report/PdfReportGenerator.php
namespace App\Services\Report;

use App\Models\Report;
use Dompdf\Dompdf;
use Dompdf\Options;

class PdfReportGenerator extends AbstractReportGenerator
{
    public function __construct(
        protected readonly Dompdf $dompdf,
        protected readonly array $pdfOptions = [],
        string $storageDisk = 'local',
        bool $debug = false
    ) {
        parent::__construct($storageDisk, $debug);

        $options = new Options();
        $options->set('isHtml5ParserEnabled', true);
        $options->set('isRemoteEnabled', true);
        $this->dompdf->setOptions($options);
    }

    protected function getReportData(Report $report): array
    {
        // Fetch specific data for PDF
        return [
            'data' => $report->getData(),
            'totals' => $report->calculateTotals(),
            'charts' => $report->getChartData(),
        ];
    }

    protected function formatContent(array $data, Report $report, array $options): string
    {
        // Render the HTML content
        $view = view($report->getPdfView(), [
            'data' => $data,
            'report' => $report,
            'options' => $options,
        ]);

        return $view->render();
    }

    protected function generateFile(string $content, Report $report, array $options): string
    {
        $this->dompdf->loadHtml($content);
        $this->dompdf->setPaper('A4', 'portrait');
        $this->dompdf->render();

        $output = $this->dompdf->output();

        $tempPath = tempnam(sys_get_temp_dir(), 'report_pdf_');
        file_put_contents($tempPath, $output);

        return $tempPath;
    }

    public function getFormat(): string
    {
        return 'pdf';
    }

    public function getMimeType(): string
    {
        return 'application/pdf';
    }

    public function getFileExtension(): string
    {
        return 'pdf';
    }

    public function supports(Report $report): bool
    {
        return in_array('pdf', $report->supported_formats ?? ['pdf']);
    }
}

// app/Services/Report/ExcelReportGenerator.php
namespace App\Services\Report;

use App\Models\Report;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;

class ExcelReportGenerator extends AbstractReportGenerator
{
    protected function getReportData(Report $report): array
    {
        // Fetch Excel-specific data
        return $report->getData();
    }

    protected function formatContent(array $data, Report $report, array $options): string
    {
        $spreadsheet = new Spreadsheet();
        $sheet = $spreadsheet->getActiveSheet();

        // Headers
        $headers = $this->getHeaders($report);
        $col = 'A';
        foreach ($headers as $header) {
            $sheet->setCellValue($col . '1', $header);
            $col++;
        }

        // Data
        $row = 2;
        foreach ($data as $item) {
            $col = 'A';
            foreach ($headers as $key => $header) {
                $value = $item[$key] ?? '';
                $sheet->setCellValue($col . $row, $value);
                $col++;
            }
            $row++;
        }

        // Auto-size columns
        foreach (range('A', $col) as $col) {
            $sheet->getColumnDimension($col)->setAutoSize(true);
        }

        $tempPath = tempnam(sys_get_temp_dir(), 'report_excel_');
        $writer = new Xlsx($spreadsheet);
        $writer->save($tempPath);

        return file_get_contents($tempPath);
    }

    protected function generateFile(string $content, Report $report, array $options): string
    {
        $tempPath = tempnam(sys_get_temp_dir(), 'report_excel_');
        file_put_contents($tempPath, $content);
        return $tempPath;
    }

    protected function getHeaders(Report $report): array
    {
        return $report->getHeaders() ?? ['Column 1', 'Column 2', 'Column 3'];
    }

    public function getFormat(): string
    {
        return 'excel';
    }

    public function getMimeType(): string
    {
        return 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
    }

    public function getFileExtension(): string
    {
        return 'xlsx';
    }

    public function supports(Report $report): bool
    {
        return in_array('excel', $report->supported_formats ?? ['pdf', 'excel']);
    }
}
Enter fullscreen mode Exit fullscreen mode
Step 5: Service Provider
// app/Providers/ReportServiceProvider.php
namespace App\Providers;

use App\Contracts\ReportGeneratorInterface;
use App\Services\Report\PdfReportGenerator;
use App\Services\Report\ExcelReportGenerator;
use App\Services\Report\ReportResolver;
use App\Services\ReportService;
use Illuminate\Support\ServiceProvider;

class ReportServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        // Register each generator
        $this->app->tag([
            PdfReportGenerator::class,
            ExcelReportGenerator::class,
        ], 'report_generators');

        $this->app->singleton(ReportResolver::class, function ($app) {
            $resolver = new ReportResolver();

            foreach ($app->tagged('report_generators') as $generator) {
                $resolver->register($generator);
            }

            return $resolver;
        });

        $this->app->singleton(ReportService::class);
    }
}
Enter fullscreen mode Exit fullscreen mode
Step 6: Controller
// app/Http/Controllers/ReportController.php
namespace App\Http\Controllers;

use App\Models\Report;
use App\Services\ReportService;
use Illuminate\Http\Request;

class ReportController extends Controller
{
    public function __construct(
        private readonly ReportService $reportService
    ) {}

    public function generate(Request $request, Report $report)
    {
        $format = $request->input('format', 'pdf');

        if (!$this->reportService->isSupported($format)) {
            return response()->json([
                'error' => "Format '{$format}' is not supported",
            ], 400);
        }

        try {
            $result = $this->reportService->generate($report, $format, [
                'use_cache' => $request->boolean('use_cache', true),
                'include_charts' => $request->boolean('include_charts', true),
            ]);

            return response($result->content)
                ->header('Content-Type', $result->mimeType)
                ->header('Content-Disposition', 'attachment; filename="' . $result->filename . '"')
                ->header('Content-Length', $result->size);

        } catch (\Exception $e) {
            return response()->json(['error' => $e->getMessage()], 500);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Why This Abstraction Is Powerful

  1. Interface: Defines the contract for all report generators.
  2. Abstract Class: Provides shared logic (data fetching, logging, caching, templating).
  3. Concrete Classes: Each handles a specific format.
  4. Flexibility: Add a new format by creating a new class.
  5. Maintainability: Shared logic is in one place.
  6. Testability: Test each generator and the abstract class separately.

10. SOLID Principles Mapping

D - Dependency Inversion Principle (DIP)

This is the most direct mapping. The ReportController depends on ReportGeneratorInterface (abstraction), not concrete implementations.

class ReportController
{
    public function __construct(
        private readonly ReportGeneratorInterface $generator
    ) {}
}
Enter fullscreen mode Exit fullscreen mode

O - Open/Closed Principle (OCP)

Adding a new format requires creating a new class, not modifying existing code.

// Adding a new JSON format
class JsonReportGenerator extends AbstractReportGenerator { /* ... */ }

// No changes to ReportService or ReportController needed!
Enter fullscreen mode Exit fullscreen mode

L - Liskov Substitution Principle (LSP)

All generators are substitutable for ReportGeneratorInterface. The ReportService works correctly with any of them.

function generate(ReportGeneratorInterface $generator, Report $report)
{
    // Works with any generator
    return $generator->generate($report);
}
Enter fullscreen mode Exit fullscreen mode

I - Interface Segregation Principle (ISP)

The interface is focused on report generation only.

interface ReportGeneratorInterface
{
    public function generate(Report $report, array $options = []): ReportResult;
    // Only generation methods. No extra methods.
}
Enter fullscreen mode Exit fullscreen mode

S - Single Responsibility Principle (SRP)

Each class has a single responsibility:

  • AbstractReportGenerator: Shared logic.
  • PdfReportGenerator: PDF-specific logic.
  • ExcelReportGenerator: Excel-specific logic.

11. Trade-offs

Benefits

  1. Flexibility: Swap implementations without changing client code.
  2. Testability: Mock the interface for unit testing.
  3. Reusability: Shared logic in abstract classes.
  4. Maintainability: Changes are isolated.
  5. Extensibility: Add new implementations without modifying existing ones.
  6. Documentation: Interfaces document the contract.

Costs

  1. Complexity: More files and abstractions.
  2. Indirection: Harder to trace code flow.
  3. Learning Curve: Developers must understand abstraction hierarchies.
  4. Over-Engineering: Unnecessary abstractions can complicate simple code.

When Is Complexity Justified?

Use abstraction when:

  • You have multiple implementations of the same concept.
  • The number of implementations is likely to grow.
  • You need to test implementations in isolation.
  • You want to adhere to SOLID principles.
  • You're building a framework or library.

Avoid abstraction when:

  • You only have one implementation (YAGNI).
  • The logic is trivial and stable.
  • The overhead of abstraction doesn't justify the benefit.

12. When NOT To Use It

3 Green Flags (USE ABSTRACTION)

  1. Multiple Implementations: You have 2+ implementations of the same concept.

  2. Likely to Grow: The number of implementations will increase over time.

  3. Need for Testing: You need to mock the dependency for testing.

3 Red Flags (AVOID ABSTRACTION)

  1. Single Implementation: You only have one implementation and it's unlikely to change.
// BAD: Unnecessary abstraction
interface CalculatorInterface { public function add(int $a, int $b): int; }
class Calculator implements CalculatorInterface { public function add(int $a, int $b): int { return $a + $b; } }
Enter fullscreen mode Exit fullscreen mode
  1. Stable Logic: The logic is simple and will never change.

  2. Tightly Coupled Implementations: All implementations share so much logic that the abstraction adds no value.


13. Common Mistakes

1. Creating an Interface for Every Class

// BAD: Interface for everything
interface UserRepositoryInterface { ... }
class UserRepository implements UserRepositoryInterface { ... }

interface UserServiceInterface { ... }
class UserService implements UserServiceInterface { ... }
Enter fullscreen mode Exit fullscreen mode

Problem: You have many interfaces with only one implementation. This is over-engineering.

Fix: Wait until you have a second implementation before creating the interface.

2. Using Abstract Class When Interface Would Suffice

// BAD: Abstract class with no shared logic
abstract class PaymentProcessor
{
    abstract public function process(Order $order): array;
}

// GOOD: Interface (no need for abstract)
interface PaymentProcessor
{
    public function process(Order $order): array;
}
Enter fullscreen mode Exit fullscreen mode

Problem: The abstract class provides no value. It just adds complexity.

Fix: Use an interface when there's no shared logic.

3. Using Interface When Abstract Class Is Needed

// BAD: Interface with no way to share code
interface PaymentInterface
{
    public function charge(): array;
    public function logTransaction(): void; // Every implementation duplicates this!
}

// GOOD: Abstract class with shared logic
abstract class AbstractPayment implements PaymentInterface
{
    public function logTransaction(): void
    {
        // Shared logic once
    }
}
Enter fullscreen mode Exit fullscreen mode

Problem: Every implementation duplicates the logTransaction logic.

Fix: Use an abstract class for shared implementation.

4. Making Abstract Class Too Specific

// BAD: Abstract class with hardcoded assumptions
abstract class AbstractPayment
{
    public function charge(float $amount, array $customer): array
    {
        // Assumes stripe-like behavior
        $intent = $this->createPaymentIntent($amount, $customer);
        return $this->processIntent($intent);
    }

    abstract protected function createPaymentIntent(float $amount, array $customer);
    abstract protected function processIntent($intent);
}
Enter fullscreen mode Exit fullscreen mode

Problem: The abstract class assumes a Stripe-like model. PayPal doesn't work this way.

Fix: Keep abstract classes generic. Let concrete classes handle their own flow.

5. Changing the Interface

// BAD: Changing the interface
interface PaymentInterface
{
    public function charge(): array;  // Removed parameters!
}

// Everything breaks
Enter fullscreen mode Exit fullscreen mode

Problem: Changing the interface breaks all implementations.

Fix: Design interfaces carefully. If you need to change them, consider a new interface or use the Adapter pattern.


14. Frequently Asked Interview Questions

Beginner/Intermediate

  1. Q: What is the difference between an interface and an abstract class in PHP?
    A: An interface defines a contract with no implementation. An abstract class can provide partial implementation and shared logic.

  2. Q: Can a class implement multiple interfaces? Can it extend multiple abstract classes?
    A: Yes, a class can implement multiple interfaces. No, PHP doesn't support multiple inheritance (extending multiple abstract classes).

  3. Q: When would you use an interface instead of an abstract class?
    A: When you only need to define a contract, and there's no shared logic to provide.

  4. Q: When would you use an abstract class instead of an interface?
    A: When you have shared logic that multiple implementations can reuse, and you want to enforce certain methods.

  5. Q: Does Laravel use interfaces or abstract classes more?
    A: Laravel uses both extensively. It uses interfaces for contracts (e.g., Queueable, Authenticatable) and abstract classes for base implementations (e.g., Model, ServiceProvider).

Senior/Architect

  1. Q: Explain the design decision: when to use an interface, an abstract class, or a concrete class.
    A: Use an interface when you need a contract that multiple unrelated classes can implement. Use an abstract class when you have a common base with shared logic. Use concrete classes when the logic is specific and not shared.

  2. Q: How do you handle evolving interfaces without breaking existing code?
    A: Use default methods (PHP 8+), create a new interface that extends the old one, or use the Adapter pattern to bridge old and new interfaces.

  3. Q: What's the role of abstractions in the Dependency Inversion Principle?
    A: DIP states that high-level modules shouldn't depend on low-level modules; both should depend on abstractions. Interfaces are the primary tool for creating these abstractions.

  4. Q: How does Laravel's Service Container interact with interfaces and abstract classes?
    A: The Service Container can resolve interfaces to concrete classes using bindings. It can also instantiate abstract classes if they have no abstract methods (or use factories).

  5. Q: What's the difference between an interface and a trait? When would you use each?
    A: An interface defines a contract. A trait provides code reuse. Use interfaces for polymorphism (different implementations). Use traits for shared code across unrelated classes (horizontal reuse).


15. Interactive Practice Challenge

The Requirement

You're building a Document Export System for a CMS. The current code is a mess of concrete classes with no abstraction.

The Code (POOR DESIGN)

// No abstraction - everything is concrete
class PdfExporter
{
    private array $config;
    private array $log = [];

    public function __construct(array $config = [])
    {
        $this->config = array_merge([
            'paper_size' => 'A4',
            'orientation' => 'portrait',
            'margin_top' => 10,
            'margin_bottom' => 10,
            'margin_left' => 10,
            'margin_right' => 10,
        ], $config);
    }

    public function export(Document $document): string
    {
        $this->log['start'] = microtime(true);
        $content = $this->generateContent($document);
        $html = $this->wrapWithTemplate($content, $document);
        $pdf = $this->renderPdf($html);
        $this->log['end'] = microtime(true);
        $this->log['size'] = strlen($pdf);

        return $pdf;
    }

    private function generateContent(Document $document): string
    {
        // Generate content specific to PDF
        return view('exports.pdf_content', ['document' => $document])->render();
    }

    private function wrapWithTemplate(string $content, Document $document): string
    {
        // Add headers, footers, styles
        return view('exports.pdf_template', [
            'content' => $content,
            'document' => $document,
            'config' => $this->config,
        ])->render();
    }

    private function renderPdf(string $html): string
    {
        $dompdf = new Dompdf();
        $dompdf->loadHtml($html);
        $dompdf->setPaper($this->config['paper_size'], $this->config['orientation']);
        $dompdf->render();
        return $dompdf->output();
    }

    public function getLog(): array
    {
        return $this->log;
    }
}

class ExcelExporter
{
    private array $config;
    private array $log = [];

    public function __construct(array $config = [])
    {
        $this->config = array_merge([
            'include_headers' => true,
            'auto_size_columns' => true,
            'date_format' => 'Y-m-d',
        ], $config);
    }

    public function export(Document $document): string
    {
        $this->log['start'] = microtime(true);
        $data = $document->getData();
        $spreadsheet = $this->createSpreadsheet($data, $document);
        $file = $this->saveSpreadsheet($spreadsheet);
        $this->log['end'] = microtime(true);
        $this->log['size'] = filesize($file);

        $content = file_get_contents($file);
        unlink($file);

        return $content;
    }

    private function createSpreadsheet(array $data, Document $document): Spreadsheet
    {
        $spreadsheet = new Spreadsheet();
        $sheet = $spreadsheet->getActiveSheet();

        if ($this->config['include_headers']) {
            $headers = $document->getHeaders();
            $col = 'A';
            foreach ($headers as $header) {
                $sheet->setCellValue($col . '1', $header);
                $col++;
            }
        }

        $row = $this->config['include_headers'] ? 2 : 1;
        foreach ($data as $item) {
            $col = 'A';
            foreach ($item as $value) {
                $sheet->setCellValue($col . $row, $value);
                $col++;
            }
            $row++;
        }

        if ($this->config['auto_size_columns']) {
            // Auto-size columns
        }

        return $spreadsheet;
    }

    private function saveSpreadsheet(Spreadsheet $spreadsheet): string
    {
        $tempPath = tempnam(sys_get_temp_dir(), 'export_');
        $writer = new Xlsx($spreadsheet);
        $writer->save($tempPath);
        return $tempPath;
    }

    public function getLog(): array
    {
        return $this->log;
    }
}

class CsvExporter
{
    private array $config;
    private array $log = [];

    public function __construct(array $config = [])
    {
        $this->config = array_merge([
            'delimiter' => ',',
            'enclosure' => '"',
            'escape_char' => '\\',
            'include_headers' => true,
        ], $config);
    }

    public function export(Document $document): string
    {
        $this->log['start'] = microtime(true);
        $data = $document->getData();
        $headers = $document->getHeaders();

        $output = fopen('php://temp', 'r+');

        if ($this->config['include_headers']) {
            fputcsv($output, $headers, $this->config['delimiter'], $this->config['enclosure']);
        }

        foreach ($data as $row) {
            fputcsv($output, $row, $this->config['delimiter'], $this->config['enclosure']);
        }

        rewind($output);
        $content = stream_get_contents($output);
        fclose($output);

        $this->log['end'] = microtime(true);
        $this->log['size'] = strlen($content);

        return $content;
    }

    public function getLog(): array
    {
        return $this->log;
    }
}

// Client code
class DocumentController
{
    public function export(Request $request, Document $document)
    {
        $format = $request->input('format', 'pdf');

        if ($format === 'pdf') {
            $exporter = new PdfExporter($request->input('config', []));
            $content = $exporter->export($document);
            $log = $exporter->getLog();
        } elseif ($format === 'excel') {
            $exporter = new ExcelExporter($request->input('config', []));
            $content = $exporter->export($document);
            $log = $exporter->getLog();
        } elseif ($format === 'csv') {
            $exporter = new CsvExporter($request->input('config', []));
            $content = $exporter->export($document);
            $log = $exporter->getLog();
        } else {
            throw new \Exception("Unsupported format: {$format}");
        }

        // Log the export
        Log::info('Export completed', $log);

        return response($content)
            ->header('Content-Type', $this->getMimeType($format))
            ->header('Content-Disposition', 'attachment; filename="export.' . $this->getExtension($format) . '"');
    }

    private function getMimeType(string $format): string
    {
        return match($format) {
            'pdf' => 'application/pdf',
            'excel' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
            'csv' => 'text/csv',
            default => 'application/octet-stream',
        };
    }

    private function getExtension(string $format): string
    {
        return match($format) {
            'pdf' => 'pdf',
            'excel' => 'xlsx',
            'csv' => 'csv',
            default => 'bin',
        };
    }
}
Enter fullscreen mode Exit fullscreen mode

The Challenges

New requirements are piling up:

  1. "We need to add JSON export for APIs."
  2. "We need to add XML export for legacy systems."
  3. "We need to add HTML export for previews."
  4. "We need to add Markdown export for documentation."
  5. "We need to compress large exports into ZIP files."
  6. "We need to track export metrics (time, size, user) across all formats."
  7. "We need to queue large exports for background processing."

The current code is unmaintainable. Adding a new format requires understanding and duplicating code.

Your Task

Refactor this system using proper abstraction. Specifically:

  1. Define an ExporterInterface with methods like export(), getFormat(), getMimeType(), getExtension(), getLog().

  2. Create an AbstractExporter that provides:

    • Shared logging logic.
    • Shared metrics tracking.
    • Shared configuration handling.
    • Shared file handling (temp files, cleanup).
  3. Implement concrete exporters for PDF, Excel, CSV.

  4. Add support for JSON, XML, HTML, and Markdown by implementing new exporters.

  5. Build a ExporterResolver that resolves the right exporter for a given format.

  6. Create a ExportService that:

    • Uses the resolver.
    • Handles queueing for large exports.
    • Tracks metrics.
  7. Update the controller to use the new abstraction.

Questions to Consider

  • What should the ExporterInterface look like?
  • What shared logic belongs in the abstract class?
  • How should you handle different configurations per format?
  • How do you track metrics without duplicating code?
  • How do you handle queuing without coupling to the export logic?
  • How do you test each exporter independently?

(We won't provide the solution—refactor this code and master abstraction!)


16. Final Mental Model

To keep it simple, memorize these three sentences:

  • One-sentence definition: Abstraction is the process of hiding implementation details and exposing only essential features through contracts (interfaces) and shared logic (abstract classes).
  • One-sentence intuition: Interfaces say what an object can do. Abstract classes help with how it does it. One is a contract, the other is a starter kit.
  • One-sentence decision rule: Use an interface to define a contract for multiple implementations. Use an abstract class when you have shared logic that multiple implementations can reuse.

17. Related Concepts

SOLID Principles

  • Dependency Inversion: Abstractions (interfaces) enable DIP.
  • Open/Closed: Abstractions enable extension without modification.
  • Liskov Substitution: Interfaces and abstract classes enforce substitutability.

Design Patterns

  • Strategy Pattern: Uses interfaces for interchangeable algorithms.
  • Template Method Pattern: Uses abstract classes for shared flow.
  • Factory Pattern: Creates abstracted objects.
  • Adapter Pattern: Bridges different interfaces.
  • Bridge Pattern: Separates abstraction from implementation.

Laravel Internals

  • Service Container: Resolves abstractions to concretions.
  • Service Providers: Register bindings between interfaces and concrete classes.
  • Contracts: Laravel's interfaces for framework services.
  • Facades: Static access to abstracted services.
  • Eloquent: Abstract class (Model) with interfaces.

Enterprise Patterns

  • Repository Pattern: Abstractions for data access.
  • Service Layer Pattern: Abstractions for business logic.
  • Data Mapper Pattern: Abstracts database interactions.
  • DAO Pattern: Abstracts data access.

Final Thoughts

Interfaces and abstract classes are the foundation of abstraction in object-oriented programming. They serve different but complementary purposes:

  • Interfaces define contracts. They ensure consistency across implementations.
  • Abstract classes provide shared logic. They reduce duplication and enforce structure.

Understanding when to use each is a hallmark of senior-level design. Use interfaces when you need a contract. Use abstract classes when you have shared logic. Use both when you need both.

Remember: Abstraction isn't about complexity. It's about clarity. It's about making your code easier to understand, extend, and maintain.


Github: Abstraction

Top comments (0)