DEV Community 👩‍💻👨‍💻

Jonilson Sousa
Jonilson Sousa

Posted on

Anotações Capítulo 3: Functions

  • As funções permaneceram durante a evolução da programação;

  • O que torna uma função fácil de ler e entender?

Pequenas!

  • “A primeira regra para funções é que elas devem ser pequenas. A segunda é que precisam ser mais espertas do que isso”.
  • Funções devem ter no máximo 20 linhas;
  • Kent Beck escreveu um programa (Java/Swing) que ele chamava de “Sparkle” e cada função desse programa tinha duas, ou três, ou quatro linhas e essa deve ser o tamanho das nossas funções.

Blocos e Indentação

  • Os blocos dentro de if, else, while e outros devem ter apenas uma linha. Possivelmente uma chamada de função.
  • O nível de indentação de uma função deve ser de, no máximo, um ou dois níveis.
  • Facilita a leitura de compreensão das funções.

Faça uma Coisa

  • “AS FUNÇÕES DEVEM FAZER UMA COISA. DEVEM FAZÊ-LA BEM. DEVEM FAZER APENAS ELA”.
  • Uma forma de saber se uma função faz mais de “uma coisa” é se você pode extrair outra função dela a partir de seu nome que não seja apenas uma reformulação de sua implementação.

Seções Dentro de Funções

  • Se temos seções, como declarações, inicializações, é um sinal de está fazendo mais de uma coisa;
  • Não dá para dividir em seções as funções que fazem apenas uma coisa.

Um Nível de Abstração por Função

  • Vários níveis dentro de uma função sempre geram confusão.
  • Leitores podem não conseguir dizer se uma expressão determinada é um conceito essencial ou um mero detalhe.

Ler o Código de Cima para Baixo: Regra Decrescente

  • Queremos que o código seja lido de cima para baixo, como uma narrativa.
  • Regra Decrescente: Cada função seja seguida pelas outras no próximo nível de modo que possamos ler o programa descendo um nível de cada vez conforme percorremos a lista de funções.
  • Acaba sendo muito difícil para programadores aprenderem a seguir essa regra e criar funções que fiquem em apenas um nível.
  • É muito importante aprender esse truque, pois ele é o segredo para manter funções curtas e garantir que façam apenas “uma coisa”.
  • Fazer com que a leitura do código possa ser feita de cima para baixo como uma série de parágrafos ”TO” é uma técnica eficiente para manter o nível consistente.

Estrutura Switch

  • É difícil criar uma estrutura switch pequena;
  • Também é difícil criar uma que faça apenas uma coisa.
  • Por padrão, os switch sempre fazem muitas coisas.
  • Mas podemos nos certificar se cada um está em uma classe de baixo nível e nunca é repetido, usando o polimorfismo.
  • A seguinte função mostra apenas uma das operações que podem depender do tipo de funcionário:
public Money calculatePay(Employee e) throws InvalidEmployeeType {
    switch (e.type) {
        case COMMISSIONED:
            return calculateCommissionedPay(e);
        case HOURLY:
            return calculateHourlyPay(e);
        case SALARIED:
            return calculateSalariedPay(e);
        default:
            throw new InvalidEmployeeType(e.type);
    }
}
Enter fullscreen mode Exit fullscreen mode
  • Essa função acima tem vários problemas:
    • Primeiro: Ela é grande, e quando adicionarmos novos tipos de funcionários ela crescerá mais ainda;
    • Segundo: Ela faz mais de uma coisa;
    • Terceiro: Ela viola o Princípio da Responsabilidade Única (SRP - SOLID) por haver mais de um motivo para alterá-la;
    • Quarto: Ela viola o Princípio de Aberto-Fechado (OCP - SOLID), porque ela precisa ser modificada sempre que novos tipos forem adicionados;
    • E possivelmente o pior problema é a quantidade ilimitada de outras funções que terão a mesma estrutura, por exemplo:
isPayday(Employee e, Date date),
Enter fullscreen mode Exit fullscreen mode

Ou

deliverPay(Employee e, Money pay),
Enter fullscreen mode Exit fullscreen mode
  • A solução nesse caso é inserir uma estrutura switch no fundo de uma ABSTRACT FACTORY:
    • Assim, a factory usará o switch para criar instâncias apropriadas derivadas de Employee;
    • As funções: calculatePay, isPayday e deliverPay, serão enviadas de forma polifórmica através da interface Employee.
  • A regra geral para estruturas switch é que são aceitáveis se aparecerem apenas uma vez para a criação de objetos polimórficos, e se estiverem escondidas atrás de uma relação de herança de modo que o resto do sistema não possa enxergá-la. Mas cada caso é um caso e podem haver casos de não respeitar todas essas regras.
  • Assim temos como solução o seguinte código:
