DEV Community

Cover image for High Cohesion, Low Coupling: The Foundation of Good Object Design
Anas Hussain
Anas Hussain

Posted on • Edited on

High Cohesion, Low Coupling: The Foundation of Good Object Design

1. Hook & Problem Statement

You've inherited a Laravel project. It's a mess. Making a change to one part of the codebase breaks three other parts. Adding a new feature feels like performing surgery on a living patient. You're terrified to touch anything.

You open a class. It's 2,000 lines long. It does everything: validates data, sends emails, processes payments, generates reports, and connects to the database. It has 45 dependencies. Changing one method requires understanding the entire class.

This isn't just bad code. This is a design problem. And the two metrics that reveal the problem are cohesion and coupling.

Every developer has worked on a codebase like this. We call it "spaghetti code" or a "big ball of mud." But the technical terms are much more precise:

  • Low Cohesion: A class does too many unrelated things.
  • High Coupling: A class knows too much about other classes.

When cohesion is low and coupling is high, your codebase is rigid, fragile, and hard to maintain. It's a system designed to fail.

Cohesion and coupling are the vital signs of your codebase. They tell you if your software is healthy or dying.


2. Why This Pattern/Concept Exists

The Software Engineering Problem It Solves

Cohesion and coupling are metrics that help you evaluate the quality of your object design. They answer two fundamental questions:

  1. Cohesion: How well do the elements inside a module (class, method, package) belong together?
  2. Coupling: How much does a module depend on other modules?

Large applications eventually fail if cohesion is low and coupling is high because:

  • Changes are expensive: Changing one class requires changing many others.
  • Testing is difficult: You can't test a class without its dependencies.
  • Understanding is hard: You need to understand the entire system to understand one part.
  • Reuse is impossible: You can't reuse a class without bringing its baggage.

The Pain That Existed Before

Before developers understood cohesion and coupling (and in codebases that ignore them):

  1. God Classes: Single classes that do everything (low cohesion).
  2. Ripple Effects: Changing one class caused a cascade of changes (high coupling).
  3. Fragile Codebases: Tests broke for seemingly unrelated reasons.
  4. Fear of Change: Developers were afraid to touch code.
  5. Abandoned Projects: Codebases were rewritten because they were "too hard to maintain."

Why Large Applications Need It

As applications scale, the cost of poor design compounds. A 100-line class with low cohesion might be manageable. A 10,000-line class with low cohesion is impossible.

The core insight: Modular design isn't just about separating code. It's about managing dependencies. High cohesion and low coupling are the characteristics of well-structured software. They're the difference between a system that's easy to evolve and one that collapses under its own weight.


3. Real World Analogy

The Restaurant Kitchen

Imagine a restaurant kitchen.

Low Cohesion:
One chef does everything: takes orders, cooks, washes dishes, manages inventory, and balances the books. This chef is a "God Object." They're overwhelmed. If they're sick, the entire restaurant stops.

High Coupling:
The kitchen is tightly connected. The oven's temperature is controlled by the same system as the dishwasher. The sous chef can't prep vegetables without knowing what the line cook is doing. Everything is interdependent. Changing one thing breaks everything else.

High Cohesion:
The kitchen is organized into stations:

  • Prep chef: Prepares ingredients.
  • Line cook: Cooks the food.
  • Saucier: Makes sauces.
  • Pastry chef: Makes desserts.
  • Dishwasher: Washes dishes.
  • Manager: Handles inventory and ordering.

Each station has a clear, focused responsibility. They don't overlap.

Low Coupling:
The stations are loosely connected through defined interfaces:

  • The prep chef preps ingredients and puts them in a labeled container (interface).
  • The line cook takes ingredients from the container (interface).
  • They don't need to know how the prep chef works. They just need the prepared ingredients.

When the restaurant is busy, they can swap stations without chaos. When one station is slow, it doesn't block the others.

Analogy Mapping:

  • Kitchen Station: A class (focus on one responsibility).
  • Chef's Specific Role: Methods that implement that responsibility.
  • Labeled Container: Interface (standardized communication).
  • Order System: The application flow.
  • Inventory Manager: Configuration or service provider.

4. The Pain (Bad Design)

Let's look at a typical "God Class" in a Laravel application.

namespace App\Services;

use App\Models\Order;
use App\Models\User;
use App\Models\Product;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Queue;
use Stripe\StripeClient;
use Twilio\Rest\Client;

class OrderService
{
    private StripeClient $stripe;
    private Client $twilio;
    private array $config;

    public function __construct()
    {
        $this->stripe = new StripeClient(config('services.stripe.secret'));
        $this->twilio = new Client(
            config('services.twilio.sid'),
            config('services.twilio.token')
        );
        $this->config = config('order');
    }

    // 1. Handles order creation
    public function createOrder(array $data): Order
    {
        // Validate data
        if (!isset($data['user_id']) || !isset($data['items'])) {
            throw new \Exception('Invalid order data');
        }

        // Check product availability
        $total = 0;
        foreach ($data['items'] as $item) {
            $product = Product::find($item['product_id']);
            if (!$product || $product->stock < $item['quantity']) {
                throw new \Exception("Product unavailable: {$item['product_id']}");
            }

            $total += $product->price * $item['quantity'];

            // Update stock
            $product->stock -= $item['quantity'];
            $product->save();
        }

        // Create order
        $order = new Order();
        $order->user_id = $data['user_id'];
        $order->items = json_encode($data['items']);
        $order->total = $total;
        $order->status = 'pending';
        $order->save();

        // Send confirmation email
        $user = User::find($data['user_id']);
        Mail::to($user->email)->send(new OrderConfirmation($order));

        // Send SMS
        if ($user->phone) {
            $this->twilio->messages->create(
                $user->phone,
                [
                    'from' => config('services.twilio.from'),
                    'body' => "Order #{$order->id} created!"
                ]
            );
        }

        // Log
        Log::info('Order created', ['order_id' => $order->id]);

        return $order;
    }

    // 2. Handles payment processing
    public function processPayment(Order $order): array
    {
        if ($order->status !== 'pending') {
            throw new \Exception('Order cannot be paid');
        }

        // Use Stripe
        $intent = $this->stripe->paymentIntents->create([
            'amount' => $order->total * 100,
            'currency' => 'usd',
            'payment_method_types' => ['card'],
        ]);

        $order->payment_intent_id = $intent->id;
        $order->save();

        Log::info('Payment initiated', ['order' => $order->id]);

        return ['client_secret' => $intent->client_secret];
    }

    // 3. Handles order fulfillment
    public function fulfillOrder(Order $order): void
    {
        if ($order->status !== 'paid') {
            throw new \Exception('Order must be paid to fulfill');
        }

        $items = json_decode($order->items, true);

        foreach ($items as $item) {
            // Prepare shipping labels
            $this->generateShippingLabel($item, $order);
        }

        $order->status = 'shipped';
        $order->save();

        // Update inventory in cache
        Cache::put("order_{$order->id}_shipped", true, 3600);

        // Queue follow-up email
        Queue::push(new SendFollowUpEmail($order));

        Log::info('Order fulfilled', ['order' => $order->id]);
    }

