Princípios SOLID: o que são e como aplicá-los no PHP/Laravel (Parte 01 - Responsabilidade Única)

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.