Do not speak Portuguese? Translate this site with Google or Bing Translator
Solid no Laravel

Posted on: June 30, 2023 07:21 PM

Posted by: Renato

Views: 142

Princípios SOLID: o que são e como aplicá-los no PHP/Laravel

OLID 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

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.

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);
        }
    }
}

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 importsreturnsvalidators, 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.

Open-closed Principle

(Princípio Aberto-fechado)

Objetos ou entidades devem ser abertas para extensão, mas fechadas para modificação.

Em outras palavras, se eu tenho uma classe genérica que atende a diversos tipos de perfis, ao invés de sair fazer condicionais para cada caso, podemos criar especializações dessa classe, e em cada uma delas eu coloco o que atende aquele perfil.

Vamos continuar o exemplo que eu usei no artigo anterior, onde nós temos 2 tipos de User, o Employee e o Contractor.

Você pode ver aqui que eu criei 2 Repositories, um para cada tipo de usuário, e em ambas eu criei o método save() executando exatamente a mesma coisa. (no final do artigo eu expliquei que usei dessa maneira para facilitar o entendimento da separação de responsabilidades)

Então, vamos melhorar esse código.

class BaseRepository
{
    protected $obj;

    protected function __construct(object $obj)
    {
        $this->obj = $obj;
    }

    public function all(): object
    {
        return $this->obj->all();
    }

    public function find(int $id): object
    {
        return $this->obj->find($id);
    }

    public function findByColumn(string $column, $value): object
    {
        return $this->obj->where($column, $value)->get();
    }

    public function save(array $attributes): bool
    {
        return $this->obj->insert($attributes);
    }

    public function update(int $id, array $attributes): bool
    {
        return $this->obj->find($id)->update($attributes);
    }
}

Criei uma classe BaseRepository que como o próprio nome diz, serve como base para classes especialistas, contendo os métodos que são genéricos a todas as classes que irão estendê-la.

Vamos criar as classes filhas (ou especialistas).

class EmployeeRepository extends BaseRepository
{
    protected $employee;

    public function __construct(Employee $employee)
    {
        parent::__construct($employee);
    }
}

class ContractorRepository extends BaseRepository
{
    protected $contractor;

    public function __construct(Contractor $contractor)
    {
        parent::__construct($contractor);
    }
}

Se as classes EmployeeRepository e ContractorRepository não tiverem nenhuma especificidade, basta criá-las apenas com o construtor chamando a classe pai que todos os métodos irão funcionar. Na camada de Service quando formos fazer a injeção de dependência, injetamos a classe especialista.

Mas utilizando alguns princípios da orientação a objetos, vamos fazer algo específico para cada uma delas.

class EmployeeRepository extends BaseRepository
{
    protected $employee;

    public function __construct(Employee $employee)
    {
        parent::__construct($employee);
    }

    public function all(): object
    {
        $except = [3,17,22];
        return $this->employee->whereNotIn('id', $except)->get();
    }
}

class ContractorRepository extends BaseRepository
{
    protected $contractor;

    public function __construct(Contractor $contractor)
    {
        parent::__construct($contractor);
    }

    public function save(array $attributes, int $hoursPerWeek): bool
    {
        $attributes['hours_per_week'] = $hoursPerWeek;
        return $this->contractor->insert($attributes);
    }
}

Na classe EmployeeRepository eu fiz uma sobreposição do método all() onde quase tudo é igual à classe pai (nome, parâmetros, retorno) exceto a implementação.

Na classe ContractorRepository eu fiz uma sobrecarga do método save() onde o nome do método é o mesmo da classe pai, mas a quantidade e/ou tipo dos parâmetros é diferente.

Dessa forma consegui deixar ambas as classes mais especializadas sem interferir na classe pai, ou perder suas características.

Espero que tenha gostado. No próximo artigo da série vou falar sobre o Liskov Substitution Principle (Princípio da Substituição de Liskov)

Liskov Substitution Principle

(Princípio da Substituição de Liskov)

Uma classe derivada deve ser substituída por sua classe base.

Este princípio tem este nome porque foi introduzido por Barbara Liskov em sua apresentação "Data Abstraction" em 1987. Alguns anos mais tarde, ela publicou um artigo, junto com Jeanette Wing, onde elas definiram o princípio como:

Seja Φ(x) uma provável propriedade sobre objetos x do tipo T. Então Φ(y) deve ser verdadeira para objetos y do tipo S, em que S é um subtipo de T.

Uma definição um tanto quanto complexa, não é mesmo? Normal, quando se trata de um artigo científico. Vamos simplificar e ver como isso se aplica na prática.