    // 4. Handles returns
    public function processReturn(Order $order, array $items): void
    {
        if ($order->status !== 'shipped') {
            throw new \Exception('Order must be shipped to return');
        }

        $returnedItems = [];
        $refundAmount = 0;

        $orderItems = json_decode($order->items, true);

        foreach ($items as $returnItem) {
            foreach ($orderItems as &$orderItem) {
                if ($orderItem['product_id'] === $returnItem['product_id']) {
                    $orderItem['returned'] = true;
                    $product = Product::find($returnItem['product_id']);
                    $refundAmount += $product->price * $returnItem['quantity'];
                    $returnedItems[] = $returnItem;
                }
            }
        }

        // Process refund
        $this->stripe->refunds->create([
            'payment_intent' => $order->payment_intent_id,
            'amount' => $refundAmount * 100,
        ]);

        // Update order
        $order->items = json_encode($orderItems);
        $order->status = 'returned';
        $order->save();

        // Send return confirmation
        Mail::to($order->user->email)->send(new ReturnConfirmation($order, $returnedItems));

        Log::info('Return processed', ['order' => $order->id]);
    }

    // 5. Generates shipping labels
    private function generateShippingLabel(array $item, Order $order): void
    {
        // 30 lines of shipping label logic
        // Connects to shipping API
    }

    // 6. Handles notifications
    public function sendStatusUpdate(Order $order): void
    {
        // Send email and SMS based on status
        $user = $order->user;

        Mail::to($user->email)->send(new OrderStatusUpdate($order));

        if ($user->phone) {
            $this->twilio->messages->create(
                $user->phone,
                [
                    'from' => config('services.twilio.from'),
                    'body' => "Order #{$order->id} status: {$order->status}",
                ]
            );
        }
    }

    // 7. Generates reports
    public function generateReport(string $type, array $filters): array
    {
        // 50 lines of report generation
        // Different logic for each report type
        return [];
    }

    // 8. Handles inventory management
    public function getInventory(): array
    {
        // 30 lines of inventory logic
        return [];
    }

    public function updateInventory(int $productId, int $quantity): void
    {
        // 20 lines of inventory update
    }

    // 9. Handles customer management
    public function getCustomerOrders(int $userId): array
    {
        // 10 lines
        return [];
    }

    public function updateCustomerProfile(int $userId, array $data): void
    {
        // 20 lines
    }
}
Enter fullscreen mode Exit fullscreen mode

Why This Is Terrible

  1. Low Cohesion:

    • The class handles order creation, payments, fulfillment, returns, shipping labels, notifications, reports, inventory, and customers.
    • 7+ unrelated responsibilities in one class.
  2. High Coupling:

    • The class knows about Stripe, Twilio, Mail, Log, Cache, Queue, DB.
    • It's tightly coupled to concrete implementations.
    • It uses facades directly (hidden dependencies).
  3. Difficult Testing:

    • To test createOrder(), you need Stripe, Twilio, Mail, DB, and Product models.
    • You're testing a thousand lines of code for one method.
  4. Difficult Change:

    • Want to switch from Twilio to Vonage? Change everything.
    • Want to change the notification flow? Find all the places.
  5. Violates SOLID:

    • SRP: Violated massively (7+ responsibilities).
    • OCP: Adding a new feature requires modifying this giant class.
    • DIP: Depends on concretions, not abstractions.
    • ISP: Users of this class get all methods, even if they only need one.

Why Developers Write Code Like This

We write code like this because:

  • It's fast to prototype.
  • We think "it's just one class."
  • We don't recognize the warning signs.
  • We haven't internalized SRP.
  • The class "works" so we don't refactor.

5. Solution Overview

Cohesion measures how well the elements inside a module (class, method, package) belong together. High cohesion means all elements work together toward a single purpose. Low cohesion means elements are unrelated.

Coupling measures how much a module depends on other modules. Low coupling means modules are independent. High coupling means modules are dependent on each other.

Core Idea

  • High Cohesion: A class should do one thing and do it well. All methods should contribute to a single responsibility.
  • Low Coupling: A class should have minimal dependencies on other classes. Dependencies should be through interfaces.

Main Participants

  1. High Cohesion:

    • Each class has one clear responsibility.
    • Methods within the class all support that responsibility.
    • The class is focused and understandable.
  2. Low Coupling:

    • Classes depend on abstractions (interfaces), not concretions.
    • Changes to one class don't require changes to others.
    • Dependencies are injected, not created internally.

How Objects Collaborate

High Cohesion (Each class has one job):
┌─────────────┐    ┌─────────────┐    ┌─────────────┐
│OrderService │    │PaymentService│    │Notification │
│(Create/     │    │(Process     │    │Service      │
│ Fulfill     │───▶│ Payments,   │───▶│(Send Emails,│
│ Orders)     │    │ Refunds)    │    │ SMS, Slack) │
└─────────────┘    └─────────────┘    └─────────────┘
        │                  │                  │
        ▼                  ▼                  ▼
┌─────────────┐    ┌─────────────┐    ┌─────────────┐
│Inventory    │    │PaymentGateway│    │MessageQueue │
│Service      │    │Interface    │    │Interface    │
└─────────────┘    └─────────────┘    └─────────────┘
Enter fullscreen mode Exit fullscreen mode

Low Coupling (Each class depends on interfaces):

OrderService
    ↓ (depends on)
PaymentGatewayInterface
    ↑ (implemented by)
StripeGateway  │  PayPalGateway  (swap implementations)
Enter fullscreen mode Exit fullscreen mode

Mental Model

Think of a well-organized toolbox.

  • High Cohesion: Each tool has one job. A hammer doesn't also function as a screwdriver. A wrench doesn't cut wood. Each tool is specialized.

  • Low Coupling: Tools are stored in separate slots. You can remove one tool without affecting the others. The hammer doesn't need to know about the wrench.

  • Low Cohesion + High Coupling: A multitool with everything built in. If you break the scissors, the whole tool is compromised. To add a new tool, you must redesign the entire multitool.

Benefits

  • Maintainability: Changes are localized to specific classes.
  • Testability: Test each class independently.
  • Readability: Classes are small and focused.
  • Reusability: Classes can be reused in different contexts.
  • Scalability: Teams can work on different classes simultaneously.

Trade-offs

  • More Classes: High cohesion means more classes.
  • More Files: Each class gets its own file.
  • More Dependencies: Classes need to be wired together.
  • Learning Curve: Developers need to understand the architecture.

6. UML Diagram

Laravel Cohesion and Coupling Mermaid Diagram

Laravel Cohesion and Coupling Mermaid Diagram

Diagram Explanation

  1. High Cohesion: Each service has a single responsibility.
  2. Low Coupling: OrderService depends on interfaces, not concretions.
  3. Replaceable Components: StripeGateway and PayPalGateway can be swapped.
  4. Clear Dependencies: Each class declares what it needs.

7. Vanilla PHP Example

Let's refactor the OrderService using high cohesion and low coupling.

Before Refactoring (Low Cohesion, High Coupling)

(The terrible code shown above)

After Refactoring (High Cohesion, Low Coupling)

Step 1: Define Interfaces (Low Coupling)
interface PaymentGatewayInterface
{
    public function charge(float $amount, string $currency, string $paymentMethodId): array;
    public function refund(string $transactionId, float $amount): array;
    public function createPaymentIntent(float $amount, string $currency): array;
}

