Refatorando Aplicações
na Prática

Vinícius Campitelli

Sobre

Sobre

Sobre mim

Vinícius Campitelli
Vinícius
Campitelli
  • Bacharel em Ciência da Computação pela UFSCar
  • Desenvolvedor há mais de 15 anos
  • Membro do PHPSP
  • Entusiasta em cibersegurança
  • Consultor de TI e instrutor de treinamentos

Preparação

Preparação

Slides

Preparação

Mãos na massa

  1. Certifique-se que você tenha o Docker e o Docker Compose instalados
  2. Clone o repositório vcampitelli/slides-refatorando-aplicacoes do GitHub
    
                                        $ git clone git@github.com/vcampitelli/slides-refatorando-aplicacoes.git
                                    
  3. Inicialize os containers da aplicação
    
                                        $ cd demo/app-antiga
                                        $ docker compose up -d
                                    
  4. Acesse localhost:8080 em seu navegador

Conceitos

Conceitos

SOLID

Princípios que foram apresentados originalmente por Robert Martin em 2000 em seu artigo Design Principles and Design Patterns, em que descreve conceitos para evitar que um software apodreça

  1. Single Responsibility Principle
  2. Open-Closed Principle
  3. Liskov Substitution Principle
  4. Interface Segregation Principle
  5. Dependency Inversion Principle
Curiosidade: no artigo não há o acrônimo SOLID, pois outros princípios são explicados, em outra ordem e até com nomes diferentes. Foi em 2004 que Michael Feathers reordenou alguns desses conceitos e sugeriu o termo.

Conceitos

Object Calisthenics

Conjunto de 9 regras inventadas por Jeff Bay no livro The ThoughtWorks Anthology de 2008

  1. Only One Level Of Indentation Per Method
  2. Don’t Use The ELSE Keyword
  3. Wrap All Primitives And Strings
  4. First Class Collections
  5. One Dot Per Line
  6. Don't Abbreviate
  7. Keep All Entities Small
  8. No Classes With More Than Two Instance Variables
  9. No Getters/Setters/Properties

Conceitos

Code Smells

Qualquer característica do código fonte de um programa de computador que indique a possibilidade de um problema mais profundo no sistema
Wikipedia: Code Smell

Conceitos

Clean Architecture

Abordagem para produzir sistemas testáveis e independentes de frameworks, UI, banco de dados, ou qualquer outro agente externo

Entities Use Cases Interface adapters Framework e drivers

Entities

Encapsulam regras de negócios de toda a empresa, que podem ser usadas por diferentes sistemas — geralmente são models e validações de seus atributos

Use Cases

Definem regras de negócios específicas da aplicação, que normalmente são um conjunto de Use Cases ou ações executadas sobre as Entities

Interface Adapters

São um conjunto de adaptadores que convertem dados de agentes externos para os Use Cases e Entities

Framework e Drivers

São serviços externos aos quais os Interface Adapters se conectam
A Brief Introduction to Architecture Design Using the Clean Architecture Approach

Refatoração

Refatoração

Disclaimer

  1. Faremos refatorações incrementais, então códigos nos slides intermediários ainda não estão prontos e apresentam problemas
  2. Mesmo no código final pode ser ainda possível identificar melhorias, mas temos um tempo limite da apresentação, logicamente
  3. Esses passos não são verdade absoluta: é algo que funcionou para mim e por isso quis compartilhar, mas existem dezenas de outras formas de refatorar aplicações, e cada pessoa tem sua própria abordagem

Refatoração

Linting

Instale ferramentas de análise estática e linting, defina as regras e executa a ferramenta que corrige automaticamente o estilo do código

Dica: configure o plugin da ferramenta na sua IDE para garantir que está escrevendo código de acordo com o formato configurado e adicione uma etapa no CI para rodar a checagem

Refatoração

Testes - Parte 1

Crie simples testes de contrato de suas controllers para ter mais confiança que a refatoração não irá quebrar suas APIs


                            public function testProductListAction() {
                                $body = $this->makeGetRequest('/product/list')->body();
                                $this->assertArrayHasKey('status', $body);
                                $this->assertTrue($body['status']);
                                $this->assertArrayHasKey('products', $body);
                                $this->assertArrayHasKey('name', $body['products'][0]);
                                $this->assertArrayHasKey('price', $body['products'][0]);
                                $this->assertArrayHasKey('description', $body['products'][0]);
                            }
                        
