quinta-feira, 28 de novembro de 2013

O nirvana do bom código fonte


"Nirvana é a libertação do sofrimento"

Ter um código fonte com essas características, é propriedade fundamental de todo software com qualidade. 

Nirvana é a libertação do sofrimento

A dívida técnica causada pelo código fonte de baixa qualidade é causa de muito sofrimento entre os desenvolvedores. Dificuldades, erros e retrabalhos comprometem até mesmo as boas iniciativas, como os métodos ágeis, por exemplo. Não há "XP", "Sprint" e "Kanban" que resistam a um código ruim, complexo e difícil de manter e testar. 

A única maneira de atingir o nirvana do código fonte é produzir código que seja:

  • Pequeno: Classes pequenas e métodos pequenos;
  • Coeso: As classes e métodos devem ter uma e somente uma responsabilidade, atendo-se estritamente a ela;
  • Simples: Devemos aumentar o reuso, substituindo procedimentos por configuração e evitando "reinventar a roda";
  • Efetivamente testado: A única evidência que um software foi testado é a presença de código de  testes automatizados e idempotentes, com a maior cobertura possível;
  • Auto documentado: A leitura do código fonte dispensa quaisquer outros recursos, como: Comentários, diagramas UML e documentação externa;
Além disto, o código fonte deve ser construído com respeito às boas práticas da engenharia de software e regras consagradas de validação, como as validações de ferramentas respeitadas: PMD, Checkstyle e Findbugs.

Neste artigo, vamos falar brevemente sobre estes fatores de nirvana.


Tamanho importa sim !

Yeah, baby! Ao contrário do que você possa ter ouvido, quando se trata de código fonte, tamanho importa sim. A facilidade de entendimento de um método é inversamente proporcional ao seu tamanho, medido em número de linhas. Quanto maior o método ou a classe, mais difícil de entender ele será. E não se trata apenas de facilitade de compreensão, existem outros fatores associados ao código grande:

  • Quanto maior uma classe, mais difícil de compreender, testar e manter ela será;
  • Classes grandes provavelmente violam o SRP - Single Responsability Principle;
  • Métodos devem ser lidos sem necessidade de “rolar a tela”. Máximo de 50 linhas de comandos;
  • Métodos grandes tendem a ser pouco coesos (coesão funcional - Page Jones);
  • Métodos de baixa coesão são difíces de manter e propagam alterações;
Por que você escreveu um método grande?

Eu trabalho diariamente analisando código fonte, e frequentemente me deparo com métodos enormes, maiores que 100 linhas e o mais curioso é que os programadores, as maiores vítimas de métodos grandes, os defendem! Para mim, as causas mais comuns são:
  1. Reinvenção de roda: Você está fazendo manualmente o que poderia ser feito por um componente de terceiros. Coisas como: "Parsing" de XML usando loops e substrings, ou sincronização por "file system" ou ainda leitura e / ou gravação procedural;
  2. Results driven programming: Consequência direta da má gestão de projetos, o RDP é um antipattern, no qual a equipe é muito focada em entregas, mas tem pouca preocupação com a qualidade geral do software;
  3. Copy and paste programming: Quando é mais fácil programar por aglutinação do que por raciocínio. É o "reuso nefasto" de código fonte, no qual vamos copiando e colando, criando verdadeiros monstros;

Então, o que fazer? 

Kent Beck, em seu livro "Test Driven Development: By Example", de 2002, já apontou o caminho, pois a sua prática, o Test Driven Development (TDD) prega que devemos repetir o ciclo:

1. Adicionar um teste;
2. Executar os testes e notar as falhas;
3. Escrever código para passar nos testes;
4. Executar os testes automatizados e repetir 3 e 4 até que consiga sucesso;
5. Refatorar o código gerado;
6. Repita todo o ciclo até concluir todos os requisitos.

Refatorações simples podem dar bons resultados: 
  • Extract Class / Extract Method / Move Method;
  • Replace Conditional with Polymorphism;



Código coeso não quebra !

O que é coesão? Meilir Page-Jones já falava sobre isso e seus conceitos se coesão são válidos até hoje! Não se trata apenas de coesão de classes, detetada por métricas mais recentes, como LCOM (Chidamber & Kemerer) e LCOM4 (Hitz & Montazeri), mas da velha e boa coesão de módulos, pois cada método pode ser encarado como um módulo, que deve fazer parte de um todo, contribuindo para a responsabilidade única de uma classe.

