Princípios SOLID: o que são e como aplicá-los no PHP/Laravel (Parte 01 - Responsabilidade Única)
Hoje vou abordar um dos temas que mais me interessa dentro do mundo do desenvolvimento de software: escrever códigos melhores.
Dentro deste tema costuma-se falar de arquitetura de software, padrões de projeto, design de software, etc.
Nesta série de artigos vou abordar o SOLID que é um princípio de design de software orientado a objeto (OOD). Basicamente, SOLID é um acrônimo para:
S - Single Responsibility Principle
O - Open-closed Principle
L - Liskov Substitution Principle
I - Interface Segregation Principle
D - Dependency Inversion Principle
Vamos entender como funciona cada um deles e exemplicar usando o PHP. Mais especificamente o Laravel, por ser um framework que adota o MVC por padrão e, com o crescimento da aplicação, tende a ficar com o código/estrutura bem bagunçados se não organizar desde o começo.
Então, neste primeiro artigo vamos abordar o primeiro dos princípios de forma detalhada.
Single Responsibility Principle
(Princípio da Responsabilidade Única)
Uma classe deve ter uma e apenas uma razão para mudança, significando que uma classe deve ter apenas uma responsabilidade.
Este é o primeiro, mais simples, e mais importante princípio, pois parte da premissa que toda classe deve ter apenas 1 objetivo quando se trata do fluxo de uma requisição do software.
Vamos dar uma olhada no exemplo abaixo:
class UserRegistrationController extends Controller
{
public function register(Request $request)
{
$attributes = $request->all();
if($attributes['type'] === 'employee') {
$this->saveEmployee($attributes);
}
if($attributes['type'] === 'contractor') {
$this->saveContractor($attributes);
}
return view('user.register');
}
public function saveEmployee($request)
{
DB::table('employees')->insert([
/* Dados do funcionário aqui... */
]);
}
public function saveContractor($request)
{
DB::table('employees')->insert([
/* Dados do terceirizado aqui... */
]);
}
}
Esta classe acima viola o princípio da responsabilidade única pelos motivos a seguir:
- Mistura responsabilidades. Um Controller deve ser responsável por apenas gerenciar requests e responses.
- Regra de negócio de ser aplicada em uma camada separada, como a camada de Service, por exemplo. (que não tem como padrão no Laravel, mas podemos criá-la manualmente)
- Operações com o banco de dados também devem estar em outra camada. No Laravel, por padrão, usamos o Model. Mas no neste artigo vou usar a camada de Repository. (mesmo caso do Service)
Vamos corrigir a classe:
class UserRegistrationController extends Controller
{
protected $userService;
public function __construct(UserService $userService)
{
$this->userService = $userService;
}
public function register(Request $request)
{
$userService->register($request);
return view('user.register');
}
}
Aqui deixamos o Controller isolado com a sua única responsabilidade: gerenciar a request e a response.
No Laravel, para podermos adicionar uma camada de comunicação com a classe atual, nós usamos a injeção de dependência nativa do framework. Basta adicionar a classe desejada como parâmetro no construtor e atribuí-la em uma propriedade da classe. E eu fiz isso adicionando a classe UserService
.
Vamos entender agora a classe UserService
:
class UserService
{
protected $employeeRepository;
protected $contractorRepository;
public function __construct(
EmployeeRepository $employeeRepository,
ContractorRepository $contractorRepository
)
{
$this->userRepository = $userRepository;
$this->contractorRepository = $contractorRepository;
}
public function register(Request $request)
{
$attributes = $request->all();
if($attributes['type'] === 'employee') {
$this->employeeRepository->save($attributes);
}
if($attributes['type'] === 'contractor') {
$this->contractorRepository->save($attributes);
}
}
}
O Service é o local onde colocamos toda a regra de negócio, como é o caso do condicional que usamos para identificar o tipo de usuário.
Aqui também usamos a injeção de dependência, mas importamos 2 classes. Exatamente os Repositories dos tipos de usuário. Como o Model é a representação da tabela do banco dentro da aplicação, e o Repository é reponsável pela operação de banco de cada Model, nada mais coerente mantê-los separados.
Vamos entender agora os Repositories:
class EmployeeRespository
{
protected $employee;
public function __construct(Employee $employee)
{
$this->employee = $employee;
}
public function save(array $attributes)
{
$this->employee->insert($attributes);
}
}
class ContractorRespository
{
protected $contractor;
public function __construct(Employee $contractor)
{
$this->contractor = $contractor;
}
public function save(array $attributes)
{
$this->contractor->insert($attributes);
}
}
Em ambos os Repositories utilizamos a mesma estrutura: injeção de dependência com o respectivos Models, e um método save()
.
Dessa maneira, separamos as responsabilidades em camadas, deixando o código mais legível e com uma arquitetura mais correta.
No próximo artigo vou abordar o segundo princípio: Open-closed Principle (Princípio Aberto-fechado).
Espero que você tenha gostado. Dúvidas e feedbacks são sempre muito bem vindos.
PS: Se você quiser adicionar Services e Repositories na sua aplicação Laravel, é só criar as pastas app/Services
e app/Repositories
e adicionar suas classes manualmente.
PPS: Neste exemplo abordei apenas o essencial para explicá-lo, deixando de fora imports, returns, validators, etc.
PPPS: Eu sei que o ideal seria ter um Model User
e estender para Employee
e Contractor
. Mas para o exemplo ficar mais claro, deixei-os separados mesmo.