Clean Code - Guia e Exemplos com C#
Clean code, ou código limpo, é um conjunto de boas práticas que aumenta a legibilidade e a manutenibilidade do software. Neste guia, você vai conhecer as principais regras, sempre com exemplos práticos em C#.
O que é clean code
O termo ganhou força com o livro Clean Code, de Robert C. Martin, o Uncle Bob. A obra virou um best seller da nossa área e reúne práticas para manter a vida útil de um software.
Na prática, clean code é um código que outra pessoa lê, entende e altera sem medo. Ou seja, o foco está em quem vai manter o sistema, e não apenas na máquina que o executa.
Importante: este artigo não substitui a leitura do livro, que é muito mais denso e profundo sobre o assunto.
Por que o clean code importa
De fato, a manutenção de um software é tão importante quanto a sua construção. Afinal, as pessoas leem e alteram o código muito mais vezes do que o escrevem.
As empresas investem milhões em software todos os anos. No entanto, com tantas mudanças no time e nas tecnologias, como fazer esse investimento durar?
Segundo Uncle Bob, as práticas a seguir são o caminho. Elas estão organizadas por categoria, como no resumo clássico do livro.
Regras gerais do clean code
Siga as convenções
Se você começou agora em um projeto, ou se o time acabou de definir suas convenções, siga-as. Por exemplo, se o projeto usa constantes em maiúsculas ou enumeradores com o prefixo E, mantenha o padrão.
Para C#, a Microsoft publica as convenções de nomenclatura de identificadores. Elas são um bom ponto de partida quando o projeto ainda não tem regras próprias.
KISS
Mantenha as coisas simples. Esse conceito vem de fora do livro e, particularmente, considero a base de uma boa solução.
Normalmente, tendemos a complicar o que poderia ser muito mais simples. Por isso, lembre sempre do lema Keep It Stupid Simple, ou KISS.
Regra do escoteiro
"Deixe sempre o acampamento mais limpo do que você encontrou." O mesmo vale para o código, então devolva o código melhor do que recebeu.
Se todo desenvolvedor do time tiver essa visão, a soma de pequenas melhorias gera uma grande mudança em pouco tempo.
Causa raiz
Sempre procure a causa raiz do problema e nunca resolva as coisas superficialmente. Na correria do dia a dia, tendemos a aplicar remendos, o que muitas vezes gera retrabalho.
Assim, ao investigar a origem do defeito, você resolve o problema de uma vez por todas.
Regras de design no clean code
Mantenha configurações em alto nível
Toda aplicação tem configurações, como as conhecidas ConnectionStrings. Por isso, mantenha essas configurações, ou o parse delas, no nível mais alto possível.
Evite sobrescrever configurações dentro de Controllers ou de classes do tipo. Se possível, faça essa leitura no início da aplicação e não mexa mais nela.
Em diversas aplicações, crio uma classe Settings no projeto base e a preencho no Startup. Dessa forma, as configurações não ficam espalhadas, e projetos que não são Web não dependem do IConfiguration do ASP.NET.
No projeto MyProject.Domain:
public static class Settings
{
public static string ConnectionString { get; set; }
}
Na camada MyProject.Api:
public Startup(IConfiguration configuration)
{
Configuration = configuration;
Settings.ConnectionString = Configuration.GetConnectionString("connectionString");
}
Por fim, na infraestrutura MyProject.Infra:
using (var connection = new SqlConnection(Settings.ConnectionString))
{
// ...
}
Polimorfismo no lugar de ifs
No clean code, um if traz uma tomada de decisão para a aplicação, o que aumenta a sua complexidade. Por isso, devemos evitar o uso excessivo de condicionais.
Nesses cenários, prefira o polimorfismo da orientação a objetos em vez de decidir em todo método. Considere, por exemplo, uma classe Payment com pagamento via boleto ou cartão de crédito.
No boleto, se o vencimento cair em um fim de semana, o cliente pode pagar no próximo dia útil. Essa regra é apenas uma demonstração e não está 100% correta.
public class Payment
{
public bool CanBePaid()
{
if (Type == EPaymentType.BankSlip)
{
if (!IsWeekend(DueDate))
return true;
}
if (Type == EPaymentType.CreditCard)
{
// ...
}
return false;
}
}
Note que o método CanBePaid tem duas decisões, e a primeira vale apenas para boletos. Então, se surgirem novas formas de pagamento, vamos encher o código de if?
A solução mais plausível é derivar a classe BankSlipPayment da classe base Payment. Assim, ela sobrescreve o método CanBePaid com o comportamento específico.
public class Payment
{
public virtual bool CanBePaid()
{
// ...
}
}
public class BankSlipPayment : Payment
{
public override bool CanBePaid()
{
return !IsWeekend(DueDate);
}
}
Código assíncrono no clean code
Sempre que necessário, use processamento em threads separadas. O C# suporta multi-thread e paralelismo há bastante tempo, e o próprio async/await ajuda nisso.
Sem async/await
[HttpGet("courses")]
public IActionResult Index([FromServices] IContentRepository repository)
{
ViewBag.Courses = repository.GetContents(EContentType.Course);
return View();
}
Com async/await
[HttpGet("courses")]
public async Task<IActionResult> Index([FromServices] IContentRepository repository)
{
ViewBag.Courses = await repository.GetContentsAsync(EContentType.Course);
return View();
}
Além disso, é uma boa prática separar o que é assíncrono do que é síncrono. Dessa forma, um trecho de código não força outro método a mudar de natureza.
public async Task<Model> GetAsync()
{
var model = new Model();
model.Courses = await _context.Courses.ToListAsync();
model.Tags = _context.Tags.ToList(); // Não assíncrono
return model;
}
Por fim, se um método é assíncrono, use sempre o sufixo Async no nome. Essa convenção aparece inclusive nas regras de nomenclatura da documentação do .NET.
public async Task<Model> GetAsync()
Evite configurações desnecessárias
Não deixe configurações no sistema só porque alguém ainda não decidiu como aquilo deve funcionar. Isso polui o código e traz uma complexidade desnecessária.
public void ConfigureMySql()
{
// Ainda não sabemos se vamos suportar MySQL também
throw new NotImplementedException();
}
Utilize injeção de dependência
Sempre que possível, use injeção de dependência. Ela torna o seu código mais limpo e desacoplado.
Para se aprofundar, assista ao vídeo sobre injeção de dependência. Também vale ler o artigo sobre Dependency Inversion Principle.
Lei de Demeter e clean code
A Lei de Demeter (LoD), ou princípio do menor conhecimento, prega três pontos principais:
- Cada unidade deve ter conhecimento limitado sobre outras unidades.
- Cada unidade deve conversar apenas com seus amigos.
- Não fale com estranhos, apenas com seus amigos imediatos.
public class Order
{
public Discount Discount { get; set; }
}
public class Discount
{
public decimal Amount { get; set; }
public void Apply() { /* ... */ }
}
Mau exemplo
public class OrderHandler
{
public void Handle()
{
var order = new Order();
order.Discount.Apply(); // <- conhece o interior do pedido
}
}
Bom exemplo
public class Order
{
public Discount Discount { get; set; }
public void Place()
{
Discount?.Apply();
}
}
public class OrderHandler
{
public void Handle()
{
var order = new Order();
order.Place();
}
}
Clean code e entendimento do código
Seja consistente
No clean code, se você faz algo de uma forma, faça todo o resto da mesma forma. Em outras palavras, siga sempre o padrão definido.
// Codificando em inglês
public class CustomerRepository { }
// Agora mudou para "portuglês"
public class ProdutoRepository { }
// Utilizou o sufixo Async no método assíncrono
public async Task<Product> GetAsync() { }
// Agora não usou mais
public async Task<Course> Get() { }
Também prefira variáveis explicativas, mesmo que resultem em nomes maiores. Elas devem dispensar comentários ou informações adicionais.
// Total do quê?
decimal total = 0;
// Total do carrinho de compras
decimal shoppingCartTotal = 0;
Obsessão primitiva e clean code
No dia a dia, tendemos a usar apenas tipos primitivos, o que gera uma obsessão por eles. Em vez disso, podemos criar objetos de valor, os Value Objects.
Mau exemplo
public class Customer
{
public string Email { get; set; }
// Valida o e-mail
}
public class Employee
{
public string Email { get; set; }
// Valida o e-mail novamente
}
Bom exemplo
// Value Object
public class Email
{
public Email(string address)
{
// Valida o e-mail em um único lugar
Address = address;
}
public string Address { get; private set; }
}
public class Customer
{
public Email Email { get; set; }
}
public class Employee
{
public Email Email { get; set; }
}
O artigo sobre como criar bons Value Objects em C# aprofunda esse tema.
Dependências lógicas e condicionais negativas
Não escreva métodos cujo funcionamento correto dependa de outro estado da própria classe. Nesse caso, a herança costuma expressar melhor a regra.
// Mau exemplo
public class Student
{
public bool IsSubscriber { get; set; }
public void Watch()
{
if (IsSubscriber)
{
// Só executa se for assinante
}
}
}
// Bom exemplo
public class Student { }
public class Subscriber : Student
{
public void Watch() { /* ... */ }
}
Evite também condicionais negativas. Afinal, no C# a negação usa o sinal !, que passa despercebido com facilidade durante a leitura.
// Evite
if (!IsSubscriber) { }
// Utilize
if (IsSubscriber) { }
Regras de nomes no clean code
Nomes descritivos e distinções significativas
Escolher bons nomes para classes, variáveis e métodos é essencial no clean code. Lembre que, se você precisa explicar o seu código, algo pode melhorar nele.
// Evite
var x = 256;
// Duração do quê? Qual a métrica?
int duration = 25;
// Muito mais expressivo
int durationInMinutes = 25;
Além disso, use nomes que permitam diferenciar o significado de outros nomes parecidos.
// Evite
var salary = 7500M;
// Tem um significado maior
var salaryInBrl = 7500M;
Nomes pronunciáveis, buscáveis e sem prefixos
Evite nomes difíceis de pronunciar e convenções inventadas. Também lembre sempre da linguagem ubíqua e da importância dela no código.
// Evite
var strText = "My text here";
// Evite
public void GenerateBoletoInLote() { }
// Evite
public void Cadastry() { }
Da mesma forma, não use prefixos com o tipo da variável, da classe ou do método. Nunca use espaços ou caracteres especiais nesses itens.
// Evite
public class clsCustomer { }
// Evite
string strName = "John";
// Evite (acentos no identificador)
var situação = "Pending";
Constantes no lugar de strings repetidas
Quem nunca perdeu horas procurando um bug que era apenas uma comparação de string? Por isso, evite digitar a mesma string várias vezes e use constantes.
// Evite
if (environment == "PROD") { }
// Utilize
const string Production = "PROD";
if (environment == Production) { }
Funções e métodos no clean code
Pequenas e com apenas um objetivo
Para o clean code, funções devem ser o menor possível. Afinal, é mais fácil ter métodos menores e reutilizáveis do que tudo dentro de um só.
// Evite
public void PlaceOrder()
{
// Cadastra o cliente
// Aplica o desconto
// Atualiza o estoque
// Salva o pedido
}
// Utilize
public void SaveCustomer() { }
public void ApplyDiscount() { }
public void UpdateInventory() { }
public void PlaceOrder() { }
A mesma regra de nomes vale aqui. Então, mantenha nomes concisos e sem caracteres especiais.
// Evite: calcular o quê?
public void Calculate() { }
// Utilize: calcula o ICMS
public void CalculateIcms() { }
Opte por poucos parâmetros
Evite exigir muitos parâmetros e use os Optional Parameters do C# quando fizer sentido. Muitas vezes, um objeto agrupa melhor os dados relacionados.
// Evite
public void SaveCustomer(string street, string number, string neighborhood, string city, string state, string country, string zipCode) { }
// Melhorando
public void SaveCustomer(Address address) { }
Efeitos colaterais e flags
Evite que uma função altere valores de outra classe que não seja a dela. Esse comportamento recebe o nome de efeito colateral.
// Evite: qualquer um fora da classe pode alterar o total
public class Order
{
public decimal Total { get; set; }
}
// Utilize: o total só muda por dentro da classe
public class Order
{
public decimal Total { get; private set; }
public void CalculateTotal() { /* ... */ }
}
var order = new Order();
order.Total = 250; // ERRO de compilação
Também não use as famosas flags para tomar decisões dentro dos métodos. Em vez disso, divida a lógica em vários métodos ou até em outras classes.
// Evite
public void CreateOrUpdate(Customer customer, bool create) { }
// Utilize
public void Create(Customer customer) { }
public void Update(Customer customer) { }
Comentários no clean code
O que evitar nos comentários
No clean code, um bom código é expressivo. Teoricamente, se você precisa comentar uma parte dele, algo não está claro o suficiente.
Portanto, evite comentários redundantes, que não acrescentam nada ao contexto. Também não feche comentários nem deixe código comentado, pois o controle de versão permite voltar no tempo.
// Evite: redundante
// Função principal do sistema
public void Main() { }
// Evite: fechamento desnecessário
// Comentário // <-
public void Main() { }
// Evite: código comentado
public void MyFunction()
{
// string text = "1234";
}
Quando os comentários ajudam
Um bom uso de comentários é explicar a intenção de um método ou classe. Outro uso interessante é esclarecer uma regra de negócio que não fica óbvia.
// Retorna a lista de produtos inativos
// para o relatório de fechamento mensal
public IList<Product> GetInactiveProducts() { /* ... */ }
public void CancelOrder()
{
// Caso o pedido já tenha sido enviado,
// ele não pode mais ser cancelado
if (ShippedAt < DateTime.Now)
AddNotification("The order has already been shipped and cannot be canceled");
}
Por fim, use comentários para alertar sobre consequências sérias. Nesse caso, recomendo um comentário XML mais elaborado.
/// <summary>
/// ATENÇÃO: este método cancela o pedido e estorna o pagamento
/// </summary>
public void CancelOrder() { /* ... */ }
Estrutura do código no clean code
Organização vertical
O clean code também cuida da estrutura de pastas, que deve ser saudável e organizada. Não precisa criar uma pasta para cada arquivo, mas separe por contexto ou funcionalidade.
MyApp
MyApp.Domain
MyApp.Domain.Contexts
MyApp.Domain.Contexts.PaymentContext
MyApp.Domain.Contexts.PaymentContext.Entities
MyApp.Domain.Contexts.PaymentContext.ValueObjects
MyApp.Domain.Contexts.PaymentContext.Enums
MyApp.Domain.Contexts.AccountContext
MyApp.Domain.Contexts.AccountContext.Entities
MyApp.Domain.Contexts.AccountContext.ValueObjects
MyApp.Domain.Contexts.AccountContext.Enums
Declare variáveis próximas do ponto de uso, e não todas juntas no começo da classe. Da mesma forma, mantenha funções similares por perto.
// Utilize: funções relacionadas agrupadas
public void CreateCustomer() { }
public void UpdateCustomer() { }
public void CheckInventory() { }
public void CreateOrder() { }
public void CalculateTotal() { }
Ordenar as funções de cima para baixo também importa. Assim, as sobrecargas aparecem da mais simples para a mais completa.
public void CreateCustomer(string name) { }
public void CreateCustomer(string name, int age) { }
public void CreateCustomer(string name, int age, Address address) { }
public void CreateCustomer(string name, int age, Address address, bool active) { }
Evite também funções com linhas longas ou com muitas linhas. Não existe um número correto, mas quanto mais código uma função tem, mais difícil é mantê-la.
public void CreateCustomer(string name)
{
var customer = new Customer(name);
_repository.Customers.Add(customer);
_repository.SaveChanges();
}
Formatação horizontal
Não há necessidade de alinhar horizontalmente variáveis, constantes ou propriedades. Esse alinhamento quebra a cada mudança e não ajuda a leitura.
// Evite
private Long requestParsingTimeLimit;
protected Request request;
private FitNesseContent context;
Use espaços em branco para associar itens relacionados, e uma boa IDE já faz esse trabalho por você. Além disso, nunca quebre a indentação, porque código sem indentação não deve entrar no projeto.
// Evite
public class MyClass{
public void Print(){
var value=12;
Console.WriteLine(value);}
}
Objetos e estruturas de dados no clean code
Esconda estruturas internas
Esse tópico do clean code gera uma discussão extensa. Esconder a estrutura de um objeto sempre traz efeitos positivos e negativos.
Particularmente, gosto de tornar os set privados, mas não aplico isso em todas as propriedades. Como consequência, precisamos de mais métodos para manipular os valores.
Se os dados não fazem sentido para objetos externos, não há discussão: mantenha-os privados.
public class NotificationContext
{
private readonly List<string> _notifications = new();
public void Add(string notification) => _notifications.Add(notification);
public bool IsValid() => !_notifications.Any();
public IEnumerable<string> Notifications => _notifications.AsEnumerable();
}
Estruturas de dados e objetos de valor
Estruturas de dados representam a forma como organizamos os dados, seja com class ou com struct. A diferença é que, com classes, temos abstração, herança e polimorfismo.
// Usando struct
public struct Email
{
public Email(string address)
{
// Permite apenas e-mails de provedores conhecidos
Address = address;
}
public string Address { get; private set; }
}
// Usando classes
public class Email
{
public Email(string address)
{
// Permite qualquer tipo de e-mail
Address = address;
}
public string Address { get; private set; }
}
public class CommonEmail : Email
{
public CommonEmail(string address) : base(address)
{
// Permite apenas e-mails de provedores conhecidos
}
}
Nos dois casos, o e-mail vira um objeto de valor. No entanto, o segundo cenário permite criar extensões e oferece mais flexibilidade.
Outro ponto polêmico é evitar misturar dados e objetos. Muitos interpretam isso como deixar só propriedades nos objetos e mover os comportamentos para outras classes.
Particularmente, acho que a essência de um objeto é agrupar propriedades e métodos, então mantenho os comportamentos nas entidades. Mais uma vez, os Value Objects aplicam bem esse conceito.
// Objeto de valor: representa a estrutura de dados de um endereço
public class Address
{
public string ZipCode { get; set; }
public string Street { get; set; }
public string Number { get; set; }
public string City { get; set; }
}
// Objeto do cliente, com seus comportamentos
public class Customer
{
public Address BillingAddress { get; private set; }
public Address ShippingAddress { get; private set; }
public void ChangeBillingAddress(Address address) { /* ... */ }
public void ChangeShippingAddress(Address address) { /* ... */ }
}
Poucas variáveis e herança saudável
Evite instanciar muitas variáveis nos objetos e nos seus métodos. Sempre que possível, aproveite as propriedades.
public class ShoppingCart
{
public List<CartItem> Items { get; } = new();
public decimal Total { get; private set; }
public void CalculateTotal()
{
Total = 0;
foreach (var item in Items)
Total += item.Price;
}
}
Além disso, uma classe base não deve saber nada sobre suas derivadas. Na verdade, não vejo um cenário saudável em que a classe pai conheça detalhes dos filhos.
Mais métodos, menos tomadas de decisão
O clean code prefere mais métodos e mais sobrecargas a tomadas de decisão. Essa regra reforça o que já vimos sobre polimorfismo.
// Evite
public void Pay(CreditCard card)
{
if (card == null)
{
// Pagamento via boleto
}
// Pagamento via cartão
}
// Utilize
public void Pay()
{
// Pagamento via boleto
}
public void Pay(CreditCard card)
{
// Pagamento via cartão de crédito
}
Evite métodos estáticos
Classes e métodos estáticos são difíceis de gerenciar e ficam compartilhados pela aplicação inteira. Imagine uma lista estática de notificações compartilhada entre as requisições de diversos usuários.
// Evite
public static class NotificationContext
{
public static IList<Notification> Notifications { get; set; }
}
// Utilize
public class NotificationContext
{
public IList<Notification> Notifications { get; set; }
}
Testes no clean code
No clean code, trate seus testes como parte fundamental do código, e não como algo secundário. O livro resume as qualidades de um bom teste em cinco pontos.
Um assert por teste e legibilidade
Use um, e apenas um, assert por teste. Afinal, mais de um assert confunde e compromete a clareza da verificação.
// Evite
[TestMethod]
public void ShouldReturnTrue()
{
Assert.IsTrue(result);
Assert.AreEqual(1, count);
}
// Utilize
[TestMethod]
public void ShouldReturnTrue()
{
Assert.IsTrue(result);
}
Os testes também precisam de organização e boa escrita, assim como o resto do software. O artigo sobre fundamentos dos testes de unidade aprofunda essa base.
Rápidos, independentes e repetíveis
Cada teste cobre uma pequena porção do código, e normalmente rodamos todos antes de publicar a aplicação. Por isso, em cenários críticos, testes lentos atrasam o deploy.
Os testes também não devem depender de entidades externas nem de outros testes. Mais uma vez, a injeção de dependência e o DIP resolvem esse problema.
O mesmo vídeo sobre injeção de dependência mostra esse isolamento na prática.
Por fim, devemos conseguir repetir o mesmo teste com parâmetros diferentes.
[TestMethod]
[DataRow("valid@email.com")]
[DataRow("email@balta.io")]
public void ShouldValidateEmail(string email)
{
Assert.IsTrue(new Email(email).IsValid());
}
Code smells e a falta de clean code
Code smells são sintomas que indicam uma má aplicação do clean code de forma geral. Conhecer cada um ajuda a identificar o problema cedo.
- Rigidez: o software é difícil de mudar, e qualquer mudança mínima causa uma cascata de outras.
- Fragilidade: uma simples mudança quebra o software em diversos locais, o famoso cobre o pé e descobre a cabeça.
- Imobilidade: você não consegue reaproveitar partes do código em outros projetos, porque tudo está muito acoplado.
- Complexidade desnecessária: padrões e arquiteturas tornam o código mais burocrático do que efetivo.
- Repetição desnecessária: você precisa repetir o mesmo código em diversos lugares.
- Opacidade: o código é difícil de entender.
A complexidade desnecessária nasce do famoso "e se". Por exemplo: e se eu quiser voar com meu carro um dia?
Bem, se um dia você precisar voar, aí você constrói as asas. Enquanto isso, foque em construir apenas o carro.
Conclusão
O clean code não é um conjunto de leis, e sim uma forma de pensar em quem lerá o código amanhã. Nomes claros, funções pequenas e poucas decisões já mudam bastante a qualidade de um projeto.
Comece pelas regras mais simples, como nomes descritivos e a regra do escoteiro. Em seguida, avance para design, testes e estrutura, sempre com o time alinhado.
Depois, complemente o estudo com os princípios do SOLID. Eles conversam diretamente com várias práticas apresentadas aqui.
FAQ
Quem criou o conceito de clean code?
O conceito ficou conhecido com o livro Clean Code, de Robert C. Martin, também chamado de Uncle Bob.
Clean code vale apenas para C#?
Não. As práticas valem para qualquer linguagem, embora os exemplos deste artigo usem C#.
Preciso aplicar todas as regras de uma vez?
Não. O ideal é adotar as regras aos poucos, seguindo a regra do escoteiro a cada alteração.
Comentários são proibidos no clean code?
Não. Eles devem explicar intenção, esclarecer regras ou alertar sobre consequências, e não repetir o que o código já diz.
Qual a relação entre clean code e SOLID?
Os dois vêm de Robert C. Martin e se complementam. O SOLID foca em design orientado a objetos, enquanto o clean code cobre também nomes, funções e comentários.