Atenção: não é recomendado testar controllers, então depois iremos refatorar esses testes também!

Refatoração

Testes - Parte 1

Adicione também uma etapa no CI para rodar esses testes e barrar Pull Requests que não passem


                                    on:
                                      pull_request:

                                    steps:
                                      - uses: actions@checkout
                                      - uses: shivammathur/setup-php@v2
                                      - run: composer test
                                

Refatoração

Uma ação por controller

Ao invés de ter grandes controllers com vários endpoints, sugiro dividi-los para que cada endpoint tenha sua própria classe (ou função, no caso de JavaScript)


                                    class ProductController {
                                        public function create(Request $req) {}

                                        public function list(Request $req) {}

                                        public function update(Request $req) {}

                                        public function delete(Request $req) {}
                                    }
                                

Código pré-refatoração


                                    class CreateProductAction {
                                        public function handle(Request $req) {}
                                    }
                                    class ListProductAction {
                                        public function handle(Request $req) {}
                                    }
                                    class UpdateProductAction {
                                        public function handle(Request $req) {}
                                    }
                                    class DeleteProductAction {
                                        public function handle(Request $req) {}
                                    }
                                

Código pós-refatoração

Refatoração

Isolamento de responsabilidades

Mantenha suas controllers leves

A controller deve ser apenas uma forma de traduzir o protocolo de entrada (HTTP) para a execução das regras de negócio do seu sistema

Afinal, pode haver outros tipos de entrada (como ferramentas CLI ou workers) que precisem executar as mesmas ações

Então, extraia as regras de negócio de suas controllers para novos serviços

Refatoração

Isolamento de responsabilidades

Mantenha suas controllers leves


                                    class ProductCreateAction {
                                        public function handle(Request $request) {
                                            $name = (string) $request->get('name');
                                            $price = (float) $request->get('price');
                                            $description = (string) $request->get('description');
                                            $idCategory = (int) $request->get('id_category');

                                            if (empty($name)) { /* ... */ }
                                            if ((empty($price)) || ($price <= 0)) { /* ... */ }
                                            // validação dos outros campos...

                                            if (!user()->authorized('PRODUCT_CREATE')) {
                                                return response(403, 'Você não tem permissão a esse recurso');
                                            }

                                            $category = $this->categoryRepository->findById($idCategory);
                                            if ((!$category) || (!$category->active)) {
                                                return response(404, 'Categoria não encontrada');
                                            }

                                            $product = new Product($name, $description, $price, $category);
                                            $this->productRepository->save($product);

                                            return response(201);
                                        }
                                    }
                                

Código pré-refatoração


                                    class ProductCreateAction {
                                        public function handle(Request $request) {
                                            $name = (string) $request->get('name');
                                            $price = (float) $request->get('price');
                                            $description = (string) $request->get('description');
                                            $idCategory = (int) $request->get('id_category');

                                            $useCase = new CreateProductUseCase();
                                            return $useCase->handle(
                                                new CreateProductUseCaseInput($name, $price, $description, $idCategory)
                                            );
                                        }
                                    }
                                    class CreateProductUseCaseInput {
                                        public function __construct(string $name, float $price, string $description, int $idCategory) {
                                            if (empty($input->name)) { /* ... */ }
                                            if ((empty($input->price)) || ($input->price <= 0)) { /* ... */ }
                                        }
                                    }
                                    class CreateProductUseCase {
                                        public function handle(CreateProductUseCaseInput $input) {
                                            // validação dos outros campos...

                                            if (!user()->authorized('PRODUCT_CREATE')) {
                                                return response(403, 'Você não tem permissão a esse recurso');
                                            }

                                            $category = $this->categoryRepository->findById($input->idCategory);
                                            if ((!$category) || (!$category->active)) {
                                                return response(404, 'Categoria não encontrada');
                                            }

                                            $product = new Product(
                                                $input->name,
                                                $input->description,
                                                $input->price,
                                                $category
                                            );
                                            $this->productRepository->save($product);

                                            return response(201);
                                        }
                                    }
                                

Código pós-refatoração

Refatoração

Isolamento de responsabilidades

Regras de negócio

Levante regras de negócio espalhadas entre controllers, comandos, workers, models e centralize-as no serviço criado anteriormente