Continuando o nosso exemplo de Employees e Contractors, no primeiro artigo dessa série eu criei 2 repositories para ambos os perfis de usuário, e os injetei como dependência no 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);
        }
    }
}

No segundo artigo da série, eu criei um arquivo BaseRepository que seria a classe pai de EmployeeRepository e ContractorRepository.

É muito simples aplicar a substituição de Liskov.

Primeiro vamos criar um repository intermediário entre a classe pai e as classes filhas.

class BaseRepository
{
    // seu código
}

class UserRepository extends BaseRepository
{
    // seu código
}

class EmployeeRepository extends UserRepository
{
    // seu código
}

class ContractorRepository extends UserRepository
{
    // seu código
}

Dessa forma, a BaseRepository continua sendo "base" para outros repositories também, e a UserRepository vira a classe pai das outras duas.

Agora vamos fazer uma pequena alteração no classe UserService.

class UserService
{
    protected $employeeRepository;
    protected $contractorRepository;

    public function __construct(
        EmployeeRepository $employeeRepository,
        ContractorRepository $contractorRepository
    )
    {
        $this->userRepository = $userRepository;
        $this->contractorRepository = $contractorRepository;
    }

    public function checkUserType(Request $request)
    {
        $attributes = $request->all();
        if($attributes['type'] === 'employee') {
            $this->register($attributes, $this->employeeRepository);
        }

        if($attributes['type'] === 'contractor') {
            $this->register($attributes, $this->contractorRepository);
        }
    }

    public function register(array $attributes, UserRepository $userRepository)
    {
        $userRepository->save($attributes);
    }
}

Renomeei o método register() para checkUserType() onde continua sendo feita a validação do tipo de usuário. Mas, ao invés de chamar diretamente o repository, agora chama outro método dentro desta mesma classe, que por sua vez, agora se chama register() e recebe como parâmetro o array $attributes e uma instância de UserRepository.

Independentemente de qual dos repositories for passado como parâmetro, ambos são do tipo UserRepository e quando for chamado o método save(), irá chamar de acordo com o que foi passado.

Dessa forma, nós temos a chamada para o repository de uma forma mais centralizada, e mantendo o mesmo tipo de objeto na passagem do parâmetro.

Espero que tenha ficado claro para você. Caso contrário, comente aí sua dúvida.

Interface Segregation Principle

(Princípio da Segregação de Interface)

Uma classe não deve ser forçada a implementar interfaces que não irá utilizar.

Abstração é o coração da orientação a objetos. Em PHP, nós conseguimos alcançar um nível de abstração utilizando interfaces e classes abstratas.

Mas, antes, vamos dar um passo atrás: o que é interface?

Interface é um conjuntos de abstrações que todas as classes que a implementam deve seguir. Cada método adicionado na interface é uma assinatura, e a partir do momento que uma classe a implementa, esta fica obrigada a adicionar todas as assinaturas previamente criadas na interface.

Partindo deste princípio, parece óbvio que uma classe não deva ser forçada a implementar algo que não irá usar. Mas nesse artigo, você verá que este princípio é muito fácil de violar.

Seguindo o nosso exemplo de Employees e Contractors, até o terceiro post da série eu criei uma classe chamada UserService que injeta os repositories para os dois tipos de usuário.

Vamos fazer uma pequena alteração, a partir de agora será um service para cada tipo de usuário.

class EmployeeService
{
  protected $employeeRepository;

  public function __construct(EmployeeRepository $employeeRepository)
  {
    $this->userRepository = $userRepository;
  }
}

class ContractorService
{
  protected $contractorRepository;

  public function __construct(ContractorRepository $contractorRepository)
  {
    $this->contractorRepository = $contractorRepository;
  }
}

Dessa maneira eu terei duas classes que irão implementar a mesma interface UserInterface.

interface UserInterface
{
  public function register(array $attributes, UserRepository $userRepository);
  public function calcWorkedHours(array $hours);
}

Interface criada com a assinatura do método register (que foi criado nos posts anteriores), e agora adicionei o método calcWorkedHours que será um método útil para o usuário do tipo Contractor onde as horas dele serão calculadas para gerar o pagamento no fim do mês.

Vamos implementar a interface nos dois services.

class EmployeeService implements UserInterface
{
  public function register(array $attributes, UserRepository $userRepository)
  {
    // implementação do código
  }
}

class ContractorService implements UserInterface
{
  public function register(array $attributes, UserRepository $userRepository)
  {
    // implementação do código
  }

  public function calcWorkedHours(array $hours)
  {
    // implementação do código
  }
}

