$ git clone git@github.com/vcampitelli/slides-refatorando-aplicacoes.git
$ cd demo/app-antiga
$ docker compose up -d
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
Conjunto de 9 regras inventadas por Jeff Bay no livro The ThoughtWorks Anthology de 2008
Qualquer característica do código fonte de um programa de computador que indique a possibilidade de um problema mais profundo no sistemaWikipedia: Code Smell
Abordagem para produzir sistemas testáveis e independentes de frameworks, UI, banco de dados, ou qualquer outro agente externo
■ 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 conectamInstale ferramentas de análise estática e linting, defina as regras e executa a ferramenta que corrige automaticamente o estilo do código
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]);
}
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
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) {}
}
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) {}
}
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
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);
}
}
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);
}
}
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)
É 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();
}
}
É 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 {}
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);
class Time {
public function vence(): void {
$this->pontos += 3;
}
public function empata(): void {
$this->pontos += 1;
}
}
$time->vence();
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()
);
}
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()
);
}
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
);
// ...
// ...
}
}
class CreatePaymentUseCase {
public function __construct(
private MySqlPaymentRepository $paymentRepository,
private MongoOrderRepository $orderRepository,
) {
}
public function handle() {
$this->paymentRepository->method();
$this->orderRepository->method();
// ...
}
}
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);
// ...
}
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 { /* ... */ }
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');
}
Abstract
, Interface
ou Type
nos nomes
(polêmico)
Helper
, Utils
ou afins
demo/app-antiga
em sua IDE e vamos começar a refatorar juntos!
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