quinta-feira, 29 de maio de 2014

Boas práticas em programação [Parte 02 de 02]

Este post é continuação da [Parte 01 de 02]. Recomenda-se a prévia leitura daquele post antes de se aprofundar neste post.

Objetos e Estrutura de Dados


Esconder implementação não é apenas uma questão de adicionar uma camada de funções entre as variáveis. Esconder implementação é uma questão de abstração. Uma classe não deve expor suas variáveis por meio de métodos de acesso e mutação (getters e setters). Em vez disso, deve expor interfaces abstratas que permitem aos seus usuários a manipulação dos seus dados, sem que os usuários sejam obrigados a conhecer sua implementação.

Considere os seguintes exemplos:

public interface Vehicle {
    double getFuelTankCapacityInGallons();
    double getGallonsOfGasoline();
}

public interface Vehicle {
    double getPercentFuelRemaining();
}

O primeiro exemplo se comunica usando termos concretos, enquanto o segundo se comunica por meio da abstração da percentagem de combustível. A segunda forma é preferível, pois o ideal é não expor os detalhes dos dados. É melhor expressar a informação em termos abstratos. Isso não é alcançado pelo mero uso de interfaces acompanhadas de getters/setters. É preciso dispensar muita atenção quanto à melhor forma de representar a informação que um objeto contém. A pior escolha é simplesmente ficar adicionando getters e setters sem um critério cuidadosamente escolhido.


Assimetria entre Dados e Objeto


Objetos escondem seus dados por trás de abstrações e expõem funções que operam sobre os dados. Estrutura de dados (data structure) expõem sua informação e não possuem funções substanciais ou relevantes. O tipo clássico de estrutura de dados é uma classe com variáveis públicas e nenhuma função. Vê-se, portanto, que 'objetos' e 'estrutura de dados' são conceitos bastante opostos.

Programação procedimental (código que usa estrutura de dados) torna fácil a adição de novas funções sem que seja preciso alterar a estrutura dos dados. Mas a adição de novas estruturas de dados é difícil porque todas as funções existentes necessariamente precisarão sofrer alterações.

Já a programação orientada por objetos torna fácil a adição de novas classes sem a necessidade de alteração das funções existentes. Mas torna difícil a adição de novas funções porque muitas classes precisarão ser alteradas. Assim, tarefas difíceis para a programação orientada por objetos são fáceis na procedimental, e tarefas difíceis na procedimental são fáceis na orientada por objetos.

Em qualquer sistema complexo haverá momentos em que orientação por objetos será mais apropriada e momentos em que programação procedimental será a melhor opção. Por isso, é um mito afirmar que "tudo é um objeto".  Muitas vezes você realmente quer apenas estrutura de dados simples com linguagem procedimental operanto sobre elas. Nem sempre haverá a necessidade de adicionar uma camada orientada por objetos.

A Lei de Demeter


Objetos escondem seus dados e expõem operações. Logo, objetos não devem expor sua estrutura interna por meio de métodos de acesso (getters). A noção fundamental da Lei de Demeter é que um objeto deve saber o mínimo possível a respeito da estrutura ou propriedade de qualquer outra coisa (outras classes e inclusive subcomponentes).

Esses são os princípios da Lei de Demeter:

- Cada unidade deve ter conhecimento limitado sobre as outras unidades: somente sobre unidades intimamente relacionadas com a unidade atual.

- Cada unidade deve conversar somente com os "amigos"; não converse com estranhos.

- Somente converse com seus amigos imediatos.

A criação de estrutura híbridas, ou seja, que são metade objeto e metade estrutura de dados, são um convite para a violação da lei de Demeter. Esses tipos híbridos possuem funções que fazem coisas significativas e também variáveis públicas ou então métodos públicos que acessam e modificam variáveis privadas (que, por isso mesmo, terminam sendo acessadas como se fossem públicas). Esse tipo híbrido termina facilitando o uso de suas variáveis por funções externas da mesma forma que a programação procedimental usaria uma estrutura de dados, violando claramente a lei de Demeter. Tais tipos híbridos devem ser evitados.

Registro Ativo (Active Record)


Registros Ativos são uma forma especial de Objetos para Transferência de Dados (Data Transfer Objects). São estruturas de dados com variáveis públicas ou métodos públicos de acesso às variáveis privadas. Em geral, são traduções de tabelas de banco de dados ou de outros tipos de fontes de dados.

É muito comum desenvolvedores tratarem esse tipo de estrutura de dados como se fossem objetos inserindo métodos de regras de negócios (business rules) neles. Ocorre que isso é exatamente o que os transforma no tipo híbrido acima referido. A solução é tratar o Active Record como se fosse apenas uma estrutura de dados e criar objetos separados que contenham métodos e escondam seus dados internos (os dados internos provavelmente serão apenas instâncias do Active Record).

Manipulação de Erros