Código coeso não quebra! Em outras palavras, é uma barreira contra o "britleness" e a propagação de alterações, aumentando a manutenibilidade e flexibilidade do software. 

Vamos enumerar os tipos de coesão possíveis em métodos, em ordem decrescente de qualidade:
  1. Funcional: Os comandos estão presentes porque são necessários à função do método - Melhor tipo;
  2. Sequencial: Os comandos estão organizados em sequência, porque a saída de um é entrada para o outro;
  3. Comunicacional: Os comandos não são relacionados, exceto por operarem sobre os mesmos dados;
  4. Procedural: Os elementos são agrupados porque seguem a mesma sequência, mas não são totalmente relacionados;
  5. Temporal: Os elementos estão juntos pelo momento;
  6. Lógica: Os elementos estão juntos por que fazem parte da mesma categoria lógica (Switch);
  7. Coincidental: Os elementos não tem a menor relação entre si, exceto pelo agrupamento (classes utilitárias) - PIOR TIPO;
Em um código funcionalmente coeso, o método responde por apenas uma e somente uma função, e todos os comandos que ele contém, contribuem para a execução apenas desta função. Se a classe também for coesa e obedecer ao SRP (Single Responsability Principle), então é o nirvana total! Eis um exemplo de método funcionalmente coeso:

public boolean persistAll(TipoListaAlertas alertas) {
  try {
   final EntityManager saveManager = entityManagerFactory
     .createEntityManager();
   saveManager.getTransaction().begin();
   saveManager.persist(alertas);
   saveManager.flush();
   saveManager.getTransaction().commit();
   saveManager.close();
   return true;
  }
  catch (Exception e) {
   logger.error("Erro ao persistir alertas: " 
                                  + e.getMessage());
   throw new PersistenceException
                              ("Erro ao persistir alertas: " +  
    e.getMessage(),e);
  }
 }

Neste pequeno método, podemos ver exatamente o que ele faz, com apenas uma rápida olhada. Eu pedi à algumas pessoas que dissessem o que o método faz, e a que demorou mais levou 15 segundos para isto. Não há comandos que não estejam intimamente relacionados com a função do método, que é persistir uma entidade. Este método está "blindado" contra propagação de alterações, pois mesmo que a classe da Entidade seja alterada, ele se manterá intacto.

Agora, vamos ver um tipo pior de coesão, a Temporal:


private void initComponents() {
 this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
 this.setLayout(new FlowLayout());
 JPanel tela = new JPanel();
 this.getContentPane().add(tela);
 tela.setLayout(new BoxLayout(tela, BoxLayout.Y_AXIS));
 tela.setBackground(Color.WHITE);
 Dimension dim = Toolkit.getDefaultToolkit().getScreenSize();
 this.setTitle("Processador de alertas");
 tela.add(lbl1);
 tela.add(txtArquivo);
 btnExecutar.addActionListener(this);
 btnExecutar.setActionCommand("executar");
 btnLocalizar.addActionListener(this);
 btnLocalizar.setActionCommand("localizar");
 tela.add(btnLocalizar);
 tela.add(btnExecutar);
 textArea = new JTextArea(5, 20);
 JScrollPane scrollPane = new JScrollPane(textArea); 
 textArea.setEditable(false);
 tela.add(scrollPane);
 this.pack();
 int w = this.getSize().width;
 int h = this.getSize().height;
 int x = (dim.width-w)/2;
 int y = (dim.height-h)/2;
 this.setLocation(x, y);
}

Este método, por definição, já é pouco coeso. A razão para estes comandos estarem nele, é por ser o momento de inicializar os componentes "javax.swing" de uma tela "JFrame". São vários componentes e os comandos não estão muito relacionados entre si. Uma prova disto é que o método é sujeito a alterações por várias razões diferentes, em função de sua baixa coesão. Ter alguns métodos assim é justificável, se for uma classe "casca", que siga estritamente o seu estereótipo de UI. Se houver métodos de lógica de negócios ou de persistência, então temos um problema mais sério ainda: código monolítico.

Como saber se um método tem baixa coesão?

Bem, se você ver que o método está fazendo mais de uma coisa, então é um indício claro. Outra pista que também pode ser seguida é tentar descrever sua função em uma só oração. Se você precisar de uma conjunção aditiva, então o método assume mais de uma responsabilidade e é de baixa coesão, por exemplo: "CalculaJurosEmulta()" é um método de baixa coesão, pois são duas responsabilidades diferentes.