Caso encontre discrepâncias, será preciso determinar qual regra é correta (às vezes, até em reuniões com o cliente)

Uso de Use Cases

Refatoração

Isolamento de responsabilidades

Models e banco de dados

É comum termos models que tenham tanto regras de negócio quanto interações com o banco de dados


                            class Product {
                                public function setName(string $name) { /* Regras de negócio do nome */ }
                                public function setPrice(float $price) { /* Regras de negócio do preço */ }
                                public static function findAll() {
                                    $idStatus = [Status::ACTIVE];
                                    if (user()->permission('PRODUCT_SEE_HIDDEN')) {
                                        $idStatus[] = Status::HIDDEN;
                                    }

                                    return Db::select()
                                        ->from('products')
                                        ->where('id_status IN (?)', $idStatus)
                                        ->build()
                                        ->fetchAll();
                                }
                            }
                        

Código pré-refatoração

Refatoração

Isolamento de responsabilidades

Models e banco de dados

É recomendado separar essas ações para facilitar a manutenção e os mocks na hora de testar


                            class Product {
                                public function setName(string $name) { /* Regras de negócio do nome */ }
                                public function setPrice(float $price) { /* Regras de negócio do preço */ }
                            }
                            class ProductRepository {
                                public function __construct(private DatabaseInterface $db) {}
                                public function findAll() {
                                    $idStatus = [Status::ACTIVE];
                                    if (user()->permission('PRODUCT_SEE_HIDDEN')) {
                                        $idStatus[] = Status::HIDDEN;
                                    }
                                    $condition = new Condition();
                                    $condition->in('id_status', $idStatus);
                                    return $this->db->find('products', $condition);
                                }
                            }
                            class PdoDatabase implements DatabaseInterface {}
                        

Código pós-refatoração

Refatoração

Princípio Tell, don't ask

Getters e setters acabam espalhando regras de negócio e responsabilidades para outras entidades do sistema


                                    class Time {
                                        public function getPontos(): int {
                                            return $this->pontos;
                                        }
                                        public function setPontos(
                                            int $pontos
                                        ): void {
                                            $this->pontos = $pontos;
                                        }
                                    }

                                    $time->setPontos($time->getPontos() + 3);
                                

Código pré-refatoração


                                    class Time {
                                        public function vence(): void {
                                            $this->pontos += 3;
                                        }
                                        public function empata(): void {
                                            $this->pontos += 1;
                                        }
                                    }



                                    $time->vence();
                                

Código pós-refatoração

Refatoração

Exceptions

Crie exceptions customizadas para seus erros ao invés de usar as padrões da linguagem para facilitar o tratamento


                                    try {
                                        // ...
                                    } catch (Exception $e) {
                                        return response(
                                            $e->getCode() ?: 500,
                                            $e->getMessage()
                                        );
                                    }
                                

Código pré-refatoração


                                    try {
                                        // ...
                                    } catch (ProductNotFoundException|ProductInactiveException $e) {
                                        return response(404, 'Produto não encontrado');
                                    } catch (SystemException $e) {
                                        $this->logger->error($e);
                                        return response(500, 'Um erro desconhecido ocorreu');
                                    } catch (UserException $e) {
                                        return response(
                                            $e->getCode() ?: 500,
                                            $e->getMessage()
                                        );
                                    }
                                

Código pós-refatoração

Refatoração

Injeção de dependências

As dependências de sua classe devem ser injetadas no construtor para deixar explícito seus vínculos


                                    class CreatePaymentUseCase {
                                      public function handle() {
                                        $paymentRepository = app(
                                          MySqlPaymentRepository::class
                                        );
                                        $orderRepository = app(
                                          MongoOrderRepository::class
                                        );
                                        // ...
                                        // ...
                                      }
                                    }
                                

Código pré-refatoração


                                    class CreatePaymentUseCase {
                                      public function __construct(
                                        private MySqlPaymentRepository $paymentRepository,
                                        private MongoOrderRepository $orderRepository,
                                      ) {
                                      }
                                      public function handle() {
                                        $this->paymentRepository->method();
                                        $this->orderRepository->method();
                                        // ...
                                      }
                                    }
                                

Código pós-refatoração

Refatoração

Dependa de abstrações