Erros são inveitáveis. Por isso, o manuseio de erros é algo importante. Porém, se a manipulação do erro obscurecer a lógica do programa, é porque está sendo feita de forma pobre ou inapropriada.

A técnica de retornar "códigos de erros" (error codes) era necessária na época em que não havia as exceções (exceptions). O problema dessa abordagem é que obriga o usuário da função a lidar com os erros de forma imediata, torando-se tarefa de fácil esquecimento, além de obscurecer a implementação do código-fonte.

Compare os dois exemplos abaixo.

public class DeviceController {
    ...
    public void sendShutDown() {
        DeviceHandle handle = getHandle(DEV1);
        // Check the state of the device
        if (handle != DeviceHandle.INVALID) {
            // Save the device status to the record field
            retrieveDeviceRecord(handle);
            // If not suspended, shut down
            if (record.getStatus() != DEVICE_SUSPENDED) {
                pauseDevice(handle);
                clearDeviceWorkQueue(handle);
                closeDevice(handle);
            } else {
                logger.log("Device suspended. Unable to shut down");
            }
        } else {
            logger.log("Invalid handle for: " + DEV1.toString());
        }
    }
    ...
}


Agora com o gerenciamento do erro por meio de exceções:

public class DeviceController {
    ...
    public void sendShutDown() {
        try {
            tryToShutDown();
        } catch (DeviceShutDownError e) {
            logger.log(e);
        }
    }

    private void tryToShutDown() throws DeviceShutDownError {
        DeviceHandle handle = getHandle(DEV1);
        DeviceRecord record = retrieveDeviceRecord(handle);
        pauseDevice(handle);
        clearDeviceWorkQueue(handle);
        closeDevice(handle);
    }

    private DeviceHandle getHandle(DeviceID id) {
        ...
        throw new DeviceShutDownError(
            "Invalid handle for: " + id.toString()
        );
        ...
    }
    ...
}

Perceba como no segundo caso tudo ficou muito mais claro. Os algorítimos para o desligamento do dispositivo e a manipulação do erro ficaram separados.

Não retorne Null


Se você cair na tentação de retornar null, pare e pense se não seria melhor lançar uma exceção (thrown exception) ou então retornar um objeto do tipo Special Case.

O problema de trabalhar com funções que retornam null é que você tem que lembrar de lidar com esse retorno toda vez que ele for possível, o que leva à duplicação de código e torna sua programação propensa a erros, pois em diversos momentos você terminará esquecendo de lidar com o null como valor de retorno.

Se você estiver trabalhando com uma API que possui retorno null, o melhor é encapsular esses métodos com outros métodos que lancem exceções ou, ainda, que retornem objetos de casos especiais (Special Case Objects).

Não passe Null


Da mesma forma que não é bom retornar null, não é boa ideia passar null como argumentos de funções. Essa prática em geral está associada com funções que possuem diversos argumentos e eles devem ser fornecidos na ordem em que foram declarados. Assim, para pular um ou algum dos argumentos, passa-se o null. A prática é ruim porque obriga a que função seja escrita de forma a verificar e testar argumento por argumento. Não é eficiente e propicia esquecimentos e erros. O melhor é seguir a regra de que as funções devem ter nenhum, um, ou, quando muito, dois argumentos, de forma que não seja necessário "pular" argumentos declarados nem ficar verificando argumentos e o tipo deles.

Classes


Classes devem ser pequenas!


Tal como afirmado em relação às funções, as classes também devem ser pequenas. Porém, não se deve medir o tamanho de uma classe em linhas, mas sim em responsabilidades.


O primeiro indicativo é o nome da Classe. Se não conseguir encontrar um nome conciso para a classe, que expresse sua responsabilidade, é porque provavelmente a classe é grande demais. Quanto mais ambíguo o nome da classe, maior a chance de ela apresentar muitas responsabilidades. Por exemplo, classes que levam os nomes Processor ou Manager ou Super em geral apontam o indevido acúmulo de muitas responsabilidades.

O Princípio da Responsabilidade Única (Single Responsibility Principle)


O princípio da responsabilidade única estabelece que uma classe ou módulo deve ter uma, e apenas uma, razão para mudar. O princípio fornece tanto a definição de responsabilidade como um guia para o tamanho da classe. Classes devem ter apenas uma responsabilidade - uma razão para mudar.

Muitos programadores temem que um grande números de classes pequenas tornará o sistema difícil de ser entendido. No entanto, um sistema com diversas classes pequenas não tem mais partes mutáveis do que um sistema com poucas porém enormes classes. O sistema deve ser composto, portanto, de diversas classes pequenas. Cada classe deve ter apenas uma responsabilidade encapsulada, ou seja, deve possuir apenas uma razão para ser alterada, e deve colaborar com algumas outras classes para que o sistema alcance a finalidade desejada.

Coesão