interface NotificationInterface
{
    public function send(User $user, string $message, string $channel): void;
    public function getAvailableChannels(): array;
}

interface InventoryInterface
{
    public function checkAvailability(int $productId, int $quantity): bool;
    public function reserveStock(int $productId, int $quantity): void;
    public function releaseStock(int $productId, int $quantity): void;
    public function getStock(int $productId): int;
}

interface OrderRepositoryInterface
{
    public function save(Order $order): void;
    public function find(int $id): ?Order;
    public function findByUser(int $userId): array;
}
Enter fullscreen mode Exit fullscreen mode
Step 2: Implement Services (High Cohesion)
// 1. Payment Service
class StripeGateway implements PaymentGatewayInterface
{
    private StripeClient $stripe;

    public function __construct(string $secretKey)
    {
        $this->stripe = new StripeClient($secretKey);
    }

    public function charge(float $amount, string $currency, string $paymentMethodId): array
    {
        $intent = $this->stripe->paymentIntents->create([
            'amount' => (int)($amount * 100),
            'currency' => $currency,
            'payment_method' => $paymentMethodId,
            'confirm' => true,
        ]);

        return [
            'transaction_id' => $intent->id,
            'status' => $intent->status,
            'client_secret' => $intent->client_secret,
        ];
    }

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

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

    public function createPaymentIntent(float $amount, string $currency): array
    {
        $intent = $this->stripe->paymentIntents->create([
            'amount' => (int)($amount * 100),
            'currency' => $currency,
            'payment_method_types' => ['card'],
        ]);

        return [
            'intent_id' => $intent->id,
            'client_secret' => $intent->client_secret,
        ];
    }
}

// 2. Notification Service
class NotificationService implements NotificationInterface
{
    private array $channels = [];

    public function __construct(array $channels)
    {
        $this->channels = $channels;
    }

    public function send(User $user, string $message, string $channel): void
    {
        if (!isset($this->channels[$channel])) {
            throw new \Exception("Channel '{$channel}' not available");
        }

        $this->channels[$channel]->send($user, $message);
    }

    public function getAvailableChannels(): array
    {
        return array_keys($this->channels);
    }
}

class EmailChannel
{
    public function send(User $user, string $message): void
    {
        Mail::to($user->email)->send(new NotificationMessage($user, $message));
    }
}

class SmsChannel
{
    private Client $twilio;

    public function __construct(Client $twilio)
    {
        $this->twilio = $twilio;
    }

    public function send(User $user, string $message): void
    {
        $this->twilio->messages->create(
            $user->phone,
            [
                'from' => config('services.twilio.from'),
                'body' => $message,
            ]
        );
    }
}

// 3. Inventory Service
class InventoryService implements InventoryInterface
{
    private ProductRepositoryInterface $productRepository;
    private CacheManager $cache;

    public function __construct(
        ProductRepositoryInterface $productRepository,
        CacheManager $cache
    ) {
        $this->productRepository = $productRepository;
        $this->cache = $cache;
    }

    public function checkAvailability(int $productId, int $quantity): bool
    {
        $stock = $this->getStock($productId);
        return $stock >= $quantity;
    }

    public function reserveStock(int $productId, int $quantity): void
    {
        $product = $this->productRepository->find($productId);

        if (!$product || $product->stock < $quantity) {
            throw new \Exception("Insufficient stock for product {$productId}");
        }

        $product->stock -= $quantity;
        $this->productRepository->save($product);
        $this->cache->forget("product_stock_{$productId}");
    }

    public function releaseStock(int $productId, int $quantity): void
    {
        $product = $this->productRepository->find($productId);

        if (!$product) {
            throw new \Exception("Product {$productId} not found");
        }

        $product->stock += $quantity;
        $this->productRepository->save($product);
        $this->cache->forget("product_stock_{$productId}");
    }

    public function getStock(int $productId): int
    {
        $cacheKey = "product_stock_{$productId}";

        return $this->cache->remember($cacheKey, 3600, function () use ($productId) {
            $product = $this->productRepository->find($productId);
            return $product ? $product->stock : 0;
        });
    }
}

// 4. Order Repository
class EloquentOrderRepository implements OrderRepositoryInterface
{
    public function save(Order $order): void
    {
        $order->save();
    }

    public function find(int $id): ?Order
    {
        return Order::find($id);
    }

    public function findByUser(int $userId): array
    {
        return Order::where('user_id', $userId)->get()->all();
    }
}
Enter fullscreen mode Exit fullscreen mode
Step 3: The Refactored Order Service (High Cohesion, Low Coupling)
class OrderService
{
    public function __construct(
        private readonly OrderRepositoryInterface $orderRepository,
        private readonly PaymentGatewayInterface $paymentGateway,
        private readonly NotificationInterface $notificationService,
        private readonly InventoryInterface $inventoryService,
        private readonly LoggerInterface $logger
    ) {}

    public function createOrder(int $userId, array $items): Order
    {
        $this->logger->info('Creating order', ['user_id' => $userId]);

        // Validate and reserve inventory
        $total = 0;
        foreach ($items as $item) {
            if (!$this->inventoryService->checkAvailability($item['product_id'], $item['quantity'])) {
                throw new \Exception("Product {$item['product_id']} unavailable");
            }

            $product = Product::find($item['product_id']);
            $total += $product->price * $item['quantity'];

            $this->inventoryService->reserveStock($item['product_id'], $item['quantity']);
        }

        // Create order
        $order = new Order();
        $order->user_id = $userId;
        $order->items = json_encode($items);
        $order->total = $total;
        $order->status = 'pending';

        $this->orderRepository->save($order);

        // Send notifications
        $user = User::find($userId);
        $this->notificationService->send($user, "Order #{$order->id} created!", 'email');
        $this->notificationService->send($user, "Order #{$order->id} created!", 'sms');

        $this->logger->info('Order created successfully', ['order_id' => $order->id]);

        return $order;
    }

    public function processPayment(Order $order, string $paymentMethodId): array
    {
        if ($order->status !== 'pending') {
            throw new \Exception("Cannot process payment for order with status: {$order->status}");
        }

        $this->logger->info('Processing payment', ['order_id' => $order->id]);

        $result = $this->paymentGateway->charge(
            $order->total,
            'usd',
            $paymentMethodId
        );

        $order->payment_intent_id = $result['transaction_id'];
        $order->status = 'paid';
        $this->orderRepository->save($order);

        $this->logger->info('Payment processed', [
            'order_id' => $order->id,
            'transaction_id' => $result['transaction_id'],
        ]);

        return $result;
    }

    public function fulfillOrder(Order $order): void
    {
        if ($order->status !== 'paid') {
            throw new \Exception("Only paid orders can be fulfilled");
        }

        $this->logger->info('Fulfilling order', ['order_id' => $order->id]);

        // Generate shipping labels
        $items = json_decode($order->items, true);
        foreach ($items as $item) {
            $this->generateShippingLabel($item, $order);
        }

        $order->status = 'shipped';
        $this->orderRepository->save($order);

        $user = User::find($order->user_id);
        $this->notificationService->send($user, "Order #{$order->id} shipped!", 'email');

        $this->logger->info('Order fulfilled', ['order_id' => $order->id]);
    }