Dessa maneira nós vamos ter um erro na classe EmployeeService pois ela implementa a UserInterface mas não usa o método calcWorkedHours.

Uma maneira errada de resolver isso seria implementar o método, mas sem nenhuma implementação dentro do escopo.

A maneira correta seria criar duas interfaces e implementá-las onde for necessário.

interface UserInterface
{
  public function register(array $attributes, UserRepository $userRepository);
}

interface ContractorInterface
{
  public function calcWorkedHours(array $hours);
}

E após criar as duas interfaces, as classes já poderiam escolher quem implementar.

class EmployeeService implements UserInterface
{
  public function register(array $attributes, UserRepository $userRepository)
  {
    // implementação do código
  }
}

class ContractorService implements UserInterface, ContractorInterface
{
  public function register(array $attributes, UserRepository $userRepository)
  {
    // implementação do código
  }

  public function calcWorkedHours(array $hours)
  {
    // implementação do código
  }
}

Dessa maneira, não obrigamos nenhuma classe a implementar algo que não irá utilizar. Sendo assim, não violamos o princípio da segregação de interface. :)

Quando tentamos entender apenas a teoria dos princípios, parecem bem mais complicados do que realmente são.

Dependency Inversion Principle

(Princípio da Inversão de Dependência)

Entidades devem depender de abstrações e não de algo concreto. Isso significa que módulos de alto nível não podem depender de módulos de baixo nível, e sim de abstrações.

Como eu citei na parte 04 "abstração é o coração da orientação a objetos. Em PHP, nós conseguimos alcançar um nível de abstração utilizando interfaces e classes abstratas". Ou seja, assim como na parte 04 nós vamos aplicar o nosso exemplo de Employees e Contractor usando interfaces.

Quando trabalhamos com arquitetura em camadas (que no nosso caso é as camadas ControllerServiceRepository e Model) o ideal é que possamos seguir o fluxo da requisição a partir do Controller, e que as camadas mais externas não saibam nada do que acontece nas camadas mais internas.

Por exemplo:

Controller não pode depender diretamente do Service, assim como o Service não pode depender da camada de Repository.

Mas por quê?

Pois dessa forma essas camadas estariam dependendo de implementações concretas. Ou seja, se eu mudar alguma coisa no meu Service, pode impactar diretamente no Controller e acabar "quebrando" a aplicação. Isso aumenta o acoplamento entre as classes, o que é uma má prática.

E como resolver isso?

Com a inversão de dependência!

Na parte 04, criei 2 interfaces com o objetivo de deixá-las segregadas, cada uma com suas particularidades.

interface UserInterface
{
  public function register(array $attributes, UserRepository $userRepository);
}

interface ContractorInterface
{
  public function calcWorkedHours(array $hours);
}

Para poder fazer a inversão de dependência, eu preciso injetar a interface do Service no Controller. Mas como fazer isso se temos duas interfaces?

A resposta é simples: herança!

Eu vou criar uma interface, e as duas interfaces criadas anteriormente (UserInterface e ContractorInterface) irão estendê-la.

interface ServiceInterface
{
}

interface UserInterface extends ServiceInterface
{
  public function register(array $attributes, UserRepository $userRepository);
}

interface ContractorInterface extends ServiceInterface
{
  public function calcWorkedHours(array $hours);
}

Dessa forma, conseguimos concentrar toda essa camada de abstração em um único ponto.

Agora, vamos ao Controller e vamos injetar essa interface.

class UserRegistrationController extends Controller
{
    protected $userService;

    public function __construct(ServiceInterface $userService)
    {
        $this->userService = $userService;
    }

    // implementação do código
}

Percebam que mesmo o nosso atributo seja nomeado como $userService, ele é do tipo ServiceInterface. E dessa forma evitamos um forte acoplamento entre as camadas concretas, deixando todo serviço a cargo da abstração, que no nosso caso é a ServiceInterface.

Fica aqui o desafio: tente fazer o mesmo processo entre as camadas de Service e Repository. :)

 

ESTE POSTE FOI FEITO NO DEV.TO pelo "Lucas Cavalcante"

Fonte:

- https://dev.to/lucascavalcante/principios-solid-o-que-sao-e-como-aplica-los-no-php-laravel-parte-01-responsabilidade-unica-3mjj

Vou deixa um video aqui abaixo sobre solid.

 


1

Share

Donate to Site


About Author

Renato

Developer

Add a Comment
Comments 0 Comments

No comments yet! Be the first to comment

Blog Search


Categories