Classes devem ter número pequeno de variáveis (instance variables). Cada um dos métodos de uma classe deve manipular uma ou algumas dessas variáveis. Em geral, quanto mais variáveis um método manipular, mais coeso esse método será em relação à classe. Porém, nem sempre será possível ou mesmo aconselhável que uma classe seja criada com tal nível de coesão. Mas, de qualquer forma, é desejável que o nível de coesão seja alto.

Atenção: a estratégia de manter as funções pequenas e a lista de parâmetros pequena ou até inexistente pode levar a uma proliferação de variáveis da classe que podem terminar sendo usadas apenas por um subgrupo de métodos da classe. Quando isso acontecer, provavelmente significa que pelo menos uma nova classe está tentando sair da classe maior. Você deve tentar separar as variáveis e métodos em duas ou mais classes, de modo que a nova classe seja mais coesa.

Manter coesão resulta em diversas classes pequenas


O mero ato de quebrar funções grandes em funções menores, por si só, já leva a uma proliferação de classes. Imagine uma função extensa com diversas variáveis declaradas dentro da função. Digamos que você queira extrair uma pequena parte dessa função para uma função separada. No entanto, o código que você deseja separar usa quatro das variáveis declaradas na função. Vem a pergunta: você deverá passar essas variáveis para a função extraída por meio de argumentos?

Com certeza não!

Se essas quatro variáveis forem promovidas a variáveis da classe (instance variables), você poderá extrair o código sem ter que passar nenhuma variável como argumento.

No entanto, por vezes isso pode ocasionar um acúmulo de mais e mais variáveis da classe que existem somente para permitir que algumas funções as compartilhem, e não que sejam necessariamente usadas pela maioria das funções da classe. Conforme acima exposto, esse acúmulo de variáveis não usadas pela maioria dos métodos da classe significa falta de coesão. Mas pare e pense: se há algumas funções que precisam compartilhar determinadas variáveis, então isso significa que tais funções e tais variáveis já merecem mesmo uma classe somente para elas. É o princípo da coesão em ação.

Logo, se uma classe começar a perder a coesão em razão da separação das funções em funções mais curtas, separe a própria classe em tantas classes quanto forem necessárias para que cada uma delas mantenha sua própria coesão.

Organizando para a Alteração


Em todos os sistemas as mudanças podem ser contínuas. Só que toda mudança causa o risco de o restante do código não funcionar mais. O programador então, resite em fazer alterações. Por isso, em um sistema limpo (ou "clean"), as classes devem ser organizadas de forma a reduzir os riscos decorrentes de mudanças.

A classe Sql a seguir é usada para gerar comandos SQL apropriados a partir de metadados que forem fornecidos. Para os fins do exemplo, ainda não se trata de classe completa. Falta-lhe, por exemplo, o suporte para comandos do tipo update. Quando chegar a hora de implementar tais métodos, teremos que abrir a classe para as mudanças. O problema é que ao abrir a classe introduzimos o risco próprio da mudança, qual seja, quebrar outras partes do código.

public class Sql {
    public Sql(String table, Column[] columns)
    public String create()
    public String insert(Object[] fields)
    public String selectAll()
    public String findByKey(String keyColumn, String keyValue)
    public String select(Column column, String pattern)
    public String select(Criteria criteria)
    public String preparedInsert()
    private String columnList(Column[] columns)
    private String valuesList(
        Object[] fields, final Column[] columns
    )
    private String selectWithCriteria(String criteria)
    private String placeholderList(Column[] columns)
}

A classe Sql acima deverá sofrer alterações sempre que adicionarmos um novo tipo de comando. Deverá mudar também quando alterarmos os detalhes de um único tipo de comando, como por exemplo se precisarmos alterar a funcionalidade select para suportar subselects. Essas duas razões para mudar indicam que a classe Sql viola o princípio da responsabilidade única (single responsibility principle).

Essa violação pode ser facilmente percebida até mesmo pela forma como a classe foi organizada. Existem métodos privados, como por exemplo o selectWithCriteria, que visivelmente somente se relaciona com os comandos do tipo select. Métodos privados que se relacionam somente com pequeno subgrupo de funções da classe são bons indicativos da necessidade de melhoria no design dessa classe.

Se não houver a menor necessidade de atualizar a classe (ou "abrir a classe" para mudança), então em princípipo ela poderia ser deixada como está. Mas a partir do momento que você se pegar "abrindo a classe" para mudança, deverá considerar a possibilidade de melhorar o design dela.

Imagine a solução a seguir ilustrada. Nela, cada uma das interfaces públicas dos métodos definidos na classe anterior passou a integrar cada uma sua própria classe derivada da classe Sql. E os métodos privados, tais como valuesList, são movidos diretamente para onde são necessários. E o comportamento privado comum foi isolado em um par de classes utilitárias, Where e ColumnList.

abstract public class Sql {
    public Sql(String table, Column[] columns)
    abstract public String generate();
}