    private function generateShippingLabel(array $item, Order $order): void
    {
        // Shipping label logic (focused)
        // Could be extracted to a ShippingService
    }
}
Enter fullscreen mode Exit fullscreen mode
Step 4: Wiring Everything Together (Factory or DI Container)
// Service Provider (Laravel)
class AppServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        // Bind interfaces to concretions
        $this->app->bind(PaymentGatewayInterface::class, StripeGateway::class);
        $this->app->bind(OrderRepositoryInterface::class, EloquentOrderRepository::class);
        $this->app->bind(InventoryInterface::class, InventoryService::class);

        // Bind notification channels
        $this->app->bind(NotificationInterface::class, function ($app) {
            return new NotificationService([
                'email' => $app->make(EmailChannel::class),
                'sms' => $app->make(SmsChannel::class),
            ]);
        });

        // Bind OrderService with all dependencies
        $this->app->bind(OrderService::class, function ($app) {
            return new OrderService(
                $app->make(OrderRepositoryInterface::class),
                $app->make(PaymentGatewayInterface::class),
                $app->make(NotificationInterface::class),
                $app->make(InventoryInterface::class),
                $app->make(LoggerInterface::class)
            );
        });
    }
}
Enter fullscreen mode Exit fullscreen mode

What We Improved

  1. High Cohesion:

    • Each service has one clear responsibility.
    • OrderService only handles order orchestration.
    • StripeGateway only handles Stripe API calls.
    • InventoryService only handles inventory operations.
  2. Low Coupling:

    • OrderService depends on interfaces, not concretions.
    • Services can be swapped without changing OrderService.
    • Dependencies are injected.
  3. Testability:

    • Test OrderService with mocks.
    • Test StripeGateway independently.
    • Test InventoryService independently.
  4. Maintainability:

    • Changes to Stripe only affect StripeGateway.
    • Changes to notifications only affect NotificationService.
  5. Extensibility:

    • Add PayPal by implementing PaymentGatewayInterface.
    • Add Slack by adding a new channel to NotificationService.

8. Laravel Internal Example

Laravel is a masterclass in high cohesion and low coupling. Let's look at how the framework applies these principles.

The HTTP Kernel (Separation of Concerns)

// Illuminate\Foundation\Http\Kernel
class Kernel implements KernelContract
{
    // High cohesion: Each middleware has one job
    protected $middleware = [
        \Illuminate\Session\Middleware\StartSession::class,      // Session management
        \Illuminate\View\Middleware\ShareErrorsFromSession::class, // Error sharing
        \App\Http\Middleware\Authenticate::class,                 // Authentication
        \App\Http\Middleware\VerifyCsrfToken::class,              // CSRF protection
    ];

    // Low coupling: Middleware are independent
    // They communicate through the request/response interface
    public function handle($request)
    {
        // The pipeline handles middleware composition
        return (new Pipeline($this->app))
            ->send($request)
            ->through($this->middleware)
            ->then(function ($request) {
                return $this->router->dispatch($request);
            });
    }
}
Enter fullscreen mode Exit fullscreen mode

Why This Is Elegant:

  • High Cohesion: Each middleware has one responsibility.
  • Low Coupling: Middleware don't know about each other.
  • Extensibility: Add a new middleware without changing others.
  • Testability: Test each middleware independently.

Service Providers (Configuration Cohesion)

// App\Providers\AppServiceProvider
class AppServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        // Cohesion: Register services
        $this->app->bind(PaymentGatewayInterface::class, StripeGateway::class);
        $this->app->bind(NotificationInterface::class, NotificationService::class);
    }

    public function boot(): void
    {
        // Cohesion: Boot services
        $this->loadMigrationsFrom(database_path('migrations'));
        $this->loadRoutesFrom(base_path('routes/api.php'));
    }
}
Enter fullscreen mode Exit fullscreen mode

Why This Is Elegant:

  • High Cohesion: Each provider focuses on a specific domain.
  • Low Coupling: Providers don't depend on each other.
  • Organization: You can have multiple providers for different concerns.

Eloquent Model (Data Cohesion)

// Illuminate\Database\Eloquent\Model
abstract class Model implements Arrayable, ArrayAccess, Jsonable
{
    // High cohesion: All methods relate to data operations
    public function save() { /* ... */ }
    public function delete() { /* ... */ }
    public function update() { /* ... */ }
    public function find() { /* ... */ }

    // Low coupling: Depends on QueryBuilder interface
    protected $queryBuilder;
    protected $connection;
}
Enter fullscreen mode Exit fullscreen mode

Why This Is Elegant:

  • High Cohesion: All model methods relate to data.
  • Low Coupling: Model doesn't know about the database implementation.
  • Extensibility: You can use different connections.

Events and Listeners (Loose Coupling)

// Event (one responsibility)
class OrderShipped
{
    public function __construct(public Order $order) {}
}

// Listener (one responsibility)
class SendShipmentNotification
{
    public function handle(OrderShipped $event): void
    {
        // Send notification
    }
}
Enter fullscreen mode Exit fullscreen mode

Why This Is Elegant:

  • High Cohesion: Each event/listener has one purpose.
  • Low Coupling: The event system connects them loosely.
  • Extensibility: Add a listener without changing the event.

Facades (Simplified Coupling)

class Cache extends Facade
{
    // High cohesion: All cache operations
    // Low coupling: The facade connects to the actual service
    protected static function getFacadeAccessor()
    {
        return 'cache';
    }
}
Enter fullscreen mode Exit fullscreen mode

Why This Is Elegant:

  • The facade provides a simple, cohesive interface.
  • It decouples the client from the implementation.

The Service Container (Coupling Manager)

// The container manages dependencies, reducing coupling
app()->bind(PaymentGatewayInterface::class, StripeGateway::class);
app()->make(OrderService::class); // All dependencies resolved
Enter fullscreen mode Exit fullscreen mode

Why This Is Elegant:

  • The container handles wiring, reducing coupling.
  • Classes declare their dependencies, the container provides them.

9. Real Laravel Application Example

Let's build a Customer Support Ticket System with high cohesion and low coupling.

Scenario

Your SaaS application needs a ticket system for customer support. Tickets can be created, assigned, escalated, resolved, and closed. Each action triggers notifications, logging, and updates to metrics.

Implementation

Step 1: Define Interfaces (Contracts)
// app/Contracts/TicketRepositoryInterface.php
namespace App\Contracts;

use App\Models\Ticket;

interface TicketRepositoryInterface
{
    public function save(Ticket $ticket): void;
    public function find(int $id): ?Ticket;
    public function findByUser(int $userId): array;
    public function findByStatus(string $status): array;
    public function assignToAgent(Ticket $ticket, int $agentId): void;
}

// app/Contracts/NotificationServiceInterface.php
namespace App\Contracts;

use App\Models\User;
use App\Models\Ticket;

interface NotificationServiceInterface
{
    public function notifyCreated(Ticket $ticket, User $assignee): void;
    public function notifyAssigned(Ticket $ticket, User $agent): void;
    public function notifyEscalated(Ticket $ticket, string $reason): void;
    public function notifyResolved(Ticket $ticket): void;
    public function notifyClosed(Ticket $ticket): void;
}

// app/Contracts/TicketMetricsInterface.php
namespace App\Contracts;

use App\Models\Ticket;