public abstract class Employee {
    public abstract boolean isPayday();
    public abstract Money calculatePay();
    public abstract void deliverPay(Money pay);
}
-----------------
public interface EmployeeFactory {
    public Employee makeEmployee(EmployeeRecord r) throws InvalidEmployeeType;
}
-----------------
public class EmployeeFactoryImpl implements EmployeeFactory {
    public Employee makeEmployee(EmployeeRecord r) throws InvalidEmployeeType {
        switch (r.type) {
            case COMMISSIONED:
                return new CommissionedEmployee(r) ;
            case HOURLY:
                return new HourlyEmployee(r);
            case SALARIED:
                return new SalariedEmploye(r);
            default:
                throw new InvalidEmployeeType(r.type);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Use Nomes Descritivos

  • É muito importante ter bons nomes;
  • Lembre-se do princípio de Ward: “Você sabe que está criando um código limpo quando cada rotina que você lê é como você esperava”;
    • Metade do esforço para satisfazer esse princípio é escolher bons nomes para funções pequenas que fazem apenas uma coisa.
    • Quanto menor e mais centralizada é a função, mais fácil é pensar em um nome descritivo.
  • Não tenha medo de criar nomes extensos, pois eles são melhores do que um pequeno e enigmático. Um nome longo e descritivo é melhor do que um comentário extenso e descritivo.
  • Experimente diversos nomes até encontrar um que seja bem descritivo.
  • Seja consistente nos nomes. Use as mesmas frases, substantivos e verbos nos nomes de funções de seu módulo.
  • Exemplos:
    • includeSetup-AndTeardownPages, includeSetupPages, includeSuiteSetupPage , e includeSetupPage.

Parâmetros de Funções

  • A quantidade ideal de parâmetros para uma função é zero. Depois vem um, seguido de dois. Sempre que possível devem-se evitar três parâmetros. Para mais de três deve-se ter um motivo muito especial, mesmo assim não devem ser usados.
  • Parâmetros são complicados. Eles requerem bastante conceito.
  • Os parâmetros são mais difíceis ainda a partir de um ponto de vista de testes:
    • Imagina a dificuldade de escrever todos os casos de testes para se certificar de que todas as várias combinações de parâmetros funcionem adequadamente.;
    • Se não houver parâmetros, essa tarefa é simples;
    • Se houver um, não é tão difícil assim;
    • Com dois, a situação fica um pouco desafiadora. Com mais de dois, pode ser desencorajador testar cada combinação de valores apropriados.
    • Os parâmetros de saída são mais difíceis de entender do que os de entrada.
  • Por fim, um parâmetro de entrada é a melhor coisa depois de zero parâmetro!

Formas Monádicas (Um parâmetro) Comuns

  • Duas razões para se passar um único parâmetro a uma função:
    • Você pode estar fazendo uma pergunta sobre aquele parâmetro, exemplo: boolean fileExists(“MyFile”’).
    • Ou você pode trabalhar parâmetro, transformando-o em outra coisa e retornando-o, exemplo: InputStream fileOpen(“MyFile”) transforma a String do nome de um arquivo em um valor retornado por InputStream.
    • Outro uso menos comum é para uma função de evento, neste caso há um parâmetro de entrada, mas nenhum de saída. Cuidado ao usar essa abordagem!
  • Se uma função vai transformar seu parâmetro de entrada, a alteração deve aparecer como o valor retornado. Por exemplo:
StringBuffer transform(StringBuffer in)
Enter fullscreen mode Exit fullscreen mode

É melhor do que:

void transform(StringBuffer out)
Enter fullscreen mode Exit fullscreen mode

Parâmetros Lógicos

  • Esses parâmetros são feios.
  • Passar um booleano para uma função certamente é uma prática horrível, pois ele complica imediatamente a assinatura do método, mostrando explicitamente que a função faz mais de uma coisa.
  • Ela faz uma coisa se o valor for verdadeiro, e outra se for falso!

Funções Díades (Dois parâmetros)

  • “Uma função com um parâmetro é mais difícil de entender do que com um”.
  • Com dois parâmetros, é preciso aprender a ignorar um dos parâmetros, porém o local que ignoramos é justamente onde os bugs se esconderão.
  • Casos em dois parâmetros são necessários:
    • Por exemplo, uma classe com eixos cartesianos, como por exemplo, Point p = new Point(0, 0), é preciso ter os dois parâmetros;
    • Nesse caso os dois parâmetros são componentes de um único valor.
  • Mesmo funções óbvias como assertEquals(expected, actual), são problemáticas!
  • Quantas vezes já colocou actual on deveria ser expected?
  • Os dois parâmetros não possuem uma ordem pré-determinada natural;
  • A ordem expected, actual é uma convenção que requer prática para assimilá-la.
  • Funções com dois parâmetros não são ruins e vamos usá-las!
  • Mas devemos tentar converter essas funções em funções de um parâmetro, usando outro método ou variáveis de classe ou ainda outra classe que recebe o parâmetro no construtor.

Tríades (Três parâmetros)

  • São consideravelmente mais difíceis de entender do que as com dois parâmetros;
  • Pense bastante antes de criar uma tríade!
  • O processo de ordenação, pausa e ignoração apresenta mais do que o dobro de dificuldade.

Objetos como parâmetro

  • “Quando uma função parece precisar de mais de dois parâmetros, é provável que alguns desses parâmetros devam ser agrupados em uma classe própria”. Por exemplo:
Circle makeCircle(double x, double y, double radius);
Circle makeCircle(Point center, double radius);
Enter fullscreen mode Exit fullscreen mode
  • Criar objetos para reduzir o número de parâmetros pode parecer trapaça, mas não é.

Listas como parâmetro

  • Quando queremos passar um número variável de parâmetros para uma função, como por exemplo:
String.format("%s worked %.2f hours.", name, hours);
Enter fullscreen mode Exit fullscreen mode
  • Se os parâmetros forem todos tratados da mesma forma, eles serão equivalentes a um único parâmetro do tipo List.
  • Por isso, o String.format é uma função com dois parâmetros:
public String format(String format, Object... args)
Enter fullscreen mode Exit fullscreen mode

Verbos e palavras-chave

  • Escolher bons nomes para uma função pode ajudar muito a explicar a intenção da função e a ordem e a intenção dos parâmetros.
  • No caso de função com um parâmetro, a função e o parâmetro devem formar um par verbo/substantivo muito bom. Por exemplo, write(name), qualquer que seja essa coisa “name” está sendo “write” (escrito). Um nome ainda melhor poderia ser writeField(name), que indica que o “name” é um campo.
  • Por exemplo: assertEquals pode ser melhor escrito como assertExpectedEqualsActual(expected, actual), assim diminui o problema de ter que lembrar a ordem dos parâmetros.

Evite Efeitos Colaterais

  • “Efeitos colaterais são mentiras”.
  • Se a função promete fazer apenas uma coisa, mas também faz outras coisas escondidas. Vamos ter efeitos indesejáveis. Por exemplo:
public class UserValidator {
    private Cryptographer cryptographer;

    public boolean checkPassword(String userName, String password) {
        User user = UserGateway.findByName(userName);
        if (user != User.NULL) {
            String codedPhrase = user.getPhraseEncodedByPassword();
            String phrase = cryptographer.decrypt(codedPhrase, password);
            if ("Valid Password".equals(phrase)) {
                Session.initialize();
                return true;
            }
        }
        return false;
    }
}
Enter fullscreen mode Exit fullscreen mode

Essa função de verificar a senha, tem um efeito colateral, que é a chamada do Session.initialize(), o nome checkPassword indica que verifica a senha, mas não indica que também inicializa a sessão. Assim, podemos correr o risco de apagar os dados da sessão existente quando ele decidir autenticar o usuário.

  • Assim, temos um efeito colateral de acoplamento. No caso o checkPassword só pode ser chamada quando realmente formos inicializar a sessão, do contrário dados serão perdidos.
  • Se realmente queremos manter o acoplamento dessa forma, deveríamos deixar explícito no nome da função, como por exemplo, checkPasswordAndInitializeSession.

Parâmetros de Saída

  • Quando precisamos reler a assinatura da função para entender o que acontece com o parâmetro de entrada, temos um problema, e isso deve ser evitado!
  • De modo geral, devemos evitar parâmetros de saída. Caso a função precise alterar o estado de algo, mude o estado do objeto que a pertence.

Separação comando-consulta

  • As funções devem fazer ou responder algo, mas não ambos. Ou alterar o estado de um objeto ou retorna informações sobre ele.
  • Fazer as duas tarefas costuma gerar confusão. Por exemplo:
public boolean set(String attribute, String value);
Enter fullscreen mode Exit fullscreen mode

Pode levar a instruções estranhas como:

if (set("username", "unclebob"))...
Enter fullscreen mode Exit fullscreen mode

E fica um caos a interpretação, o que significa esse trecho de código, estamos perguntando se o atributo “username” recebeu o valor “unclebob”? Ou se “username” obteve êxito ao receber o valor “unclebob”?
A intenção neste código acima é ter o set como um adjetivo, assim deveríamos ler “se o atributo username anteriormente recebeu o valor unclebob, porém não fica bem claro, para isso devemos usar o nome melhor como setAndCheckIfExists, mesmo assim ainda tinhamos um código estranho:

if (attributeExists("username")) {
setAttribute("username", "unclebob");
...
}
Enter fullscreen mode Exit fullscreen mode

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

  • Fazer funções retornarem códigos de erros é uma leve violação da separação comando-consulta, pois os comandos são usados como expressões de comparação em estruturas if:
if (deletePage(page) == E_OK)
Enter fullscreen mode Exit fullscreen mode
  • Retornar código de erro se torna um problema para quem chama a função, já que ele vai ter que lidar com o erro e possivelmente criar estruturas aninhadas, deixando o código muito ruim.
  • Mas se usarmos exceções, o código de tratamento de erro pode ficar separado do código e ser simplificado, por exemplo:
try {
    deletePage(page);
    registry.deleteReference(page.name);
    configKeys.deleteKey(page.name.makeKey());
}
catch (Exception e) {
    logger.log(e.getMessage());
}
Enter fullscreen mode Exit fullscreen mode
  • Extraia os blocos try/catch
    • Esses blocos não tem o direito de serem feios;
    • Eles confundem a estrutura do código e misturam o tratamento de erro com o processamento normal do código;
    • É melhor colocar esses blocos em suas próprias funções:
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());
}
Enter fullscreen mode Exit fullscreen mode

Assim, a função delete só faz o tratamento de erro, a função deletePageAndAllReferences só trata de processos que excluem toda página, e o log apenas adicionar a mensagem do erro no console.

Tratamento de erro é uma coisa só

