R e f a t o r a n d o

A p l i c a ç õ e s

n a   P r á t i c a

por Vinícius Campitelli

Sobre mim

Sobre mim

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

Slides

Slides

Quando refatorar?

Quando refatorar?

  • Quando é a terceira vez que mexemos naquele código
  • Antes de criar ou alterar uma funcionalidade grande ou complexa
  • Quando o código é difícil de entender
  • Para pagar débito técnico ou corrigir algo mal feito
  • Quando há algum bug não identificado
  • Para criar testes

Quando refatorar?

You have to refactor when you run into ugly code — but excellent code needs plenty of refactoring too. Martin Fowler

Quando não refatorar?

  • Quando você não tem experiência o suficiente
  • Quando você não conhece o código (ou o negócio) o suficiente
  • Se você não irá precisar modificar aquele código
  • Quando é mais fácil reescrever do que refatorar

Conceitos

Conceitos

Any fool can write code that a computer can understand. Good programmers write code that humans can understand. Martin Fowler
Refactoring: Improving the Design of Existing Code

Conceitos

Coesão

Relacionamento que os membros de um módulo possuem. Códigos coesos são aqueles de relação forte, onde seus membros estão intimamente ligados e estão ali por um objetivo comum. Membros que não são absolutamente necessários para aquele módulo não devem estar presentes em códigos coesos.
StackOverflow: O que são os conceitos de coesão e acoplamento?

Conceitos

Acoplamento

Relacionamento entre os módulos. Indica quanto um módulo depende de outro para funcionar. Códigos desacoplados são aqueles de relação fraca, e não dependem de outros para fazer sua funcionalidade básica. É sempre desejável um baixo nível de acoplamento.
StackOverflow: O que são os conceitos de coesão e acoplamento?

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

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

  • Single Responsibility Principle
  • Open-Closed Principle
  • Liskov Substitution Principle
  • Interface Segregation Principle
  • 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

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 e análise estática

Instale ferramentas de análise estática e linting, defina as regras e execute ferramentas que corrigem automaticamente o estilo

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

Tipar argumentos e retornos

Especifique os tipos dos argumentos e os retornos dos métodos


                                    public function create(
                                        $request,
                                        $alias,
                                        $id
                                    ) {}
                                

                                    export default function create(
                                        request: any,
                                        alias: any,
                                        id: any
                                    ): any {}
                                

Códigos pré-refatoração


                                    public function create(
                                        Request $request,
                                        string $alias,
                                        int $id
                                    ): Response {}
                                

                                    export default function create(
                                        request: Request,
                                        alias: string,
                                        id: number
                                    ): Entity {}
                                

Códigos pós-refatoração

Isso ajudará inclusive ao criar testes de unidade (já que ferramentas como o PHPUnit conseguem criar mocks automaticamente) e também nas ferramentas de análise estática (como o larastan)

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

Não dependa de strings

Use constantes ao invés de strings para identificadores e outros textos pré-estabelecidos, diminuindo a possibilidade de errar e facilitando possíveis refatorações no futuro


                                    return match ($method) {
                                        'pagarme' => /* ... */,
                                        'pagseguro'=> /* ... */
                                    };
                                

                                    if (stepBefore === 'steps.account.username') {
                                        return redirectTo('steps.merchant.config');
                                    }
                                

Códigos pré-refatoração


                                    return match ($method) {
                                        PagarmeGateway::IDENTIFIER => /* ... */,
                                        PagSeguroGateway::IDENTIFIER => /* ... */
                                    };
                                

                                    enum STEPS {
                                        ACCOUNT_USERNAME = 'steps.account.username',
                                        MERCHANT_CONFIG = 'steps.merchant.config',
                                    }
                                    if (stepBefore === STEPS.ACCOUNT_USERNAME) {
                                        return redirectTo(STEPS.MERCHANT_CONFIG);
                                    }
                                

Códigos pós-refatoração

Refatoração

Separe informações de sistemas externos