interface TicketMetricsInterface
{
    public function trackCreated(Ticket $ticket): void;
    public function trackResolved(Ticket $ticket): void;
    public function trackEscalated(Ticket $ticket): void;
    public function getStatistics(array $filters): array;
}

// app/Contracts/TicketEscalationInterface.php
namespace App\Contracts;

use App\Models\Ticket;

interface TicketEscalationInterface
{
    public function shouldEscalate(Ticket $ticket): bool;
    public function escalate(Ticket $ticket): void;
    public function getEscalationLevel(Ticket $ticket): int;
}
Enter fullscreen mode Exit fullscreen mode
Step 2: Implement Services (High Cohesion)
// app/Services/TicketRepository.php
namespace App\Services;

use App\Contracts\TicketRepositoryInterface;
use App\Models\Ticket;

class EloquentTicketRepository implements TicketRepositoryInterface
{
    public function save(Ticket $ticket): void
    {
        $ticket->save();
    }

    public function find(int $id): ?Ticket
    {
        return Ticket::find($id);
    }

    public function findByUser(int $userId): array
    {
        return Ticket::where('user_id', $userId)->get()->all();
    }

    public function findByStatus(string $status): array
    {
        return Ticket::where('status', $status)->get()->all();
    }

    public function assignToAgent(Ticket $ticket, int $agentId): void
    {
        $ticket->assigned_to = $agentId;
        $ticket->assigned_at = now();
        $ticket->save();
    }
}

// app/Services/NotificationService.php
namespace App\Services;

use App\Contracts\NotificationServiceInterface;
use App\Models\User;
use App\Models\Ticket;
use Illuminate\Support\Facades\Mail;

class NotificationService implements NotificationServiceInterface
{
    public function notifyCreated(Ticket $ticket, User $assignee): void
    {
        Mail::to($assignee->email)->send(new TicketCreatedNotification($ticket));

        // Also send Slack notification if agent has Slack
        if ($assignee->slack_id) {
            // Send Slack message
        }
    }

    public function notifyAssigned(Ticket $ticket, User $agent): void
    {
        // Notify the agent
        Mail::to($agent->email)->send(new TicketAssignedNotification($ticket));

        // Notify the customer
        Mail::to($ticket->user->email)->send(new TicketAssignedCustomerNotification($ticket));
    }

    public function notifyEscalated(Ticket $ticket, string $reason): void
    {
        // Escalate to team lead
        $lead = User::role('team_lead')->first();
        Mail::to($lead->email)->send(new TicketEscalatedNotification($ticket, $reason));
    }

    public function notifyResolved(Ticket $ticket): void
    {
        // Notify the customer
        Mail::to($ticket->user->email)->send(new TicketResolvedNotification($ticket));

        // Notify all agents
        $agents = User::role('agent')->get();
        foreach ($agents as $agent) {
            Mail::to($agent->email)->send(new TicketResolvedAgentNotification($ticket));
        }
    }

    public function notifyClosed(Ticket $ticket): void
    {
        Mail::to($ticket->user->email)->send(new TicketClosedNotification($ticket));
    }
}

// app/Services/TicketMetrics.php
namespace App\Services;

use App\Contracts\TicketMetricsInterface;
use App\Models\Ticket;
use Illuminate\Support\Facades\DB;

class TicketMetrics implements TicketMetricsInterface
{
    public function trackCreated(Ticket $ticket): void
    {
        DB::table('ticket_metrics')->insert([
            'ticket_id' => $ticket->id,
            'event' => 'created',
            'data' => json_encode([
                'user_id' => $ticket->user_id,
                'category' => $ticket->category,
                'priority' => $ticket->priority,
            ]),
            'created_at' => now(),
        ]);

        // Increment daily count
        $this->incrementDailyMetric('tickets_created');
    }

    public function trackResolved(Ticket $ticket): void
    {
        $timeToResolve = $ticket->resolved_at->diffInHours($ticket->created_at);

        DB::table('ticket_metrics')->insert([
            'ticket_id' => $ticket->id,
            'event' => 'resolved',
            'data' => json_encode([
                'time_to_resolve' => $timeToResolve,
                'agent_id' => $ticket->resolved_by,
            ]),
            'created_at' => now(),
        ]);

        $this->incrementDailyMetric('tickets_resolved');
    }

    public function trackEscalated(Ticket $ticket): void
    {
        DB::table('ticket_metrics')->insert([
            'ticket_id' => $ticket->id,
            'event' => 'escalated',
            'data' => json_encode([
                'reason' => $ticket->escalation_reason,
                'level' => $ticket->escalation_level,
            ]),
            'created_at' => now(),
        ]);

        $this->incrementDailyMetric('tickets_escalated');
    }

    public function getStatistics(array $filters): array
    {
        $query = DB::table('ticket_metrics')
            ->select('event', DB::raw('count(*) as count'))
            ->groupBy('event');

        if (isset($filters['from'])) {
            $query->where('created_at', '>=', $filters['from']);
        }
        if (isset($filters['to'])) {
            $query->where('created_at', '<=', $filters['to']);
        }

        return $query->get()->toArray();
    }

    private function incrementDailyMetric(string $key): void
    {
        $today = now()->toDateString();
        DB::table('daily_metrics')->updateOrInsert(
            ['key' => $key, 'date' => $today],
            ['value' => DB::raw('value + 1'), 'updated_at' => now()]
        );
    }
}

// app/Services/TicketEscalation.php
namespace App\Services;

use App\Contracts\TicketEscalationInterface;
use App\Models\Ticket;

class TicketEscalation implements TicketEscalationInterface
{
    private const MAX_HOURS_OPEN = 24;
    private const MAX_AGENT_TICKETS = 10;

    public function shouldEscalate(Ticket $ticket): bool
    {
        // Escalate if ticket has been open too long
        if ($ticket->created_at->diffInHours(now()) > self::MAX_HOURS_OPEN) {
            return true;
        }

        // Escalate if agent has too many tickets
        if ($ticket->assigned_to) {
            $agentTickets = Ticket::where('assigned_to', $ticket->assigned_to)
                ->where('status', 'open')
                ->count();

            if ($agentTickets > self::MAX_AGENT_TICKETS) {
                return true;
            }
        }

        return false;
    }

    public function escalate(Ticket $ticket): void
    {
        $ticket->escalation_level = $this->getEscalationLevel($ticket) + 1;
        $ticket->escalation_reason = $this->determineEscalationReason($ticket);
        $ticket->escalated_at = now();
        $ticket->status = 'escalated';
        $ticket->save();
    }

    public function getEscalationLevel(Ticket $ticket): int
    {
        return $ticket->escalation_level ?? 0;
    }

    private function determineEscalationReason(Ticket $ticket): string
    {
        if ($ticket->created_at->diffInHours(now()) > self::MAX_HOURS_OPEN) {
            return "Ticket open for more than " . self::MAX_HOURS_OPEN . " hours";
        }

        if ($ticket->assigned_to) {
            $agentTickets = Ticket::where('assigned_to', $ticket->assigned_to)
                ->where('status', 'open')
                ->count();

            if ($agentTickets > self::MAX_AGENT_TICKETS) {
                return "Agent has {$agentTickets} open tickets (max " . self::MAX_AGENT_TICKETS . ")";
            }
        }

        return "Escalated due to unspecified reason";
    }
}
Enter fullscreen mode Exit fullscreen mode
Step 3: The Orchestrator (High Cohesion, Low Coupling)
// app/Services/TicketService.php
namespace App\Services;