public class CreateSql extends Sql {
    public CreateSql(String table, Column[] columns)
    @Override public String generate()
}

public class SelectSql extends Sql {
    public SelectSql(String table, Column[] columns)
    @Override public String generate()
}

public class InsertSql extends Sql {
    public InsertSql(String table, Column[] columns, Object[] fields)
    @Override public String generate()
    private String valuesList(
        Object[] fields, final Column[] columns
    )
}

public class SelectWithCriteriaSql extends Sql {
    public SelectWithCriteriaSql(
        String table, Column[] columns, Criteria criteria
    )
    @Override public String generate()
}

public class SelectWithMatchSql extends Sql {
    public SelectWithMatchSql(
        String table, Column[] columns, Column column, String pattern
    )
    @Override public String generate()
}

public class FindByKeySql extends Sql {
    public FindByKeySql(
        String table, Column[] columns,
        String keyColumn, String keyValue
    )
    @Override public String generate()
}

public class PreparedInsertSql extends Sql {
    public PreparedInsertSql(String table, Column[] columns)
    @Override public String generate() {
        private String placeholderList(Column[] columns)
    }
}

public class Where {
    public Where(String criteria)
    public String generate()
}

public class ColumnList {
    public ColumnList(Column[] columns)
    public String generate()
}

Assim, o código em cada classe torna-se extremamente simples. O risco de uma função quebrar outra ficou reduzido a praticamente zero. Além disso, o isolamento de cada classe tornou a produção de testes tarefa fácil (Test Driven Development).

E quando chegar a hora de adicionar o comando update, nenhuma das classes existentes precisará ser alterada. A lógica para criar comandos update será escrita em nova subclasse da classe Sql chamada UpdateSql. Nenhum outro código no sistema será quebrado por conta desse acréscimo.

Assim, a lógica reestruturada representa o melhor dos mundos. Suporta o princípio da responsabilidade única e também suporta outro fator chave do desenvolvimento orientado por objetos, que é o "Princípio Aberto-fechado" (Open-Closed Principle). Classes devem estar abertas a extensões mas fechadas para modificações. A classe Sql reestruturada está aberta para permitir nova funcionalidade por meio de subclasses, mas isso pode ser feito mantendo-se as demais classes fechadas. Simplesmente colocaremos a nova classe UpdateSql no lugar dela.

Em um sistema ideal, incorporamos novas funcionalidades estendendo o sistema e não fazendo modificações no código existente.

Isolando das Mudanças


Classes concretas implementam detalhes (código) e classes abstratas representam apenas conceitos. Uma classe cliente que dependa de detalhes concretos está em perigo quando esses detalhes mudarem. Por isso, interfaces e classes abstratas podem ser introduzidas para ajudar a ilosar o impacto desses detalhes.

A dependência de detalhes concretos torna complicados os testes do sistema. Imagine a classe Portfolio que dependa da API externa TokyoStockExchange para obter o valor do portfolio. Testar a classe vai ser extremamente difícil por conta da mutabilidade do valor retornado. A cada cinco minutos a resposta será diferente.

Em vez de definir a classe Portfolio de forma que ela dependa diretamente de TokyoStockExchange, podemos criar uma interface, SotckExchange, que declara um único método:

public interface StockExchange {
    Money currentPrice(String symbol);
}

Então definimos TokyoStockExchange como implementação dessa interface. Também nos certificamos de que o construtor de Portfolio receba como argumento uma referência do tipo StockExchange:

public Portfolio {
    private StockExchange exchange;
    public Portfolio(StockExchange exchange) {
        this.exchange = exchange;
    }
    // ...
}

Agora poderá ser criada uma implementação passível de teste da interface SotckExchange que emula a TokyoStockExchange. Essa implementação de teste irá ajustar o valor atual para qualquer símbolo (variável symbol no exemplo) que usemos no teste. Se nosso teste demonstrar a compra de cinco ações da empresa imaginária MSFT para o nosso portfolio, criamos a implementação do teste para sempre retornar $100 por ação dessa empresa. Nossa implementação teste da interface StockExchange fica reduzida a uma simples busca de tabela. Podemos então escrever um teste que espera o total de $500 para nosso valor do portfolio quando são adicionadas 5 ações.

public class PortfolioTest {
    private FixedStockExchangeStub exchange;
    private Portfolio portfolio;

    @Before
    protected void setUp() throws Exception {
        exchange = new FixedStockExchangeStub();
        exchange.fix("MSFT", 100);
        portfolio = new Portfolio(exchange);
    }

    @Test
    public void GivenFiveMSFTTotalShouldBe500() throws Exception {
        portfolio.add(5, "MSFT");
        Assert.assertEquals(500, portfolio.value());
    }
}
Se um sistema for separado o suficiente para ser testado dessa forma, será também mais flexível para promover o reuso de código. Assim, os elementos ficam isolados um dos outros e isolados de mudanças. Essa classe então atenderá a outro princípio de elaboração de classes conhecido como Inversão de Dependência (Dependency Inversion Principle - DIP). Em resumo, esse princípio estabelece que as classes devem depender de abstrações e não de detalhes concretos.

