Robert C. Martin, also known as Uncle Bob, emphasizes in his book Clean Architecture the importance of good code as the foundation of good software systems. He compares well-made code to high-quality bricks in building construction. Even with excellent architectural plans, poorly made bricks can undermine the entire structure. On the other hand, well-made bricks can still result in a messy construction if not assembled properly. This is where SOLID principles play a crucial role.
SOLID is an acronym representing five fundamental principles in object-oriented programming and design. Let's break it down:
- S stands for Single Responsibility Principle (SRP)
- O stands for Open/Closed Principle (OCP)
- L stands for Liskov Substitution Principle (LSP)
- I stands for Interface Segregation Principle (ISP)
- D stands for Dependency Inversion Principle (DIP)
These principles, coined by Robert C. Martin, provide guidelines for writing clean, maintainable, and extensible code. Each principle focuses on a specific aspect of software design, aiming to improve modularity, flexibility, and scalability. Understanding and applying these principles can lead to more robust and adaptable software systems.
Single Responsibility Principle
In this series kickoff, we'll explore SRP with example using PHP & Laravel framework, showcasing how it fosters cleaner, more maintainable code.
The Single Responsibility Principle (SRP) emphasizes that a class or method should have only one reason to change, meaning it should have a single responsibility or purpose. By adhering to SRP, we ensure that each entity in our codebase is focused on doing one thing well, making our code more maintainable, understandable, and flexible.
Imagine writing a Laravel controller for product management that not only validates & store product data but also dispatches email notifications to interested customers.
<?php
namespace App\Http\Controllers;
use App\Models\Product;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Mail;
use App\Mail\ProductRequestedNotification;
class ProductController extends Controller
{
public function store(Request $request)
{
// Validation rules
$validatedData = $request->validate([
'name' => 'required|string|max:255',
'description' => 'required|string',
'price' => 'required|numeric|min:0',
'quantity' => 'required|integer|min:0',
'email' => 'required|email',
]);
// Create new product
$product = Product::create([
'name' => $validatedData['name'],
'description' => $validatedData['description'],
'price' => $validatedData['price'],
'quantity' => $validatedData['quantity'],
]);
// Send notification to customer
Mail::to($validatedData['email'])->send(new ProductRequestedNotification($product));
// Return success response
return response()->json(['message' => 'Product stored successfully'], 200);
}
}
In the provided example, the ProductController
is handling multiple responsibilities, including data validation, product storage, and email notification. This violates the Single Responsibility Principle (SRP), which states that a class should have only one reason to change.
To adhere to SRP and improve the controller's clarity and extensibility, we can refactor it to delegate each responsibility to separate classes. This separation of concerns enhances maintainability and allows for easier extension in the future:
- Request Validation: Extract the validation rules to a separate request class.
- Product Creation: Move the product creation logic to a service class.
- Notification: Handle the notification logic in a separate class.
First, create a new Form Request class using the artisan command:
php artisan make:request StoreProductRequest
This will generate a new file StoreProductRequest.php
in the app/Http/Requests
directory.
Open the StoreProductRequest.php
file and update the rules()
method with the validation rules:
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class StoreProductRequest extends FormRequest
{
public function authorize()
{
return true; // Authorization logic can be added if needed
}
public function rules()
{
return [
'name' => 'required|string|max:255',
'description' => 'required|string',
'price' => 'required|numeric|min:0',
'quantity' => 'required|integer|min:0',
'email' => 'required|email',
];
}
}
To create the ProductService
class, follow these steps:
- Navigate to the app/Http/ directory in your Laravel application.
- Create a new folder named
Services
if it doesn't exist already. - Inside the
Services
folder, create a new file namedProductService.php
. - Open the
ProductService.php
file and add the following code:
<?php
namespace App\Services;
use App\Models\Product;
class ProductService
{
public function create(array $data): Product
{
return Product::create([
'name' => $data['name'],
'description' => $data['description'],
'price' => $data['price'],
'quantity' => $data['quantity'],
]);
}
}
Lastly, we will generate a Laravel Notification
<?php
namespace App\Notifications;
use App\Models\Product;
use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Notification;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
class ProductRequestedNotification extends Notification implements ShouldQueue
{
use Queueable;
protected $product;
public function __construct(Product $product)
{
$this->product = $product;
}
public function via($notifiable)
{
return ['mail'];
}
public function toMail($notifiable)
{
return (new MailMessage)
->line('Your product request has been received successfully.')
->line('Product Name: ' . $this->product->name)
->line('Description: ' . $this->product->description)
->line('Price: ' . $this->product->price)
->line('Quantity: ' . $this->product->quantity);
}
}
Here's what the ProductController
looks like after refactoring:
<?php
namespace App\Http\Controllers;
use App\Http\Requests\ProductRequest;
use App\Services\ProductService;
use App\Notifications\ProductRequestedNotification;
class ProductController extends Controller
{
protected $productService;
public function __construct(ProductService $productService)
{
$this->productService = $productService;
}
public function store(ProductRequest $request)
{
// Create new product
$product = $this->productService->create($request->validated());
// Send notification to customer
$product->notify(new ProductRequestedNotification($product));
// Return success response
return response()->json(['message' => 'Product stored successfully'], 200);
}
}
With this refactoring, the ProductController
is now responsible only for handling HTTP requests and responses, while the validation rules are encapsulated in the StoreProductRequest
class, the email notification is handled by ProductRequestedNotification
class, and the business logic for creating a product is handled by the ProductService
class. This separation of concerns improves code readability, maintainability, and testability.
In the upcoming part of the series, we'll delve into another fundamental principle: the Open/Closed Principle.
Top comments (1)
π