use App\Contracts\TicketRepositoryInterface;
use App\Contracts\NotificationServiceInterface;
use App\Contracts\TicketMetricsInterface;
use App\Contracts\TicketEscalationInterface;
use App\Models\Ticket;
use App\Models\User;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\DB;

class TicketService
{
    public function __construct(
        private readonly TicketRepositoryInterface $repository,
        private readonly NotificationServiceInterface $notification,
        private readonly TicketMetricsInterface $metrics,
        private readonly TicketEscalationInterface $escalation,
        private readonly LoggerInterface $logger
    ) {}

    public function create(array $data): Ticket
    {
        DB::beginTransaction();

        try {
            $ticket = new Ticket();
            $ticket->user_id = $data['user_id'];
            $ticket->subject = $data['subject'];
            $ticket->description = $data['description'];
            $ticket->category = $data['category'];
            $ticket->priority = $data['priority'];
            $ticket->status = 'open';
            $ticket->created_at = now();

            $this->repository->save($ticket);

            // Auto-assign to available agent
            $agent = $this->findAvailableAgent();
            if ($agent) {
                $this->repository->assignToAgent($ticket, $agent->id);
                $this->notification->notifyAssigned($ticket, $agent);
            }

            // Track metrics
            $this->metrics->trackCreated($ticket);

            // Notify user
            $this->notification->notifyCreated($ticket, $ticket->user);

            $this->logger->info('Ticket created', ['ticket_id' => $ticket->id]);

            DB::commit();

            return $ticket;

        } catch (\Exception $e) {
            DB::rollBack();
            $this->logger->error('Ticket creation failed', ['error' => $e->getMessage()]);
            throw $e;
        }
    }

    public function resolve(Ticket $ticket, User $agent, string $resolution): void
    {
        if ($ticket->status === 'closed') {
            throw new \Exception("Ticket already closed");
        }

        DB::beginTransaction();

        try {
            $ticket->status = 'resolved';
            $ticket->resolved_at = now();
            $ticket->resolved_by = $agent->id;
            $ticket->resolution = $resolution;

            $this->repository->save($ticket);

            // Track metrics
            $this->metrics->trackResolved($ticket);

            // Notify customer
            $this->notification->notifyResolved($ticket);

            $this->logger->info('Ticket resolved', [
                'ticket_id' => $ticket->id,
                'agent_id' => $agent->id,
            ]);

            DB::commit();

        } catch (\Exception $e) {
            DB::rollBack();
            $this->logger->error('Ticket resolution failed', ['error' => $e->getMessage()]);
            throw $e;
        }
    }

    public function checkAndEscalate(Ticket $ticket): void
    {
        if ($ticket->status !== 'open' && $ticket->status !== 'in_progress') {
            return;
        }

        if ($this->escalation->shouldEscalate($ticket)) {
            DB::beginTransaction();

            try {
                $this->escalation->escalate($ticket);
                $this->repository->save($ticket);

                // Track metrics
                $this->metrics->trackEscalated($ticket);

                // Notify team lead
                $this->notification->notifyEscalated($ticket, $ticket->escalation_reason);

                $this->logger->warning('Ticket escalated', [
                    'ticket_id' => $ticket->id,
                    'level' => $ticket->escalation_level,
                    'reason' => $ticket->escalation_reason,
                ]);

                DB::commit();

            } catch (\Exception $e) {
                DB::rollBack();
                $this->logger->error('Ticket escalation failed', ['error' => $e->getMessage()]);
                throw $e;
            }
        }
    }

    public function getMetrics(array $filters): array
    {
        return $this->metrics->getStatistics($filters);
    }

    private function findAvailableAgent(): ?User
    {
        // Find agent with fewest open tickets
        $agent = User::role('agent')
            ->withCount('tickets')
            ->orderBy('tickets_count')
            ->first();

        return $agent;
    }
}
Enter fullscreen mode Exit fullscreen mode
Step 4: Controller (Thin Client)
// app/Http/Controllers/TicketController.php
namespace App\Http\Controllers;

use App\Models\Ticket;
use App\Services\TicketService;
use Illuminate\Http\Request;

class TicketController extends Controller
{
    public function __construct(
        private readonly TicketService $ticketService
    ) {}

    public function store(Request $request)
    {
        try {
            $ticket = $this->ticketService->create($request->validated());
            return response()->json($ticket, 201);
        } catch (\Exception $e) {
            return response()->json(['error' => $e->getMessage()], 400);
        }
    }

    public function resolve(Request $request, Ticket $ticket)
    {
        try {
            $this->ticketService->resolve($ticket, $request->user(), $request->input('resolution'));
            return response()->json(['message' => 'Ticket resolved']);
        } catch (\Exception $e) {
            return response()->json(['error' => $e->getMessage()], 400);
        }
    }

    public function metrics(Request $request)
    {
        return response()->json(
            $this->ticketService->getMetrics($request->only(['from', 'to']))
        );
    }
}
Enter fullscreen mode Exit fullscreen mode

Why This Design Works

  1. High Cohesion:

    • Each class has one clear responsibility.
    • TicketRepository: Data persistence.
    • NotificationService: Sending notifications.
    • TicketMetrics: Tracking statistics.
    • TicketEscalation: Escalation rules.
    • TicketService: Orchestration.
  2. Low Coupling:

    • TicketService depends on interfaces, not concretions.
    • Services are independently testable.
    • Changes to notification logic don't affect ticket logic.
  3. Extensibility:

    • Add a new notification channel (e.g., Slack) by implementing the interface.
    • Change escalation rules by modifying TicketEscalation.
    • Add new metrics by extending TicketMetrics.

10. SOLID Principles Mapping

S - Single Responsibility Principle (SRP)

Cohesion is the direct measure of SRP. Each class in the refactored design has one responsibility:

  • TicketService: Orchestrates ticket operations.
  • TicketRepository: Manages data persistence.
  • NotificationService: Handles notifications.
  • TicketMetrics: Tracks statistics.
  • TicketEscalation: Manages escalation rules.

O - Open/Closed Principle (OCP)

Low coupling enables OCP. You can add new implementations without changing existing code:

  • Add a new payment gateway: Implement PaymentGatewayInterface.
  • Add a new notification channel: Implement NotificationChannel.

L - Liskov Substitution Principle (LSP)

With low coupling through interfaces, any implementation can be substituted:

function process(OrderService $service) {
    // Works with any implementation
}
Enter fullscreen mode Exit fullscreen mode

D - Dependency Inversion Principle (DIP)

High-level modules (TicketService) depend on abstractions (interfaces), not concretions. This is the definition of DIP.

I - Interface Segregation Principle (ISP)

Interfaces are focused and minimal:

  • TicketRepositoryInterface: Only repository methods.
  • NotificationServiceInterface: Only notification methods.
  • No class implements methods it doesn't need.

11. Trade-offs

Benefits

  1. Maintainability: Changes are localized.
  2. Testability: Test each class independently.
  3. Readability: Classes are small and focused.
  4. Reusability: Classes can be reused.
  5. Extensibility: Add new features without changing existing code.
  6. Parallel Development: Teams can work on different classes.