Em vez de depender da implementação de detalhes da classe TokyoStockExchange, nossa classe Portfolio agora depende da interface StockExchange. Essa interface representa o conceito abstrato de perguntar qual é o preço corrente de um símbolo ('symbol' no exemplo). Essa abstração isola todos os detalhes específicos para se obter tal preço, incluindo de onde e de que forma esse preço pode ser obtido.

Há muito mais


Nem de longe o resumo contido nesses posts esgota o tema das boas práticas em programação.

Em posts futuros procurarei resumir mais conceitos e práticas interessantes.

De qualquer forma, há muito material disponível para pesquisa e sem dúvida o programador profissional deve se aprimorar constantemente para escrever o desejado "clean code".

terça-feira, 27 de maio de 2014

Boas práticas em programação [Parte 01 de 02]

Um dos erros mais comuns em programação é escrever o código-fonte somente para si mesmo.

No momento em que o programador estiver escrevendo o programa, provavelmente ele não perceberá que a adoção de texto pouco legível e enigmático, por mais inteligente e astuto que possa inicialmente parecer, terminará prejudicando a ele mesmo, ainda que o código não seja destinado a ser usado ou lido por outras pessoas.

Outra prática ruim é a pressa no momento da escrita. Supõe-se ganhar tempo ao usar nomes curtos e de pouca significação, quando na verdade o tempo gasto para escolher nomes significativos poupará muito mais tempo posteriormente.

Isso porque a maioria esmagadora do tempo gasto em programação se faz exatamente com leitura de código-fonte. Pare para pensar a quantidade de tempo que você gasta rolando o texto para cima e para baixo, consultando classes e métodos e lendo comentários em comparação ao tempo que você gasta exclusivamente escrevendo. A verdade é inescapável: o programador lê constantemente código já escrito como parte do esforço para escrever código novo. Estima-se que a proporção de leitura e escrita durante a programação seja de 10:1.

Por isso, fazer com que o código-fonte seja fácil de ler na verdade o torna, ao mesmo tempo, fácil de ser escrito. Escrever código bem organizado, limpo, com o uso de nomes significativos para funções, variáveis e classes não é apenas recomendação de estilo, mas sim boa prática de qualquer programador com pretensões sérias ou profissionais.



Com base nessas premissas, o programador e autor Robert C. Martin escreveu o livro "Clean Code - A Handbook of Agile Software Craftsmanship", cuja leitura é altamente recomendada para qualquer programador, não importa a linguagem que se pretenda trabalhar. Os exemplos do livro estão em Java, mas aplicam-se a toda e qualquer linguagem orientada por objetos.

A seguir, tentei resumir algumas das principais dicas que pude extrair do livro, por vezes acrescentando alguns exemplos e comentários meus. Em postagens futuras acrescentarei mais dicas e exemplos.

Nomes significativos


Use nomes que revelam intenção


Escolher nomes bons toma tempo, mas economiza mais tempo do que toma. O nome (e não comentários) de uma variável, função ou classe deve responder todas as grandes questões sobre sua intenção.

Por exemplo, em vez de usar:

int d; // elapsed time in days

Use:

int elapsedTimeInDays;

Não há nenhum problema com nomes grandes, desde que sejam auto explicativos. As IDEs de hoje em dia (Eclipse, Netbeans, Aptana etc.) e seus recursos de auto-completar tornam a utilização de nomes grandes e explicativos totalmente viável.

Faça distinções significativas


De que adianta criar variáveis cuja significação não pode ser diferenciada uma da outra? Usar productData e productInfo em uma mesma classe ou em um mesmo contexto com certeza será motivo de confusão tanto para o autor do código como, pior ainda, para quem tiver que lidar com ele.

Use nomes pronunciáveis


Humanos são bons com palavras. Evite nomes impronunciáveis como genymdhms (generation date, year, month, day, hour, minute and second).  Muito mais adequado seria generationTimestamp. Este é pronunciável e aquele não.

Inglês, por favor!


Desnecessário dizer que é de todo recomendável, e às vezes até mesmo essencial, especialmente se você estiver trabalhando em código aberto ou opensource, que todos os nomes, expressões e comentários sejam escritos em inglês. Sim, primeiro porque a sintaxe da própria linguagem de programação já está em inglês e será muito confuso para qualquer pessoa ler código que está metade em inglês e metade na língua do autor do código. Pior ainda é misturar as duas línguas dentro de uma mesma função ou classe. Segundo porque, goste ou não, inglês é linguagem universal para programação (e para computadores em geral), de modo que a única forma de garantir que seu código será compreendido pelo resto de mundo será escrevê-lo em inglês.