Seu código não deve depender de implementações concretas, mas sim respeitar contratos impostos por interfaces


                            public function __construct(
                              private PdoPaymentRepository $paymentRepository,
                              private MongoOrderRepository $orderRepository,
                              private PHPMailer $mailer,
                            ) {
                            }

                            public function handle(PaymentUseCaseInput $input) {
                              $payment = $this->paymentRepository->findById($input->paymentId);
                              $order = $this->orderRepository->findByPayment($payment);
                              $this->mailer->send($to, $subject, $body);
                              // ...
                            }
                        

Código pré-refatoração

Refatoração

Dependa de abstrações

Seu código não deve depender de implementações concretas, mas sim respeitar contratos impostos por interfaces


                            public function __construct(
                              private PaymentRepositoryInterface $paymentRepository,
                              private OrderRepositoryInterface $orderRepository,
                              private MailerInterface $mailer,
                            ) {
                            }

                            public function handle(PaymentUseCaseInput $input) {
                              $payment = $this->paymentRepository->findById($input->paymentId);
                              $order = $this->orderRepository->findByPayment($payment);
                              $this->mailer->send($to, $subject, $body);
                              // ...
                            }

                            interface PaymentRepositoryInterface { /* ... */ }
                            interface OrderRepositoryInterface { /* ... */ }
                            interface MailerInterface { /* ... */ }
                        

Código pós-refatoração

Refatoração

Testes - Parte 2

Refatore seus testes criados em Testes - Parte 1 para começar a testar os UseCases e models ao invés das controllers


                            public function testListActiveProductsUseCase() {
                                $products = [
                                    new Product(title: 'Produto Ativo 1', active: true),
                                    new Product(title: 'Produto Ativo 2', active: true),
                                    new Product(title: 'Produto Inativo', active: false),
                                ];

                                $mock = $this->createMock(ProductRepositoryInterface::class);
                                $mock->method('findAllActive')
                                    ->willReturn($products);

                                $useCase = new ListActiveProductsUseCase($mock);
                                $products = $useCase->handle();
                                $this->assertCount(2, $products, 'Retorna apenas 2 produtos ativos');
                            }
                        

Refatoração

Dicas gerais

  • Antes de refatorar algo, certifique-se que você possua bons testes para aquela funcionalidade
  • Utilize as funções das IDEs para refatoração, principalmente Extract Method
  • Dedique tempo pensando o quê — e quando — refatorar
  • Escolha bons nomes para variáveis e não abrevie
  • Não coloque Abstract, Interface ou Type nos nomes (polêmico)
  • Evite superclasses Helper, Utils ou afins

Mãos na massa

Mãos na massa

  • Certifique-se que você clonou o repositório e inicializou os containers, como descrito no slide de preparação
  • Abra a pasta demo/app-antiga em sua IDE e vamos começar a refatorar juntos!

Mãos na massa

Essas são as requisições que usaremos para validar as principais regras de negócio do sistema:


                            $ curl localhost:8080/products
                            $ curl localhost:8080/products/1
                            $ curl localhost:8080/products/10
                            $ curl -X POST localhost:8080/products
                            $ curl -X POST -d 'id_category=erro' localhost:8080/products
                            $ curl -X POST -d 'id_category=1' localhost:8080/products
                            $ curl -X POST -d 'id_category=1&name=A' localhost:8080/products
                            $ curl -X POST -d 'id_category=1&name=Meu+nome+de+produto+muito+grande' localhost:8080/products
                            $ curl -X POST -d 'id_category=1&name=Meu+produto' localhost:8080/products
                            $ curl -X POST -d 'id_category=1&name=Meu+produto&sku=TESTE' localhost:8080/products
                            $ curl -X POST -d 'id_category=1&name=Meu+produto&sku=TESTE00000' localhost:8080/products
                            $ curl -X POST -d 'id_category=1&name=Meu+produto&sku=TESTE00000&price=0' localhost:8080/products
                            $ curl -X POST -d 'id_category=1&name=Meu+produto&sku=TESTE00000&price=10' localhost:8080/products
                            $ curl -X POST \
                                -d 'products[0][id_category]=1&products[0][name]=Meu+produto+1&products[0][sku]=BULK000001&products[0][price]=10&products[1][id_category]=2&products[1][name]=Meu+produto+2&products[1][sku]=BULK000002&products[1][price]=20' \
                                localhost:8080/products
                        

Referências

Referências

Sites

Treinamentos in company

Workshops

Obrigado!