Costs

  1. More Classes: High cohesion means more classes.
  2. More Files: Each class gets its own file.
  3. More Complexity: More moving parts to understand.
  4. Setup Overhead: More configuration to wire everything together.
  5. Learning Curve: Developers need to understand the architecture.

When Is Complexity Justified?

Use high cohesion and low coupling when:

  • The application is large and complex.
  • The codebase is likely to grow.
  • Multiple teams work on the codebase.
  • You need to test components independently.
  • You want to adhere to SOLID principles.

Avoid over-engineering when:

  • The application is small and simple.
  • The logic is unlikely to change.
  • The complexity of abstraction isn't justified.

12. When NOT To Use It

3 Green Flags (AIM FOR HIGH COHESION, LOW COUPLING)

  1. Large Codebases: The application is growing or complex.
  2. Multiple Teams: Different teams work on different modules.
  3. Frequent Changes: Requirements change often, requiring flexibility.

3 Red Flags (AVOID OVER-ENGINEERING)

  1. Small Projects: The application is small and simple.
  2. Prototype: You're building a proof of concept.
  3. Stable Logic: The logic is simple and unlikely to change.

13. Common Mistakes

1. Creating "Utility" Classes (Low Cohesion)

// BAD: Utility class with unrelated methods
class StringHelper
{
    public static function slug(string $str): string { /* ... */ }
    public static function generatePassword(): string { /* ... */ }
    public static function validateEmail(string $email): bool { /* ... */ }
    public static function encrypt(string $str): string { /* ... */ }
}
Enter fullscreen mode Exit fullscreen mode

Problem: These methods are unrelated. The class has low cohesion.

Fix: Separate into focused classes: SlugGenerator, PasswordGenerator, EmailValidator, Encrypter.

2. God Objects (Low Cohesion)

// BAD: God object
class UserService
{
    public function createUser() { /* ... */ }
    public function updateUser() { /* ... */ }
    public function deleteUser() { /* ... */ }
    public function sendEmail() { /* ... */ }
    public function generateReport() { /* ... */ }
    public function processPayment() { /* ... */ }
}
Enter fullscreen mode Exit fullscreen mode

Problem: The class does too many things.

Fix: Split into UserService, EmailService, ReportService, PaymentService.

3. Procedural Static Methods (Low Cohesion)

// BAD: All static, no state
class OrderHelper
{
    public static function calculateTotal($items) { /* ... */ }
    public static function applyDiscount($total, $code) { /* ... */ }
    public static function formatCurrency($amount) { /* ... */ }
    public static function generateInvoiceNumber() { /* ... */ }
}
Enter fullscreen mode Exit fullscreen mode

Problem: This is procedural code masquerading as OOP.

Fix: Create focused classes with state: OrderCalculator, DiscountApplier, CurrencyFormatter.

4. Deeply Nested Dependencies (High Coupling)

// BAD: Tight coupling
class OrderService
{
    private StripePayment $payment;
    private TwilioClient $sms;
    private Mailer $mail;
    private Logger $log;

    public function __construct()
    {
        $this->payment = new StripePayment(config('stripe'));
        $this->sms = new TwilioClient(config('twilio'));
        $this->mail = new Mailer(config('mail'));
        $this->log = new Logger(config('log'));
    }
}
Enter fullscreen mode Exit fullscreen mode

Problem: The class is tightly coupled to all dependencies.

Fix: Inject dependencies through the constructor.

5. Law of Demeter Violations (High Coupling)

// BAD: Talking to strangers
$city = $user->getAddress()->getCity()->getName();
$street = $user->getAddress()->getStreet()->getNumber();

// GOOD: Tell the user what you need
$city = $user->getCityName();
$address = $user->getFullAddress();
Enter fullscreen mode Exit fullscreen mode

Problem: You're reaching through multiple objects.

Fix: Each object should provide its own information.


14. Frequently Asked Interview Questions

Beginner/Intermediate

  1. Q: What is cohesion in object-oriented design?
    A: Cohesion measures how well the elements inside a module (class, method) belong together. High cohesion means the module is focused and has a single responsibility.

  2. Q: What is coupling in object-oriented design?
    A: Coupling measures how much a module depends on other modules. Low coupling means modules are independent and communicate through well-defined interfaces.

  3. Q: Why is high cohesion desirable?
    A: High cohesion makes code easier to understand, test, maintain, and reuse. Changes are localized to specific classes.

  4. Q: Why is low coupling desirable?
    A: Low coupling makes code more flexible, maintainable, and testable. Changing one module doesn't require changing others.

  5. Q: What's the relationship between cohesion, coupling, and SOLID?
    A: High cohesion and low coupling are the goals of SOLID principles. Each SOLID principle helps achieve these goals.