Evite mapeamento mental


Às vezes vemos variáveis nomeadas com uma letra só. Sem dúvida são perfeitamente aceitáveis em um loop (i é a mais comum), mas desde que o loop seja pequeno e não haja conflitos. Em outros contextos, as variáveis de uma só letra certamente serão problemáticas, pois o leitor terá que fazer mapeamento mental daquela letra para o seu real significado. Com o passar do tempo, nem o próprio autor do código lembrará qual o verdadeiro conceito daquela variável de uma letra só.

Eis exemplo de função em JavaScript que é excelente em seu desiderato, porém praticamente impossível de ser examinada ou alterada sem muito tempo gasto com observação e teste. A função converte números para o formato dinheiro (string). Recebe como parâmetros os caracteres para milhar (t, de thousand) e decimal (d, de decimal) e a quantidade de casas decimais (c, de cases):

Number.prototype.toMoney = function(t, d, c) {
    var n = this,
    c = isNaN(c = Math.abs(c)) ? 2 : c,
    d = (d === undefined) ? "." : d,
    t = (t === undefined) ? "," : t,
    s = n < 0 ? "-" : "",
    i = parseInt(n = Math.abs(+n || 0).toFixed(c)) + "",
    j = (j = i.length) > 3 ? j % 3 : 0;
    return
        s + (j ? i.substr(0, j) + t : "") +
        i.substr(j).replace(/(\d{3})(?=\d)/g, "$1" + t) +
        (c ? d + Math.abs(n - i).toFixed(c).slice(2) : "");
};

Como se vê, qualquer alteração será extremamente penosa, especialmente porque foram usadas expressões regulares (regular expressions). Tivesse o autor dessa função usado nomes significativos para as variáveis e evitado mapeamentos mentais, a função seria muito mais proveitosa e fácil de ser atualizada e compreendida em sua implementação.

A diferença entre o programador esperto e o programador profissional é que o último entende que clareza é a rainha. Profissionais escrevem códigos que os outros conseguem ler e entender sem maiores esforços.

Nomes de Classes


Classes e Objetos devem ter nomes com substantivos como por exemplo Customer, WikiPage e Account. Se for usar um verbo, associe-o a um substantivo, como por exemplo AddressParser. Evite usar somente verbos, como por exemplo Manager ou Processor, principalmente porque verbos devem ser usados para métodos ou funções.

Nomes de Métodos


Métodos (ou funções) devem ser nomeadas com verbos, como por exemplo postPayment, deletePage ou save. Métodos que acessam, mudam ou predicam devem ser nomeados de acordo com o valor deles e prefixados com os verbos get, set ou is, como por exemplo getName, isPosted ou setColor.

Escolha uma palavra por conceito (coerência)


Escolha uma palavra para cada conceito abstrato e permaneça com ela até o fim. Não mude a palavra no meio do caminho. Por exemplo, é confuso usar palavras diferentes, porém de significado semelhante, para métodos equivalentes de classes diferentes, como por exemplo fetch, retrieve e get. Como você lembrará qual método pertence a qual classe?

Da mesma forma, é confuso ter palavras como controller, manager e driver na mesma base de código. Qual a diferença essencial, em termos de conceito, entre essas palavras? A diferença somente será descoberta se for feita a busca no código, o que tomará muito tempo de rolamento e navegação no texto.

Adicione contexto significativo


Muitas vezes você terá que colocar nomes dentro de contextos. Para tanto, deverá inseri-los em classes bem nomeadas, funções ou namespaces. Imagine as variáveis firstName, lastName, street, city, state etc. Quando juntas, fica muito claro que se referem a uma lista de endereços. Mas se você se deparar com state apenas, não concluirá automaticamente que se refere a um endereço postal. Nesse caso, a melhor opção é criar uma classe chamada Address e encapsular essas variáveis nessa classe, pois assim passarão a ser referenciadas por Address.state, Address.firstName e assim por diante, adicionando-se, dessa forma, contexto significativo às variáveis.

Funções


Pequenas!


A primeira e mais importe regra é que funções devem ser pequenas. A segunda regra é que devem ser menores ainda. Funções pequenas são fáceis de entender e facilitam o cumprimento do princípio que será abaixo descrito ("faça apenas uma coisa").

O tamanho pequeno da função implica blocos if, else e loops também pequenos. Na verdade, quanto mais perto de uma linha apenas forem os blocos, melhor será. Se preciso, o ideal é separar o código que viria dentro do bloco em outra função, preferencialmente como um nome bem descritivo para essa função, de forma a auto-documentar o código sem adicionar qualquer comentário, usando apenas o nome da função.

Faça uma coisa (apenas)


A regra de ouro é: "Funções devem fazer uma coisa. Devem fazê-la bem. Devem fazer somente essa coisa." O problema dessa regra é identificar o que seria "fazer apenas uma coisa". Em geral, essa regra deve ser entendida como a possibilidade de descer apenas um nível de abstração em relação ao verbo contido no nome da função (afinal, funções são escritas exatamente para decompor conceitos maiores em sequência de passos no próximo nível de abstração).