OUTROS (15) Variados (109) PHP (127) Laravel (154) Black Hat (3) front-end (28) linux (110) postgresql (37) Docker (25) rest (5) soap (1) webservice (6) October (1) CMS (2) node (7) backend (13) ubuntu (54) devops (25) nodejs (5) npm (2) nvm (1) git (8) firefox (1) react (6) reactnative (5) collections (1) javascript (6) reactjs (7) yarn (0) adb (1) Solid (2) blade (3) models (1) controllers (0) log (0) html (2) hardware (3) aws (14) Transcribe (2) transcription (1) google (4) ibm (1) nuance (1) PHP Swoole (5) mysql (28) macox (4) flutter (1) symfony (1) cor (1) colors (2) homeOffice (2) jobs (3) imagick (2) ec2 (1) sw (1) websocket (1) markdown (1) ckeditor (1) tecnologia (14) faceapp (1) eloquent (14) query (3) sql (36) ddd (3) nginx (9) apache (4) certbot (1) lets-encrypt (3) debian (11) liquid (1) magento (2) ruby (1) LETSENCRYPT (1) Fibonacci (1) wine (1) transaction (1) pendrive (1) boot (1) usb (1) prf (1) policia (2) federal (1) lucena (1) mongodb (4) paypal (1) payment (1) zend (1) vim (3) ciencia (6) js (1) nosql (1) java (1) JasperReports (1) phpjasper (1) covid19 (1) saude (1) athena (1) cinnamon (1) phpunit (2) binaural (1) mysqli (3) database (37) windows (6) vala (1) json (2) oracle (1) mariadb (4) dev (12) webdev (24) s3 (4) storage (1) kitematic (1) gnome (2) web (2) intel (3) piada (1) cron (2) dba (13) lumen (1) ffmpeg (2) android (2) aplicativo (1) fedora (2) shell (4) bash (3) script (3) lider (1) htm (1) csv (1) dropbox (1) db (3) combustivel (2) haru (1) presenter (1) gasolina (1) MeioAmbiente (1) Grunt (1) biologia (1) programming (21) performance (3) brain (1) smartphones (1) telefonia (1) privacidade (1) opensource (3) microg (1) iode (1) ssh (3) zsh (2) terminal (3) dracula (1) spaceship (1) mac (2) idiomas (1) laptop (2) developer (36) api (4) data (1) matematica (1) seguranca (2) 100DaysOfCode (9) hotfix (1) documentation (1) laravelphp (9) RabbitMQ (1) Elasticsearch (1) redis (2) Raspberry (4) Padrao de design (4) JQuery (1) angularjs (4) Dicas (36) Kubernetes (2) vscode (2) backup (1) angular (3) servers (2) pipelines (1) AppSec (1) DevSecOps (4) rust (1) RustLang (1) Mozilla (1) algoritimo (1) sqlite (1) Passport (1) jwt (4) security (2) translate (1) kube (1) iot (1) politica (2) bolsonaro (1) flow (1) podcast (1) Brasil (1) containers (2) traefik (1) networking (1) host (1) POO (2) microservices (2) bug (1) cqrs (1) arquitetura (2) Architecture (3) sail (3) militar (1) artigo (1) economia (1) forcas armadas (1) ffaa (1) autenticacao (1) autorizacao (2) authentication (4) authorization (2) NoCookies (1) wsl (4) memcached (1) macos (2) unix (2) kali-linux (1) linux-tools (5) apple (1) noticias (2) composer (1) rancher (1) k8s (1) escopos (1) orm (1) jenkins (4) github (5) gitlab (3) queue (1) Passwordless (1) sonarqube (1) phpswoole (1) laraveloctane (1) Swoole (1) Swoole (1) octane (1) Structurizr (1) Diagramas (1) c4 (1) c4-models (1) compactar (1) compression (1) messaging (1) restfull (1) eventdrive (1) services (1) http (1) Monolith (1) microservice (1) historia (1) educacao (1) cavalotroia (1) OOD (0) odd (1) chatgpt (1) openai (3) vicuna (1) llama (1) gpt (1) transformers (1) pytorch (1) tensorflow (1) akitando (1) ia (1) nvidia (1) agi (1) guard (1) multiple_authen (2) rpi (1) auth (1) auth (1) auth (1) auth (1) Oh My Zsh (1) Manjaro (1) BigLinux (2) ArchLinux (1) Migration (1) Error (1) Monitor (1) Filament (1) LaravelFilament (1) replication (1) phpfpm (1) cache (1) vpn (1) l2tp (1) zorin-os (1) optimization (1) scheduling (1) monitoring (1) linkedin (1) community (1) inteligencia-artificial (2) wsl2 (1) maps (1) API_KEY_GOOGLE_MAPS (1)

New Articles



Get Latest Updates by Email