Senior/Architect

  1. Q: How do you measure cohesion and coupling in a codebase?
    A: Cohesion can be measured by the number of responsibilities per class. Coupling can be measured by the number of dependencies, the number of imports, or using tools like PHP Metrics.

  2. Q: What's the ideal balance between cohesion and coupling?
    A: There's no single "ideal." It depends on the application. But the goal is high cohesion (classes that do one thing) and low coupling (classes that don't depend on each other).

  3. Q: How does the Law of Demeter relate to coupling?
    A: The Law of Demeter (Don't Talk to Strangers) reduces coupling by restricting which objects you can interact with. It prevents "train wrecks" like $user->getAddress()->getCity()->getName().

  4. Q: How do you refactor a codebase with low cohesion and high coupling?
    A: Start by identifying responsibilities. Extract them into separate classes. Define interfaces for communication. Use dependency injection to wire everything together.

  5. Q: What metrics tools can you use to measure cohesion and coupling in PHP?
    A: PHP Metrics (PHPMetrics), PHPStan, Psalm, and Scrutinizer. They can detect high coupling and low cohesion.


15. Interactive Practice Challenge

The Requirement

You're building a Blog Management System. The current code is a mess of low cohesion and high coupling.

The Code (POOR DESIGN)

// app/Services/BlogService.php
namespace App\Services;

use App\Models\Post;
use App\Models\User;
use App\Models\Category;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;

class BlogService
{
    // 1. Handles post creation
    public function createPost(array $data, User $author): Post
    {
        // Validate
        if (empty($data['title']) || empty($data['content'])) {
            throw new \Exception('Title and content are required');
        }

        // Generate slug
        $slug = Str::slug($data['title']);
        $existing = Post::where('slug', $slug)->first();
        if ($existing) {
            $slug = $slug . '-' . time();
        }

        // Create post
        $post = new Post();
        $post->title = $data['title'];
        $post->slug = $slug;
        $post->content = $data['content'];
        $post->user_id = $author->id;
        $post->status = 'draft';
        $post->views = 0;
        $post->save();

        // Handle categories
        if (isset($data['categories'])) {
            $categoryIds = Category::whereIn('slug', $data['categories'])->pluck('id');
            $post->categories()->sync($categoryIds);
        }

        // Cache
        Cache::forget('recent_posts');
        Cache::forget('popular_posts');

        // Log
        Log::info('Post created', ['post_id' => $post->id, 'author' => $author->id]);

        // Notify subscribers
        $subscribers = User::where('subscribed', true)->get();
        foreach ($subscribers as $subscriber) {
            Mail::to($subscriber->email)->send(new NewPostNotification($post));
        }

        return $post;
    }

    // 2. Handles post publishing
    public function publishPost(Post $post): void
    {
        if ($post->status === 'published') {
            throw new \Exception('Post already published');
        }

        $post->status = 'published';
        $post->published_at = now();
        $post->save();

        // Update cache
        Cache::forget('recent_posts');
        Cache::forget('popular_posts');

        // Send to social media
        $this->postToSocialMedia($post);

        // Send to newsletter
        $this->sendToNewsletter($post);

        Log::info('Post published', ['post_id' => $post->id]);
    }

    // 3. Handles post deletion
    public function deletePost(Post $post): void
    {
        // Remove comments
        $post->comments()->delete();

        // Detach categories
        $post->categories()->detach();

        // Delete post
        $post->delete();

        // Update cache
        Cache::forget('recent_posts');
        Cache::forget('popular_posts');

        Log::info('Post deleted', ['post_id' => $post->id]);
    }

    // 4. Handles post viewing
    public function viewPost(string $slug): ?Post
    {
        $post = Post::with(['user', 'categories', 'comments'])
            ->where('slug', $slug)
            ->where('status', 'published')
            ->first();

        if (!$post) {
            return null;
        }

        // Increment views
        $post->views++;
        $post->save();

        // Cache recent post
        Cache::put("post_{$slug}", $post, 3600);

        // Track analytics
        $this->trackView($post);

        return $post;
    }

    // 5. Handles post search
    public function searchPosts(string $query): array
    {
        // Simple search
        return Post::where('title', 'LIKE', "%{$query}%")
            ->orWhere('content', 'LIKE', "%{$query}%")
            ->where('status', 'published')
            ->limit(50)
            ->get()
            ->all();
    }

    // 6. Handles popular posts
    public function getPopularPosts(int $limit = 10): array
    {
        return Cache::remember('popular_posts', 3600, function () use ($limit) {
            return Post::where('status', 'published')
                ->orderBy('views', 'desc')
                ->limit($limit)
                ->get()
                ->all();
        });
    }

    // 7. Handles related posts
    public function getRelatedPosts(Post $post, int $limit = 5): array
    {
        $categories = $post->categories->pluck('id');

        return Post::where('id', '!=', $post->id)
            ->where('status', 'published')
            ->whereHas('categories', function ($query) use ($categories) {
                $query->whereIn('category_id', $categories);
            })
            ->limit($limit)
            ->get()
            ->all();
    }

    // 8. Handles analytics
    private function trackView(Post $post): void
    {
        // Track view in analytics system
        $this->sendAnalyticsEvent('post_viewed', [
            'post_id' => $post->id,
            'user_id' => auth()->id(),
            'timestamp' => now(),
        ]);
    }

    private function sendAnalyticsEvent(string $event, array $data): void
    {
        // Send to analytics service
        Log::debug('Analytics event', ['event' => $event, 'data' => $data]);
    }

    // 9. Handles social media
    private function postToSocialMedia(Post $post): void
    {
        // Post to Twitter
        // Post to Facebook
        // Post to LinkedIn
        Log::info('Posted to social media', ['post_id' => $post->id]);
    }

    // 10. Handles newsletter
    private function sendToNewsletter(Post $post): void
    {
        // Send to newsletter service
        Log::info('Sent to newsletter', ['post_id' => $post->id]);
    }
}
Enter fullscreen mode Exit fullscreen mode

The Challenges

The BlogService is a God Class with 10+ responsibilities. It has:

  • Low cohesion: Handles creation, publishing, deletion, viewing, searching, popular posts, related posts, analytics, social media, and newsletters.
  • High coupling: Depends on Post, User, Category, Cache, Log, Mail, DB, and Str.

New requirements are coming:

  1. "We need to schedule posts for future publication."
  2. "We need to save drafts and autosave."
  3. "We need to moderate comments before they appear."
  4. "We need to add SEO metadata."
  5. "We need to export posts to JSON, XML, and RSS."
  6. "We need to track post performance with detailed analytics."
  7. "We need to add multiple authors and review workflows."

Your Task

Refactor this system using high cohesion and low coupling. Specifically:

  1. Identify all responsibilities in the BlogService.
  2. Create focused classes for each responsibility:

    • PostCreator
    • PostPublisher
    • PostDeleter
    • PostViewer
    • PostSearcher
    • PostQueries
    • SocialMediaPublisher
    • NewsletterService
    • AnalyticsService
    • CacheManager
    • PostRepository
  3. Define interfaces for each service.

  4. Implement services with high cohesion.

  5. Wire everything together using dependency injection.

  6. Update the controller to use the new services.

Questions to Consider

  • How should you handle the relationships between services?
  • Should PostPublisher depend on SocialMediaPublisher and NewsletterService?
  • How do you handle caching across services?
  • How do you test each service independently?
  • Where should you put the orchestration logic?

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


16. Final Mental Model

To keep it simple, memorize these three sentences:

  • One-sentence definition: Cohesion measures how well a class's methods belong together; coupling measures how much a class depends on others.

  • One-sentence intuition: High cohesion means each class does one thing; low coupling means classes don't depend on each other.

  • One-sentence decision rule: If a class has more than one clear responsibility, split it. If a class knows too much about another, use dependency injection and interfaces.


17. Related Concepts

SOLID Principles

  • Single Responsibility: Cohesion is the measure of SRP.
  • Dependency Inversion: Low coupling through interfaces.
  • Open/Closed: Low coupling enables extension.
  • Liskov Substitution: Low coupling through substitutability.
  • Interface Segregation: Low coupling through focused interfaces.

Design Patterns

  • Strategy Pattern: Low coupling through interchangeable algorithms.
  • Factory Pattern: Reduces coupling by creating objects.
  • Observer Pattern: Low coupling between event sources and listeners.
  • Decorator Pattern: Adds behavior without changing classes.
  • Adapter Pattern: Reduces coupling between incompatible interfaces.

Laravel Internals

  • Service Container: Reduces coupling by managing dependencies.
  • Service Providers: Organize services by cohesion.
  • Contracts: Define interfaces for low coupling.
  • Facades: Simplified access with controlled coupling.
  • Middleware: Each middleware has one responsibility (high cohesion).
  • Events/Listeners: Decouple event sources from handlers.

Enterprise Patterns

  • Repository Pattern: Separates data access (high cohesion in data layer).
  • Service Layer Pattern: Organizes business logic.
  • Unit of Work: Manages transactions.
  • DTOs: Reduce coupling between layers.
  • CQRS: Separates commands from queries.

Final Thoughts

Cohesion and coupling are the foundational metrics of object-oriented design. They tell you if your code is healthy or dying.

High cohesion and low coupling are the goals. They make your code:

  • Maintainable: Changes are localized.
  • Testable: Each class can be tested independently.
  • Readable: Classes are small and focused.
  • Reusable: Classes can be used in different contexts.
  • Extensible: New features don't break existing code.

The next time you find yourself in a codebase where changes break everything, think about cohesion and coupling. You'll probably find low cohesion and high coupling.

Remember: High cohesion means "do one thing." Low coupling means "don't know about others." Master these two metrics, and you'll master object design.


Github: Cohesion and Coupling Practice Labs

Top comments (0)