Para simplificar, o melhor jeito de saber se a função está fazendo "mais do que uma coisa" é verificar se é possível extrair outra função de dentro dela, porém com um nome que não seja mera repetição da implementação da função. Se isso for possível, então a função estará violando a regra "faça uma coisa (apenas)".

Isso leva ao princípio seguinte.

Um nível de abstração por função


Para se certificar que a função está fazendo "uma coisa (apenas)", deve ser verificado se as expressões contidas na função estão no mesmo nível de abstração. Se a função misturar conceitos de nível alto, intermediário e baixo de abstração, haverá confusão e o princípio estará sendo violado. Os leitores não conseguirão dizer se uma expressão em particular é um conceito essencial ou apenas um detalhe.

Leitura de código de cima para baixo: A regra de descer


O código deve poder ser lido como uma narrativa de cima para baixo. Isso significa que toda função deve ser seguida pela função do próximo nível de abstração, de forma que o código possa ser lido um nível de abstração por vez, de cima para baixo. Ajuda se pensarmos em termos de pequenas frases iniciadas por TO:

TO create a user, we generate an id number.
   TO generate an id number, we run the ramdom number generator.
      TO run the random number generator,
          we call a low level function.
No exemplo, cada frase iniciada por TO representa uma função diferente. É como se existisse um 'to' imaginário antes de cada nome de função, para nos ajudar a implementar o próximo nível de abstração:

public User (TO) createUser() {...}

Use nomes descritivos


Você sabe que está trabalhando com código de qualidade quando cada rotina faz exatamente aquilo que você espera. Quanto menor e mais focada a função, mais fácil será a escolha de um nome descritivo o suficiente. Não tenha medo de escolher nomes grandes. Nome grande e bem descritivo é melhor do que nome pequeno e enigmático.

Argumentos de funções


O número ideal de argumentos para uma função é zero. Depois um, seguido por dois. Três argumentos devem ser evitados sempre que possível. Mais do que três argumentos não são recomendados e precisam de justificativa realmente muito importante para serem usados.

Além disso, argumentos de saída devem ser evitados. Quando a função é lida, a tendência é interpretá-la como informação entrando na função por meio dos argumentos e saindo por meio do valor de retorno (return value). Em geral, não se espera que informação saia da função por meio dos argumentos.

Há duas boas razões para usar argumento único em uma função. Você pode estar fazendo uma pergunta sobe o argumento, como em boolean fileExists('myfile.txt'). Ou você pode estar operando sobre esse argumento, transformando-o em outra coisa e devolvendo o argumento alterado. Por exemplo: User getUserById(345).

É boa prática fazer da função e argumento, sempre que possível, um par verbo/substantivo, como por exemplo writeField(name).

Argumentos do tipo true ou false devem ser evitados, pois claramente indicam que a função vai fazer mais de uma coisa. Se o argumento for verdadeiro, terá um destino, se for falso, terá outro. O melhor seria criar uma função para cada caminho possível. Por exemplo, em vez de render(true), que por si só não revela nenhuma informação sobre o que seria a natureza do argumento, melhor seria criar duas funções separadas, como renderActiveFields() e renderInactiveFields(), em vez de deixar que o argumento leve a um resultado ou outro.

Use argumento duplo, quando inevitável, somente se entre os parâmetros houver estreita relação, como por exemplo range(1, 10). Fica evidente que você quer o intervalo entre 1 e 10. Seria estranho imaginar a função como range(1). Que intervalo isso representaria? Em casos como esse, é perfeitamente válido usar os dois argumentos. Mas por exemplo assertEquals(expected, actual) obriga o leitor a consultar a declaração da função, sob pena de confundir a ordem dos argumentos. Se for inevitável o uso de dois argumentos, o melhor é denunciar a ordem deles no nome da função, como por exemplo assertExpectedEqualsActual(expected, actual).

Quando uma função parecer precisar de mais de dois ou três argumentos, provavelmente alguns desses argumentos devem ser embrulhados em classes próprias, de forma que você passará objetos como argumentos. Reduzir o número de argumentos dessa forma pode parecer um atalho, mas não é. Quando variáveis são passadas em grupos, provavelmente elas já fazem mesmo parte de um conceito único, de forma que merecem fazer parte de uma classe única (só para elas).

Não use argumentos de saída


Argumentos são naturalmente intepretados como "entrada" de uma função. Na maioria das vezes, quando o argumento for "saída", você provavelmente terá que consultar a declaração da função para entender o que está se passando com ela.

Imagine o exemplo:

appendFooter(s);

Essa função acrescenta 's' como rodapé de algo? Ou acrescenta algum rodapé a 's'? O argumento 's' é uma entrada ou saída?