Ao invés de acessar os valores brutos das respostas de sistemas externos, crie abstrações, como os DTOs (Data Transfer Objects), diminuindo a quantidade de mudanças necessárias em seu código quando houver alterações externas


                                    {# produtos/externos/listar.phtml #}
                                    {% for product in products %}
                                        <li>
                                            <p>{{ product.titulo }}</p>
                                            R$ {{ product.preco|number_format(2, '.', ',') }}
                                        </li>
                                    {% endfor %}

                                    {# produtos/externos/visualizar.phtml #}
                                    <div>
                                        <p>{{ product.titulo }}</p>
                                        R$ {{ product.preco|number_format(2, '.', ',') }}
                                    </div>
                                

Código pré-refatoração

E se o sistema externo mudar a resposta?

Refatoração

Separe informações de sistemas externos

Ao invés de acessar os valores brutos das respostas de sistemas externos, crie abstrações, como os DTOs (Data Transfer Objects), diminuindo a quantidade de mudanças necessárias em seu código quando houver alterações externas


                                    {# produtos/externos/listar.phtml #}
                                    {% for product in products %}
                                        <li>
                                            <p>{{ product.title }}</p>
                                            R$ {{ (product.price / 100)|number_format(2, '.', ',') }}
                                        </li>
                                    {% endfor %}

                                    {# produtos/externos/visualizar.phtml #}
                                    <div>
                                        <p>{{ product.title }}</p>
                                        R$ {{ (product.price / 100)|number_format(2, '.', ',') }}
                                    </div>
                                

Código pré-refatoração com mudanças no sistema externo refletidas

Refatoração

Separe informações de sistemas externos

Ao invés de acessar os valores brutos das respostas de sistemas externos, crie abstrações, como os DTOs (Data Transfer Objects), diminuindo a quantidade de mudanças necessárias em seu código quando houver alterações externas


                                    {# produtos/externos/listar.phtml #}
                                    {% for productDto in products %}
                                        <li>
                                            <p>{{ productDto.title }}</p>
                                            R$ {{ productDto.price|number_format(2, '.', ',') }}
                                        </li>
                                    {% endfor %}

                                    {# produtos/externos/visualizar.phtml #}
                                    <div>
                                        <p>{{ productDto.title }}</p>
                                        R$ {{ productDto.price|number_format(2, '.', ',') }}
                                    </div>
                                

Código pós-refatoração


                                    class ProductDto
                                    {
                                        public static function build(
                                            stdClass $data
                                        ) {
                                            return new ProductDto(
                                                title: $data->titulo,
                                                price: $data->preco,
                                            );
                                        }
                                    }
                                

DTO para isolar o sistema externo

Refatoração

Separe informações de sistemas externos

Ao invés de acessar os valores brutos das respostas de sistemas externos, crie abstrações, como os DTOs (Data Transfer Objects), diminuindo a quantidade de mudanças necessárias em seu código quando houver alterações externas


                                    {# produtos/externos/listar.phtml #}
                                    {% for productDto in products %}
                                        <li>
                                            <p>{{ productDto.title }}</p>
                                            R$ {{ productDto.price|number_format(2, '.', ',') }}
                                        </li>
                                    {% endfor %}

                                    {# produtos/externos/visualizar.phtml #}
                                    <div>
                                        <p>{{ productDto.title }}</p>
                                        R$ {{ productDto.price|number_format(2, '.', ',') }}
                                    </div>
                                

Código pós-refatoração resiliente às mudanças no sistema externo


                                    class ProductDto
                                    {
                                        public static function build(
                                            stdClass $data
                                        ) {
                                            return new ProductDto(
                                                title: $data->title,
                                                price: $data->price / 100,
                                            );
                                        }
                                    }
                                

As mudanças ocorrem apenas no DTO

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

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

Refatoração

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

Refatoração

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

                                    $useCase = new CreateProductUseCase();
                                    return $useCase->handle(
                                        $name, $price, $description, $idCategory
                                    );
                                }
                            }
                            class CreateProductUseCase {
                                public function handle(string $name, float $price, string $description, int $idCategory) {
                                    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 pós-refatoração

Refatoração

Models e banco de dados

Separe para facilitar a manutenção e criação de mocks para 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 */ }
                                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

Models e banco de dados

Separe para facilitar a manutenção e criação de mocks para 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
  • Dedique tempo pensando no 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

Referências

Referências

Treinamentos in company

Workshops
Gostou? Então conheça meus treinamentos corporativos e sob demanda sobre Desenvolvimento, Segurança da Informação, DevOps, Arquitetura de Sistemas e diversos outros assuntos em viniciuscampitelli.com

Obrigado!