Outra maneira é analisar seu acoplamento (outro conceito de Meilir Page-Jones) com outros métodos. Vamos listar rapidamente os tipos de acoplamento:

  1. Dados: Dados atômicos são passados entre métodos, como parâmetros e como resposta - Baixo acoplamento;
  2. Imagem: Um Objeto ou estrutura é passado - Médio acoplamento;
  3. Comum: Uso de variáveis globais pelos métodos, ou de variáveis de contexto entre métodos de classes diferentes - Alto acoplamento;
  4. Controle: Um “flag” ou indicador é passado de um método para outro, que modifica seu comportamento com base nisto - Péssimo acoplamento;
  5. Conteúdo: (“goto”) Em Java, é quanto um método usa a Reflection API para acessar métodos e variáveis privadas de outra classe - Não há como classificar essa atitude!

Busque por indícios de alto acoplamento entre métodos e, se existirem, provavelmente os dois métodos apresentam baixa coesão.

Meu método é pouco coeso... O que devo fazer?

Calma, tudo tem jeito! As mesmas soluções aplicadas para o tamanho, servem para aumentar a coesão. Vamos ver um pequeno exemplo:

class Xpto {
 ...
 void m() {
  ...
  Calculadora calc = new Calculadora();
  if (deltax < reft) {
   x = calc.verificar(deltax, 2);
  }
  else if (deltax > reft) {
    x = calc.verificar(deltax, 1);
    ...
 }
class Abcd {
 ...
 void n() {
  if ...
   x = calc.verificar(deltax, 3);
  ...
  if ...
   x = calc.verificar(deltax, 1);
   ...
 }
class Calculadora {
 ...
 float verificar(float valor, int tipo) {
  float calculo = 0.0f;
  switch case (tipo) {
   case 1:
    calculo = ...
    break;
   case 2:
    calculo = ...
    break;
   case 3:
    calculo = ...
  }
  return calculo;
 }
}

As classes "Xpto" e "Abcd" apresentam alto acoplamento com a classe "Calculadora", pois existe um acoplamento de controle entre seus métodos "m()", "n()" com o método "verificar", desta última. Eles precisam saber qual é o valor do "flag" "tipo" que deverá ser passado. Alterações nesse "protocolo" pode implicar em alterações nas três classes.

Isto quer dizer que o método "verificar()", da classe "Calculadora", apresenta coesão Lógica, ou seja, muda seu comportamento de acordo com um parâmetro externo.

Se um método muda seu comportamento de acordo com o parâmetro, afeta completamente o que se espera da classe, logo, seria melhor criar classes diferentes e usar o polimorfismo para resolver o problema: Replace Conditional with Polymorphism parece ser um bom refactoring a ser aplicado. A classe "Calculadora" poderia ser fatorada em três outras subclasses, e as classes "Xpto" e "Abcd" poderiam usar Dependency Injection para "injetar" as instâncias necessárias.

Isto aumentaria a coesão dos métodos, diminuiria o acoplamento entre eles e, ainda por cima, melhoraria a arquitetura e projeto do software, através da aplicação do princípio DIP - Dependency Inversion Principle.


A complexidade vende mais

O grande professor Dijkstra disse:
"Simplicity is a great virtue but it requires hard work to achieve it and education to appreciate it. And to make matters worse: complexity sells better."
Traduzindo: A simplicidade é uma grande virtudde, mas requer trabalho duro para alcançar e educação para apreciar. E, para complicar as coisas: A complexidade vende mais.

Quem não conhece "heróis" no trabalho? Aqueles caras que faltam, chegam atrasados, cospem no chão, tratam a todos como se fossem lixo, mas, sempre que é necessário, estão lá para resolver os problemas e salvar a pele de todos, afinal, só eles conseguem entender o software complexo que criaram. A complexidade vende mais e melhor. As pessoas tendem a acreditar que coisas complexas são coisas boas.

Código fonte complexo significa também que ele tem baixa manutenibilidade, baixa flexibilidade e baixa testabilidade.

Uma das maneiras de medir a complexidade é através do cálculo de Complexidade ciclomática, desenvolvido por Thomas J. McCabe, Sr, em 1976. Ele se propõe a estudar a complexidade de um módulo de código fonte, através da sua transformação em grafo. Estudamos a quantidade de caminhos independentes, existentes em um trecho de código fonte e estimamos sua complexidade a partir dai. Por exemplo, suponha este trecho de código:

 1 public int print(Graphics g, PageFormat pf, int pagina)
 2   throws PrinterException {
 3  int retorno = PAGE_EXISTS;
 4  int totalPaginas = relatorio.size() / 30;
 5  if (relatorio.size() % 30 > 0) {
 6   totalPaginas++;
 7  }
 8  if (pagina > totalPaginas) {
 9   retorno = NO_SUCH_PAGE;
10  }
11  else {
12   int inicial = pagina * 30;
13   int contaLinhas = 0;
14   Graphics2D g2d = (Graphics2D)g;
15      g2d.translate(pf.getImageableX(), pf.getImageableY());
16   for (int x = inicial; x < relatorio.size(); x++) {
17    contaLinhas++;
18    if (contaLinhas > 30) {
19     break;
20    }
21    g.drawString(relatorio.get(x), 40, 50 + contaLinhas * 14);
22   }
23  }
24  return retorno;
25 }

Pegamos os comandos mais relevantes, os de controle de fluxo, e montamos um grafo, sendo as linhas os nós e as arestas os caminhos:


A complexidade deste trecho de código pode ser calculada como 5. Isto significa que temos 5 caminhos independentes (acrescentam um novo nó ou passam por uma nova aresta) a serem testados:

1) 5,6,8,9,24
2) 5,8,9,24
3) 5,8,16,24
4) 5,8,16,18,24
5) 5,5,8,16,18,21,16,24

