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);
}
}
- 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),
Ou
deliverPay(Employee e, Money pay),
- 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
edeliverPay
, serão enviadas de forma polifórmica através da interfaceEmployee
.
- Assim, a factory usará o
- 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);
}
}
}
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
, eincludeSetupPage
.
-
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 aString
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!
- Você pode estar fazendo uma pergunta sobre aquele parâmetro, exemplo:
- 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)
É melhor do que:
void transform(StringBuffer out)
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.
- Por exemplo, uma classe com eixos cartesianos, como por exemplo,
- Mesmo funções óbvias como
assertEquals(expected, actual)
, são problemáticas! - Quantas vezes já colocou
actual
on deveria serexpected
? - 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);
- 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);
- 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)
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 serwriteField(name)
, que indica que o “name” é um campo. - Por exemplo:
assertEquals
pode ser melhor escrito comoassertExpectedEqualsActual(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;
}
}
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);
Pode levar a instruções estranhas como:
if (set("username", "unclebob"))...
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");
...
}
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)
- 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());
}
- 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());
}
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 comogoto
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)