Escolar Documentos
Profissional Documentos
Cultura Documentos
estecapas de capítulo
O principal perigo são os bugs que estão à espreita em seus programas. Os bugs
podem custar muito se se manifestarem na hora errada. Lembra do bug Y2K?
Muitos programas escritos entre 1960 e 1990 usavam apenas dois dígitos para re-
presentar o ano em datas porque os programadores não esperavam que seus pro-
gramas durassem até o próximo século. Muitos desses programas que ainda esta-
vam em uso na década de 1990 teriam tratado o ano 2000 como 1900. O custo esti-
mado desse bug, atualizado em dólares americanos de 2017, foi de US$ 417 bi-
lhões. 1
Mas para bugs que ocorrem em um único programa, o custo pode ser muito
maior. Em 4 de junho de 1996, o primeiro vôo do foguete francês Ariane 5 termi-
nou após 36 segundos com um estrondo. Parece que o acidente foi devido a um
único bug no sistema de navegação. Um único estouro aritmético inteiro causou
uma perda de $ 370 milhões. 2
Como você se sentiria se fosse responsabilizado por tal desastre? Como você se
sentiria se estivesse escrevendo esse tipo de programa no dia a dia, sem nunca ter
certeza de que um programa que funciona hoje ainda funcionará amanhã? Isso é
o que a maioria dos programadores faz: escrever programas não determinísticos
que não produzem o mesmo resultado toda vez que são executados com os mes-
mos dados de entrada. Os usuários estão cientes disso e, quando um programa
não funciona como esperado, eles tentam novamente, como se a mesma causa pu-
desse produzir um efeito diferente na próxima vez. E às vezes acontece porque
ninguém sabe do que esses programas dependem para sua saída.
O que precisamos para fazer programas mais seguros? Alguns responderão que
precisamos de melhores programadores. Mas bons programadores são como bons
motoristas. Dos programadores, 90% concordam que apenas 10% de todos os pro-
gramadores são bons o suficiente, mas, ao mesmo tempo, 90% dos programadores
pensam que fazem parte dos 10%!
O que podemos fazer sobre este problema? Nenhuma ferramenta, técnica ou dis-
ciplina de programação jamais garantirá que nossos programas estejam comple-
tamente livres de erros. Mas existem muitas práticas de programação que podem
eliminar algumas categorias de bugs e garantir que os bugs remanescentes apare-
çam apenas em áreas isoladas (inseguras) de nossos programas. Isso faz
umenorme diferença porque torna a caça de bugs muito mais fácil e eficiente. En-
tre essas práticas estão escrever programas que são tão simples que obviamente
não têm erros, em vez de escrever programas que são tão complexos que não têm
erros óbvios. 3
Programaçãoé muitas vezes visto como uma forma de descrever como algum pro-
cesso deve ser realizado. Tal descrição geralmente inclui ações que alteram um
estado no modelo de um programa para resolver um problema e decisões sobre o
resultado de tais mutações. Isso é algo que todos entendem e praticam, mesmo
que não sejam programadores.
Se você tem alguma tarefa complexa para realizar, você a divide em etapas. Você
então executa a primeira etapa e examina o resultado. Após o resultado deste
exame, você continua com a próxima etapa ou outra. a Por exemplo, um pro-
grama para adicionar dois valores positivos b pode ser representado pelo se-
guinte pseudocódigo:
se b = 0, retorna a
senão incrementa a e decrementa b
comece novamente com o novo a e b
Você pode ver facilmente como esse programa pode dar errado. Altere quaisquer
dados no fluxograma ou altere a origem ou o destino de qualquer seta e você ob-
terá um potencialprograma bugado. Se você tiver sorte, poderá obter um pro-
grama que não roda, ou que roda para sempre e nunca para. Isso pode ser consi-
derado boa sorte porque você verá imediatamente que há um problema que pre-
cisa ser corrigido. A Figura 1.2 mostra três exemplos de tais problemas.
Figura 1.2 Três versões com bugs do mesmo programa
Há uma solução? Sim existe. O que você pode fazer é evitar o uso de referências
mutáveis, ramificações (se sua linguagem permitir) e loops. Tudo que você precisa
fazer é programar com disciplina.
Outra fonte comum de bugs é o null referência. Como você verá no capítulo 6,
com Kotlin você pode separar claramente o código que permite referências nulas
do código que as proíbe. Mas, em última análise, cabe a você erradicar completa-
mente o uso de referências nulas de seus programas.
Muitos bugs são causados por programas que dependem do mundo externo para
serem executados corretamente. Mas depender do mundo exterior geralmente é
necessário de alguma forma em todosprogramas. Restringir essa dependência a
áreas específicas de seus programas tornará os problemas mais fáceis de detectar
e lidar, embora não elimine completamente a possibilidade desses tipos de bugs.
Neste livro, você aprenderá várias técnicas para tornar seus programas muito
mais seguros. Aqui está uma lista dessas práticas:
ComoEu disse, a palavra efeitos significa todas as interações com o mundo ex-
terno, como escrever no console, em um arquivo, em um banco de dados ou em
uma rede, e também a mutação de qualquer elemento fora do escopo do compo-
nente. Os programas geralmente são escritos em pequenos blocos que possuem
escopo. Em algumas linguagens, esses blocos são chamados de procedimentos ; em
outros (como Java), eles são chamados de métodos . Em Kotlineles são chamados
de funções , embora isso não tenha o mesmo significado que o conceito matemá-
tico de uma função.
Funções Kotlin são basicamente métodos, como em Java e muitas outras lingua-
gens modernas. Esses blocos de código têm umscope , significando uma área do
programa que é visível apenas por esses blocos. Os blocos não apenas têm visibili-
dade do escopo envolvente, mas também fornecem visibilidade dos escopos exter-
nos e, por transitividade, para o mundo exterior. Qualquer mutação do mundo ex-
terno causada por uma função ou método (como a mutação do escopo envolvente,
como a classe na qual o método é definido) é, portanto, um efeito.
Se o programa não retornar um resultado, você não pode chamar seu efeito ob-
servável de efeito colateral; é o efeito primário. Ainda pode ter efeitos colaterais
(secundários), embora isso também seja geralmente considerado uma prática
ruim, seguindo o que é chamado de princípio da “responsabilidade única”.
Código que não sofre mutação nem depende do mundo externo é considerado re-
ferencialmente transparente . O código referencialmente transparente tem vários
atributos interessantes:
A partir deo que descrevi, você provavelmente pode adivinhar os muitos benefí-
cios que pode esperar usando a transparência referencial:
oO principal benefício de usar funções que retornam um valor sem nenhum ou-
tro efeito observável é que elas são equivalentes ao valor de retorno. Tal função
não faz nada. Tem um valor, que depende apenas de seus argumentos. Como con-
sequência, é sempre possível substituir uma chamada de função ou qualquer ex-
pressão referencialmente transparente por seu valor, como mostra a figura 1.4 .
Figura 1.4 Substituir expressões referencialmente transparentes por seus valores não altera o significado
geral.
Quando aplicado a funções, o modelo de substituição permite substituir qualquer
chamada de função por seu valor de retorno. Considere o seguinte código:
Por outro lado, substituir a chamada para a add função por seu valor de retorno
altera o significado do programa, porque a chamada para log não será mais feita
e, portanto, não haverá registro. Isso pode ou não ser importante; em qualquer
caso, altera o resultado daprograma.
1.2.2 Aplicação de princípios seguros a um exemplo simples
② Devolve o donut
O problema com esse tipo de código é que ele é difícil de testar. A execução do
programa para teste envolveria entrar em contato com o banco e registrar a tran-
sação usando algum tipo de conta fictícia. Ou você precisaria criar um cartão de
crédito fictício para registrar o efeito de ligar para o charge função e verificar o
estado do simulado após o teste.
Se você quiser poder testar seu programa sem entrar em contato com o banco ou
usar um mock, você deve remover oefeito colateral. Mas como você ainda quer
cobrar no cartão de crédito, a única solução é adicionar uma representação dessa
operação ao valor devolvido. Sua buyDonut função terá que devolver tanto o do-
nut quanto esta representação do pagamento. Para representar o pagamento,
você pode usar uma Payment classe, conforme mostrado na listagem a seguir.
Freqüentemente, você precisará dessa classe para manter dois (ou mais) valores
de tipos diferentes porque, para tornar os programas mais seguros, é necessário
substituir os efeitos colaterais pelo retorno de uma representação desses efeitos.
Em vez de criar uma Purchase classe específica, você pode usar uma genérica,
Pair . Esta classe é parametrizada pelos dois tipos que contém (neste caso, Do-
nut e Payment ). Kotlin fornece essa classe, assim como Triple , que permite a
representação de três valores. Tal classe seria útil em uma linguagem como Java
porque definir o Purchase classe implicaria escrever um construtor, getters e
provavelmente equals e hashcode métodos, bem como toString . Isso é muito
menos útil em Kotlin porque o mesmo resultado pode ser obtido com uma única
linha de código:
Nesta fase, você não está mais preocupado com a forma como o cartão de crédito
será cobrado. Isso adiciona alguma liberdade à maneira como você cria seu apli-
cativo. Você pode processar o pagamento imediatamente ou armazená-lo para
processamento posterior. Você pode até combinar pagamentos armazenados para
o mesmo cartão e processá-los em uma única operação. Isso economizaria algum
dinheiro minimizando as taxas bancárias para o serviço de cartão de crédito.
o combine A função na listagem 1.3 é usada para combinar pagamentos. Se os
cartões de crédito não corresponderem, uma exceção será lançada. Isso não con-
tradiz o que eu disse sobre programas seguros que não lançam exceções. Aqui,
tentar combinar dois pagamentos com dois cartões de crédito diferentes é consi-
derado um bug, portanto, deve travar o aplicativo. (Isto não é realista. Você terá
que esperar até o capítulo 7 para aprender como lidar com tais situações sem lan-
çar exceções.)
pacote com.fpinkotlin.introduction.listing03
classe Payment(val creditCard: CreditCard, valor val: Int) {
fun combine(pagamento: Pagamento): Pagamento =
if (creditCard == payment.creditCard)
Pagamento(cartão de crédito, valor + pagamento.valor)
senão
throw IllegalStateException("Cartas não coincidem.")
}
Nesse cenário, a combine função não seria eficiente ao comprar vários donuts de
uma só vez. Para isso, você pode substituir a buyDonut função por
buyDonuts(n: Int, creditCard: CreditCard) conforme mostrado na lista-
gem a seguir, mas precisa definir um novo Purchase classe. Como alternativa, se
você tivesse escolhido usar um Pair<Donut, Payment> , teria que substituí-lo
por Pair<List<Donut>, Payment> .
ou
{ _ -> Rosquinha{} }
Quando há um único parâmetro, você pode omitir a parameter -> parte e usar
o parâmetro como it . Como não é usado, o código é reduzido para { Donut()
} . Se isso não estiver claro, não se preocupe: falarei mais sobre isso no próximo
capítulo
buyDonuts(creditCard = cc)
Em Java, você teria que sobrecarregar o método com uma segunda implementa-
ção, como
Agora você pode testar seu programa semusando uma simulação. Por exemplo,
aqui está um teste para o método buyDonuts :
import org.junit.Assert.assertEquals
import org.junit.Test
classe DonutShopKtTest {
@Teste
divertido testeCompreDonuts() {
val cartão de crédito = cartão de crédito()
val compra = buyDonuts(5, creditCard)
assertEquals(Donut.price * 5, purchase.payment.amount)
assertEquals(cartão de crédito, compra.pagamento.cartãodecrédito)
}
}
Outro benefício de ter refatorado seu código é que seu programa pode ser com-
posto com mais facilidade. Se a mesma pessoa fizer várias compras com seu pro-
grama inicial, você terá que entrar em contato com o banco (e pagar a taxa cor-
respondente) cada vez que a pessoacomprou algo. Com a nova versão, porém,
você pode optar por carregar no cartão imediatamente a cada compra ou agrupar
todos os pagamentos feitos com o mesmo cartão e cobrar uma única vez o total.
Para agrupar pagamentos, você precisará usar funções adicionais do
Kotlin List classe:
Com essas funções, você já pode criar uma nova função que agrupe pagamentos
com cartão de crédito, conforme a listagem a seguir.
pacote com.fpinkotlin.introduction.listing05;
classe Payment(val creditCard: CreditCard, valor val: Int) {
fun combine(pagamento: Pagamento): Pagamento =
if (creditCard == payment.creditCard)
Pagamento(cartão de crédito, valor + pagamento.valor)
senão
throw IllegalStateException("Cartas não coincidem.")
objeto complementar {
fun groupByCard(pagamentos: List<Pagamento>): List<Pagamento> =
Payments.groupBy { it.creditCard } ①
.values ②
.map { it.reduce(Pagamento::combinar) } ③
}
}
Comovocê viu, você pode escrever programas mais seguros que são mais fáceis de
testar compondofunções puras , o que significa funções sem efeitos colaterais.
Você pode declarar essas funções usando o fun palavra-chave ou como funções de
valor, como os argumentos dos métodos groupBy , map ou reduce na listagem
anterior. As funções de valor são funções representadas de tal forma que, ao con-
trário das fun funções, podem ser manipuladas pelo programa. Na maioria dos
casos, você pode usá-los como argumentos para outras funções ou como valores
retornados por outras funções. Você aprenderá como isso é feito nos capítulos
seguintes.
Mas o conceito mais importante aqui é a abstração . Olhe para a reduce função.
Ele toma como argumento uma operação e usa essa operação para reduzir uma
lista a um único valor. Aqui a operação possui dois operandos do mesmo tipo. Ex-
ceto por isso, poderia ser qualquer operação.
Considere uma lista de números inteiros. Você poderia escrever um sum função
para calcular a soma dos elementos. Então você poderia escrever um pro-
duct função para calcular o produto dos elementos ou a min ou uma max função
para calcular o mínimo ou o máximo da lista. Como alternativa, você também
pode usar a reduce função para todos esses cálculos. Isso é abstração. Você abs-
trai a parte que é comum a todas as operações na reduce função e passa a parte
variável (a operação) como um argumento.
Você poderia ir mais longe. o reduce function é um caso particular de uma fun-
ção mais geral que pode produzir um resultado de um tipo diferente dos elemen-
tos da lista. Por exemplo, pode ser aplicado a uma lista de caracteres para produ-
zir um arquivo String . Você precisaria começar de um determinado valor (pro-
vavelmente uma string vazia). Nos capítulos 3 e 5, você aprenderá como usar esta
função, chamada fold .
A reduce função não funcionará em uma lista vazia. Pense em uma lista de nú-
meros inteiros — se quiser calcular a soma, você precisa de um elemento para co-
meçar. Se a lista estiver vazia, o que você deve retornar? Você sabe que o resul-
tado deve ser 0, mas isso só funciona para uma soma. Não vai funcionar para um
produto.
No restante deste livro, você aprenderá como abstrair muitas coisas, de modo que
só precisará defini-las uma vez. Você aprenderá, por exemplo, como abstrair lo-
ops para nunca mais precisar escrever loops novamente. E você aprenderá como
abstrair a paralelização de uma maneira que permitirá alternar do processa-
mento serial para o paralelo selecionando uma função ema List classe.
Resumo
2
Relatório da comissão de enquête Ariane 501 Echec du vol Ariane 501
http://www.astrosurf.com/luxorion/astronautique-accident-ariane-v501.htm .
3
“...existem duas maneiras de construir um projeto de software: Uma maneira é
torná-lo tão simples que obviamente não haja deficiências, e a outra maneira é
torná-lo tão complicado que não haja deficiências óbvias. O primeiro método é
muito mais difícil." Veja CAR Hoare, “As roupas velhas do imperador,” Comunica-
ções do ACM 24 (fevereiro de 1981): 75–83.