E qual é um bom número? 

Infelizmente, McCabe disse que, quando um módulo atingisse complexidade ciclomática 10, ele deveria ser refatorado. O que faltou ele dizer é que 10 é um número péssimo, então, muita gente confundiu as coisas e passou a considerar 10 como um bom número. Mas não é! Eu aconselho que você limite a 5! E configure suas ferramentas de análise para isso.

Por que 5?

Para começar, Steve McConnell, autor do livro "Code Complete: A Practical Handbook of Software Construction", diz que este é um valor considerado ok, logo, eu não estou sozinho esta afirmação.

A segunda corroboração vem da área de Psicologia cognitiva, mas especificamente, de um artigo escrito pelo pesquisador George A. Miller, no qual ele sugere que as pessoas conseguem manter 7+- 2 objetos em sua memória de trabalho. Alguns conseguem lidar com até 9 objetos simultaneamente, mas alguns só conseguem lidar com 5. Logo, faz todo sentido limitar a complexidade a este número, tornando fácil para todas as pessoas compreenderem o nosso código fonte.

E, para concluir, eu não acho prático criar mais do que 5 casos de teste só para um método. Isto não quer dizer que 5 é bom, mas que é um limite superior tolerável e que devemos tentar reduzir ao máximo a complexidade dos nossos métodos e classes.

Como reduzir complexidade?

Mais uma vez, através de refatorações. Mas não é só isso, pois a causa maior de complexidade é o código procedural, que pode denotar coesão Procedural de métodos, o que é muito ruim.

Devemos ter como meta o reuso! Evitar reinventar a roda, procurando usar componentes de terceiros, sempre que possível. Assim, substituímos PROCEDIMENTO por CONFIGURAÇÃO, diminuindo a complexidade dos nosso próprio trabalho.


Seu código passou no anti dopping?

A única evidência de que um software foi testado, é a presença e idempotência do código de teste. Nada mais prova que seu software foi testado. Simples assim. E, se você não entrega o código de teste, você está enganando seu Cliente, pois ele pagou por isso.

Testes idempotentes podem ser repetidos sem comprometer a infraestrutura ou a integridade do software, e podem ser executados através de automatização, em um ambiente de Integração contínua.

Sempre que eu pergunto "Cadê os testes?" ouço as mais velhas e esfarrapadas desculpas:

  • "Deve estar na máquina do Fulano ou do Cicrano";
  • "O cara que roda os testes está de férias";
  • "Tem uma planilha com os resultados de cada teste";
Cara, se eu sou o Cliente, processava todos por estelionato, afinal, se eu paguei por algo, tenho direito a ver. Muitas vezes, o código de teste é "negligenciado" para mascarar a verdade: nem tudo está funcionando como deveria. 