  • Tratamento de erro é uma coisa só, portanto uma função que trata erros não deve fazer mais nada!
  • Assim, a instrução try deve ser a primeira instrução da função e nada mais antes dela.
  • Assim, podemos evitar o uso de classe de erros como por exemplo Error.java, sendo um Enum como vários erros e que tudo dependeria dessa classe.

Evite repetição

  • Repetição é um problema.
  • Sempre será necessário modificar mais de um lugar quando o algoritmo mudar.
  • E nisso podemos omitir erros gerando bugs.
  • A duplicação pode ser a raiz de todo o mal no software.
  • Muitos princípios e práticas têm sido criadas com a finalidade de controlar ou eliminar a repetição de código.

Programação estruturada

  • Programação estruturada de Edsger Dijkstra: “Cada função e bloco dentro de uma função deve ter uma entrada e uma saída”.
  • Apenas em funções maiores tais regras proporcionam benefícios significativos.
  • Se mantivermos funções pequenas, as várias instruções return, break, continue não trarão problemas. Mas instruções como goto só devem existir em grandes funções e devemos evitá-las.

Como escrever funções como essa?

  • Talvez não seja possível aplicar todas as regras vistas até aqui de início.
  • Nas funções, elas começam longas e complexas, com muitos níveis de indentações e loops aninhados, muitos parâmetros, nomes ruins e aleatórios, duplicação de código.
  • Porém depois organizamos, refinamos o código, dividindo em funções, trocamos os os nomes, removemos a duplicação, e no fim devemos ter uma função que respeite as regras vistas aqui.

Conclusão

  • As funções são os verbos e as classes os substantivos.
  • “Essa é uma verdade muito antiga”.
  • “A arte de programar é, e sempre foi, a arte do projeto de linguagem” (linguagem literal, narrativa).
  • Seguindo as regras deste capítulo, suas funções serão curtas, bem nomeadas e bem organizadas.
  • “Mas jamais se esqueça de que seu objetivo verdadeiro é contar a história do sistema”.

Top comments (0)

🌚 Friends don't let friends browse without dark mode.

Sorry, it's true.