Em programação orientada por objetos a necessidade de usar argumentos de saída é praticamente inexistente porque a variável especial this é destinada a servir como argumento de saída. Em outras palavras, no exemplo acima o melhor seria a função ser invocada da seguinte forma:

report.appendFooter();

Ou seja, se uma função tiver que alterar o estado de algo, faça com que mude o estado do objeto que a contém.

Separação entre comando e consulta


Funções devem fazer algo ou responder algo, nunca os dois. Ou a função deve alterar o estado de um objeto ou deve retornar alguma informação sobre o objeto. Fazer as duas coisas em geral leva a confusões.

Considere a seguinte função:

public boolean set(String attribute, String value);

A função define o valor de um atributo e retorna true se houver sucesso e false se o atributo não existir.

O problema é que essa dupla finalidade (um comando e uma consulta na mesma função) gera ambiguidades do seguinte tipo. Imagine essa expressão:

if (set('username', 'bob')) { ... }

Quem estiver lendo não saberá se o teste está verificando se o username estava anteiormente definido como bob e, nesse caso, o teste será verdadeiro, ou se o resultado da definição do atributo username como bob é verdadeiro. Para evitar a ambiguidade, o melhor é separar o comando da consulta:

if (attributeExists('username')) {
    setAttribute('username', 'bob');
}

Prefira exceções a retornos de códigos de erro


Retornar diveros códigos de erro (error codes) em funções implica criação de subestruturas profundas e difícies de lidar. Além disso, se você criar um retorno de código de erro, o usuário que chamar a função terá que lidar com o erro imediatamente. Por outro lado, se você usar exceções (exceptions), o processamento do código de erro poderá ser feito separadamente:
try {
    deletePage(page);
    registry.deleteReference(page.name);
    configKeys.deleteKey(page.name.makeKey());
} catch (Exception e) {
    logger.log(e.getMessage());
}

Extraia blocos Try/Catch


Blocos try/catch já são feios por natureza. Eles misturam processamento normal com processamento de erros. O melhor é extrair o corpo do bloco try/catch em função própria.
public void delete(Page page) {
    try {
        deletePageAndAllReferences(page);
    } catch (Exception e) {
        logError(e);
    }
}

private void deletePageAndAllReferences(Page page) throws Exception {
    deletePage(page);
    registry.deleteReference(page.name);
    configKeys.deleteKey(page.name.makeKey());
}

private void logError(Exception e) {
    logger.log(e.getMessage());
}
Nesse exemplo, o bloco try/catch foi isolado na função delete. A função deletePageAndAllReferences trata somente do processamento para deletar. Essa seperação é importante para tornar o código mais fácil de ser lido e modificado.

Lidar com erro já é UMA coisa


Funções devem fazer apenas uma coisa. Gerenciar o erro já é uma coisa. Logo, funções que lidam com erro não devem fazer nada além disso. Assim, se a expressão try exsitir em uma função, deve ser a primeira linha dele e após o bloco não deve haver mais nada na função.


Não se repita (Don't Repeat Yourself)


É o princípio rei da programação. Muitas das técnicas e até mesmo toda a base da programação orientada por objeto tem como finalidade eliminar a repetição do código-fonte. A redundância ou duplicação de código é a prática mais maléfica que há na programação. Deve ser evitada a todo custo.


Comentários


Comentários, quando muito, são um mal necessário. Se as linguagens de programação fossem expressivas o suficiente ou se nós tivéssemos talento o suficiente para usar a linguagem de programação a fim de expressar nossas intenções, não precisaríamos de comentários no código-fonte.

O uso correto de comentários é apenas aquele que compensa a nossa incapacidade de nos expressarmos na forma de código. Logo, se você sentir a necessidade de escrever um comentário, pense duas vezes se não é possível melhorar o código de forma que a lingugem de programação exprima aquilo que seria escrito no comentário, de modo que ele não seja preciso.

Em geral, comentários mentem. Aliás, quanto mais antigos e não atualizados, mais desinformação e perda de tempo eles irão gerar. Assim, comentários imprecisos são muito pior do que nenhum comentário. Somente o código pode dizer o que ele realmente faz. Expresse-me por meio do código e não por meio de comentários.

As exceções que em geral admitem comentários são: comentários legais ou a respeito de licenças, comentários que revelam a intenção por trás de uma decisão de design, comentários que avisam programadores a respeito de consequências importantes ou específicas, comentários do tipo TODO (por fazer) - já que o destino deles é efetivamente desaparecer -, comentários que destacam a importância de algo que normalmente passaria por trivial e, obviamente, comentários a respeito da API pública (Javadoc, phpdoc etc.).

Veja no próximo post


Na próxima parte são analisados objetos e estrutudas de dados, manipulação de erros e o design de classes.


CLIQUE AQUI PARA VER A [PARTE 02 de 02] DAS BOAS PRÁTICAS.