Certa vez, eu estava ouvindo a preleção de uma especialista em "Qualidade de software", que recomendava usar um documento PDF assinado, com o "Screen shot" das telas mostrando o resultado dos testes. Eu comecei a rir e ela ficou zangada comigo. Eu pedi desculpas e fiz a pergunta mortal: "Quem garante que os Screen shots são autênticos?" Ficou um silêncio constrangedor no Auditório e ela mudou de assunto.

Como diria um grande amigo meu: "É tão ingênuo que chega a ser bonitinho"...

Cobertura de código

Além da presença do código de teste, é necessário atestar a sua qualidade... Que raios é isso? Um bom teste é aquele que exercita o máximo de linhas e condições do código fonte possíveis. Se um teste exercita menos de 80% do código fonte, o que ele está testando? Tem pelo menos 20% que não foram testados!!!

Cobertura de testes ou "Code coverage" é uma medida de quanto o código fonte foi exercitado após um teste. As métricas de cobertura de teste mais comuns são:

  • Function coverage: Cada função presente foi executada?
  • Statement coverage: Cada comando foi executado?
  • Branch coverage: Cada ramificação de cada estrutura de controle foi executada?
  • Decision coverage: Cada possível resultado de decisão foi executado?
  • Condition coverage: Cada sub expressão lógica foi executada?
  • Condition/decision coverage: Cada par de decisão e condição foi executado e verificado?
  • State coverage: Cada estádo (em uma máquina de estados) foi testado?
  • Parameter Value Coverage: Cada possível valor de parâmetro foi testado?
Podemos medira a cobertura de testes com ferramentas, como o "JaCoCo", o "Cobertura" ou mesmo com o "SonarQube". 

Segregação de testes

E devemos segregar os testes de acordo com o objetivo:
  • Testes Caixa Preta: Funcionais. Avaliam se o sistema está de acordo com os requisitos. Podem ser automatizados usando o Selenium, rodando na IC;
  • Testes Caixa Branca: Estruturais. Avaliam COMO o sistema está funcionando. São automatizados via plugins Maven. Temos: Unitários e de Integração;
  • Testes de Sistema: Avaliam os RNFs, e podem ser de carga ou “stress”. 
Mas não é só isso... Temos que fazer o que muitos desenvolvedores negligenciam, ou seja segregar os testes quanto ao alvo:
  • Testes de unidade: Devem testar apenas uma unidade lógica, que é uma classe ou um conjunto delas, que só funcionam juntas - Objetivo: acurácia da unidade;
  • Testes de integração: Devem testar se as várias unidades funcionam em conjunto - Objetivo: aderência à especificação;
Quando misturamos os alvos, temos problemas, pois nem testamos bem as unidades e nem as especificações.

O que é um bom número?

Simples: 100% de cobertura garante que seu código foi testado completamente.

Porém, isso nem sempre é possível ou economicamente viável de alcançar... Existem alguns obstáculos que podem impedir uma cobertura maior de testes, entre eles:
  • Classes criadas por geradores de código: Nem sempre é possível ou desejável ficar testando tudo, pois, afinal, podem ser recriadas sem o nosso controle;
  • Classes com estereótipos fortes: Por exemplo, classes de UI como: JFrames ou JSF ManagedBeans são mais difíceis de testar, pois exitem a presença de muitos recursos (Servidores e APIs), o que dificulta o teste;
  • Classes de alta complexidade: Podem exigir muitos casos de teste e, mesmo assim, podemos deixar de cobrir todas as condições;
Mas tem como melhorar isso?

Certamente que sim! As classes criadas por geradores de código devem ser ignoradas, afinal, ou você confia no gerador de código, ou deve deixar de usá-lo. Um exemplo é o HyperJAXB3, um pacote que gera Entidades que podem ser desserializadas a partir de arquivos XML e persistidas em Banco de dados com o Hibernate. Ele gera classes a partir de um esquema XML (XSD) e, certamente, não vamos nem tentar testar tudo. Neste caso, podemos configurar a ferramenta de avaliação para ignorar estas classes.

Quanto às classes com estereótipos fortes, é possível testar usando ferramentas de automação de testes funcionais:
  • FEST - testar classes Swing: (http://docs.codehaus.org/display/FEST/Home);
  • Selenium - testar a partir do navegador (http://www.seleniumhq.org/);
  • JSFUnity  - testar aplicações Javaserver Faces (http://www.jboss.org/jsfunit/);
E, finalmente, quanto às classes de grande complexidade, é melhor refatorá-las para melhorar sua testabilidade, simples assim. 

Teste unitário ou de integração?

A segregação por alvo não afeta diretamente a testabilidade. É possível criar testes mistos que cubram boa parte do código. A questão é que, quando misturamos as coisas, podemos, mesmo que indiretamente, deixar de testar condições, como no caso de exceções, por exemplo. 

Se você seguiu os princípios: ISP - Interface Segregation Principle e DIP - Dependency Inversion Principle, então criou abstrações para os principais comportamentos e estereótipos de seu software, logo, pode usar um software como o JMock para sintetizar classes concretas "on the fly", podendo criar condições de teste mais detalhadas. 

Um exemplo, vamos supor que você escreva um teste para uma classe de negócio. Esta classe precisa acessar um Banco de dados, e você usa um SGBD de teste, isso está ok? Não! Para começar, este teste não é idempotente, ou seja, o estado do banco de dados precisa ser recomposto após cada teste, e ele precisa estar ativo. Em segundo lugar, se você está acessando um SGBD, então trata-se de um teste de integração. 

O certo seria usar um padrão DAO, através de uma abstração (uma interface), injetada em sua classe. Você pode sintetizar uma instância para usar em seu teste e controlar o que está sendo lido do Banco de dados, inclusive, criando exceções, que seriam muito difíceis de forjar com um SGBD de verdade. Além disto, este teste seria idempotente


O código deve ser sua própria documentação

Por que perder tempo criando diagramas UML e escrevendo arquivos de documentação? Isto é DTD - Desperdício de Tempo e Dinheiro! E vou te dar uma razão muito simples: Ficarão desatualizados rapidamente.

Código auto documentado

É o código que se auto explica, sem a necessidade de documentação adicional, como: fluxos, diagramas UML, comentários etc (ObjectMentor).

Comentários devem existir no código, mas apenas para documentar a sua API (Classes e métodos). Não devem existir comentários que tentem explicar o código fonte. 

Vamos ver o que dizem sobre comentários no código fonte:
  • Javaranch: Debug only code. Comments can lie. (depure apenas o código. Comentários podem mentir);
  • Martin Fowler (Refactorings): Comments lead us to bad code... often are used as a deodorant (for bad smells) (Comentários nos levam a código ruim... sendo frequentemente usados como desodorante (para maus cheiros));
Como fazer?

Deixe de ser "programador cientista" e escreva código claro e simples! Por exemplo, ao invés disto: 

public double calcular(double a, double b, double c) {
 return (b*b-4*a*c)<0?Double.NaN:Math.sqrt(b*b-4*a*c);
}

Prefira isto:

/**
 * Calcula o valor do delta de uma equação do segundo grau, 
 * segundo a fórmula de Bhaskara,
 * já calculada sua raiz quadrada.
 * @param coeficienteA double valor do coeficiente "a" da equação
 * @param coeficienteB double valor do coeficiente "b" da equação
 * @param coeficienteC double valor do coeficiente "c" da equação
 * @return valor do delta ou NaN se for negativo
 */
public double calcularDelta(double coeficienteA, 
        double coeficienteB, 
                            double coeficienteC) {
 double valorDelta =  Math.pow(coeficienteB, 2) 
          - 4 * coeficienteA * coeficienteC;
 if (valorDelta < 0) {
  valorDelta = Double.NaN;
 }
 else {
  valorDelta = Math.sqrt(valorDelta);
 }
 return valorDelta;
}

Existem vários refactorings associados a código auto documentado, entre eles:

  • Extract (method e class);
  • Decompose conditional;
  • Introduce Explaining Variable;
  • Replace Conditional with Polymorphism;
  • Replace Nested Conditional with Guard Clauses;
  • Replace Parameter with Explicit Methods; 



Onde eu posso saber mais sobre isso?


Para começar, sugiro ler o meu livro: "Guia de Campo do Bom Programador", editado pela Brasport e disponível em vários formatos, inclusive e-book.

Em segundo lugar, sugiro o meu novo livro: "Qualidade de software na prática", editado pela Ciência Moderna, que deverá sair em Janeiro de 2014.

E, se você não gosta de gastar dinheiro, tem as minhas palestras gratuitas, ministradas pelo CISL - Comitè de Software Livre do Governo Federal, com vídeos gravados que você pode assistir no seu Lar:


E leia os artigos deste blog: O Bom Progamador. Tem sempre alguma coisa boa.





Nenhum comentário:

Postar um comentário