Você está na página 1de 26

1Tornando os programas mais seguros

estecapas de capítulo

Identificando armadilhas de programação


Olhando para problemas com efeitos colaterais
Como a transparência referencial torna os programas mais seguros
Usando o modelo de substituição para raciocinar sobre programas
Aproveitando ao máximo a abstração

A programação é uma atividade perigosa. Se você é um programador amador,


pode se surpreender ao ler isso. Você provavelmente pensou que estava seguro
sentado na frente da tela e do teclado. Você pode pensar que não arrisca muito
mais do que uma dor nas costas por ficar sentado por muito tempo, alguns pro-
blemas de visão ao ler caracteres minúsculos na tela ou até mesmo uma tendinite
no pulso se você digitar muito furiosamente. Mas se você é (ou quer ser) um pro-
gramador profissional, a realidade é muito pior do que isso.

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.

Com o desenvolvimento deinteligência artificial (IA), o problema da confiabili-


dade do software torna-se mais crucial. Se os programas se destinam a tomar de-
cisões que podem colocar em risco a vida humana, como pilotar aviões ou dirigir
carros autônomos, é melhor garantirmos que funcionem como pretendido.

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%!

A qualidade mais necessária para os programadores é reconhecer suas próprias


limitações. Vamos enfrentá-lo: somos apenas, na melhor das hipóteses, programa-
dores medianos. Gastamos 20% do nosso tempo escrevendo programas com bugs
e depois gastamos 40% do nosso tempo refatorando nosso código para obter pro-
gramas sem bugs aparentes. E depois, gastamos outros 40% depurando o código
que já está em produção porque os bugs vêm em duas categorias: aparentes e não
aparentes. Fique tranquilo, bugs não aparentes se tornarão aparentes - é apenas
uma questão de tempo. A questão permanece: quanto tempo e quanto dano será
causado antes que os bugs se tornem aparentes.

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 

No restante deste capítulo, apresento brevemente conceitos como imutabilidade,


transparência referencial e modelo de substituição, bem como outras sugestões
que, juntas, você pode usar para tornar seus programas muito mais seguros. Você
aplicará esses conceitos repetidas vezes nos próximos capítulos.
1.1 Armadilhas de programação

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

Nesse pseudocódigo, você pode reconhecer as instruções tradicionais da maioria


das linguagens: testar condições, modificar variáveis, ramificar e retornar um va-
lor. Este código pode ser representado graficamente por um fluxograma como o
mostrado na figura 1.1 .
Figura 1.1 Um fluxograma representando um programa como um processo que ocorre no tempo. Várias coi-
sas são transformadas e estados são alterados até que o resultado seja obtido.

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

O primeiro exemplo produz um resultado incorreto e o segundo e o terceiro


nunca terminam. Observe, no entanto, que sua linguagem de programação pode
não permitir que você escreva alguns desses exemplos. Nenhum deles pode ser
escrito em uma linguagem que não permita referências mutantes e nenhum deles
pode ser escrito em uma linguagem que não permita ramificações ou loops. Você
pode pensar que tudo o que precisa fazer é usar essa linguagem. E, de fato, você
poderia. Mas você ficaria restrito a um pequeno número de idiomas e provavel-
mente nenhum deles seria permitido em seu ambiente profissional.

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.

Não use recursos perigosos comomutações e loops. É simples assim! E se você


achar que eventualmente precisa de referências ou loops mutáveis, abstraia-os.
Escreva algum componente que abstraia a mutação de estado de uma vez por to-
das e você nunca mais terá que lidar com o problema. (Alguns idiomas mais ou
menos exóticos oferecem isso pronto para uso, mas provavelmente também não
são idiomas que você pode usar em seu ambiente.) O mesmo se aplica ao loop.
Nesse caso, a maioria das linguagens modernas oferece abstrações de loop ao lado
de um uso mais tradicional de loops. Mais uma vez, é uma questão de disciplina.
Use apenas as partes boas! Mais sobre isso nos capítulos 4 e 5.

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:

Evitar referências mutáveis ​(variáveis) e abstrair o único caso em que a muta-


ção não pode ser evitada.
Evitar estruturas de controle.
Restringir os efeitos (interação com o mundo exterior) a áreas específicas do
seu código. Isso significa nenhuma impressão no console ou em qualquer dis-
positivo, nenhuma gravação em arquivos, bancos de dados, redes ou qualquer
outra coisa que possa acontecer fora dessas áreas restritas.
Nenhum lançamento de exceção. Lançar exceções é a forma moderna derami-
ficação ( GOTO ), que leva ao que é chamadospaghetti code , o que significa que
você sabe onde começa, mas não pode seguir para onde vai. No capítulo 7,
você aprenderá como evitar completamente lançar exceções.

1.1.1 Manuseio de efeitos com segurança

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.

Algummétodos (funções) retornam um valor. Alguns transformam o mundo exte-


rior e alguns fazem as duas coisas. Quando um método ou função retorna um va-
lor e tem um efeito, isso é chamado de efeito colateral . A programação com efei-
tos colaterais é errada em todos os casos. Na medicina, oO termo “efeitos colate-
rais” é usado principalmente para descrever resultados secundários adversos in-
desejados. Na programação, um efeito colateral é algo observável fora do pro-
grama e vem além do resultado retornado pelo programa.

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”.

Programas seguros são construídos compondo funções que recebem um argu-


mento e retornam um valor, e é isso. Não nos importamos com o que está aconte-
cendo dentro das funções porque, em teoria, nada acontece lá. Alguns idiomas
oferecem apenas esses efeitos semfunções: os programas escritos nessas lingua-
gens não têm nenhum efeito observável além de retornar um valor. Mas esse va-
lor pode, na verdade, ser um novo programa que você pode executar para avaliar
o efeito. Essa técnica pode ser usada em qualquer idioma, mas geralmente é con-
siderada ineficiente (o que é discutível). Uma alternativa segura é separar clara-
mente a avaliação de efeitos do restante do programa e até, tanto quanto possível,
abstrair a avaliação de efeitos. Você aprenderá muitas técnicas que permitem isso
nos capítulos 7, 11 e12.

1.1.2 Tornar os programas mais seguros com transparência referencial

Tendonenhum efeito colateral (sem mutação em nada no mundo externo) não é


suficiente para tornar um programa seguro e determinístico. Os programas tam-
bém não devem ser afetados pelo mundo externo - a saída de um programa deve
depender apenas de seu argumento. Isso significa que os programas não devem
ler dados do console, um arquivo, uma URL remota, um banco de dados ou
mesmo do sistema.

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:

É independente. Vocêpode usá-lo em qualquer contexto; tudo o que você pre-


cisa fazer é fornecer um argumento válido.
É determinístico. Istosempre retorna o mesmo valor para o mesmo argumento,
então você não ficará surpreso. Pode, no entanto, retornar um resultado er-
rado, mas pelo menos para o mesmo argumento, o resultado nunca muda.
Ele nunca lançará nenhum tipo de exceção. Pode serlançar erros, como erros de
falta de memória (OOMEs) ou erros de estouro de pilha (SOEs), mas esses erros
significam que o código tem um bug. Esta não é uma situação que você, como
programador, ou os usuários de sua API devem lidar (além de travar o aplica-
tivo, o que geralmente não acontece automaticamente e, eventualmente, corri-
gir o bug).
Ele não cria condições que fazem com que outro código falhe inesperadamente.
Ele não modificará argumentos ou alguns outros dados externos, por exemplo,
fazendo com que o chamador se encontre com dados obsoletos ou exceções de
acesso simultâneo.
Não depende de nenhum dispositivo externo para funcionar. Ele não trava por-
que algum dispositivo externo (seja banco de dados, sistema de arquivos ou
rede) está indisponível, muito lento ou quebrado.

A Figura 1.3 ilustra a diferença entre um programa referencialmente transpa-


rente e um que não éreferencialmentetransparente.
Figura 1.3 Comparando um programa referencialmente transparente com um que não é
1.2 Os benefícios da programação segura

A partir deo que descrevi, você provavelmente pode adivinhar os muitos benefí-
cios que pode esperar usando a transparência referencial:

Seus programas serão mais fáceis de raciocinar porque serão determinísticos.


Uma entrada específica sempre dará a mesma saída. Em muitos casos, você
pode provar que um programa está correto em vez de testá-lo extensivamente
e ainda permanecer incerto sobre se ele será interrompido em condições
inesperadas.
Seus programas serão mais fáceis de testar. Como não há efeitos colaterais,
você não precisarásimulações, que geralmente são necessárias ao testar para
isolar os componentes do programa do lado de fora.
Seus programas serão mais modulares. Isso porque eles serão construídos a
partir de funções que possuem apenas entrada e saída; não há efeitos colate-
rais para lidar, nenhuma exceção para capturar, nenhuma mutação de con-
texto para lidar, nenhum estado mutável compartilhado e nenhuma modifica-
ção simultânea.
A composição e recombinação de programas é muito mais fácil. Para escrever
um programa, você começa escrevendo os váriosfunções básicas necessárias e,
em seguida, combine essas funções em outras de nível superior, repetindo o
processo até que você tenha uma única função correspondente ao programa
que deseja criar. E, porque todas essas funçõessão referencialmente transpa-
rentes, eles podem então ser reutilizados para construir outros programas sem
nenhuma modificação.
Seus programas irão ser inerentemente thread-safe porque evitam a mutação de
estados compartilhados. Isso não significa que todos os dados devem ser imutá-
veis, apenas os dados compartilhados devem ser. Mas os programadores que
aplicam essas técnicas logo percebem que os dados imutáveis ​são sempre mais
seguros, mesmo que a mutação não seja visível externamente. Um motivo é
que os dados que não são compartilhados em um ponto podem ser comparti-
lhados acidentalmente após a refatoração. Sempre usar dados imutáveis ​ga-
rante que esse tipo de problema nunca aconteça.

No restante deste capítulo, apresentarei alguns exemplos de uso de transparência


referencial para escrever programas mais seguros.

1.2.1 Usando o modelo de substituição para raciocinar sobre programas

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:

fun main(args: Array<String>) {


val x = add(mult(2, 3), mult(4, 5))
println(x)
}
fun add(a: Int, b: Int): Int {
log(String.format("Retornando ${a + b} como resultado de $a + $b"))
retornar a + b
}
fun mult(a: Int, b: Int) = a * b
registro divertido(m: String) {
println(m)
}

Substituir mult(2, 3) e mult(4, 5) com seus respectivos valores de retorno


não altera o significado do programa. Isso é mostrado aqui:

val x = add(6, 20)

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

Paraconverter um programa inseguro em um mais seguro, vamos considerar um


exemplo simples que representa a compra de um donut com cartão de crédito.

Listagem 1.1 Um programa Kotlin com efeitos colaterais

fun buyDonut(creditCard: CreditCard): Donut {


val rosquinha = rosquinha()
creditCard.charge(donut.price) ①
return donut ②
}

① Cobra o cartão de crédito como efeito colateral

② Devolve o donut

Nesse código, a cobrança no cartão de crédito é um efeito colateral. Cobrar um


cartão de crédito provavelmente consiste em ligar para o banco, verificar se o car-
tão de crédito é válido e autorizado e registrar a transação. A função retorna 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.

Listagem 1.2 A Payment classe

classe Payment(val creditCard: CreditCard, valor val: Int)

Esta aulacontém os dados necessários para representar o pagamento, que con-


siste em um cartão de crédito e o valor a ser cobrado. Como a buyDonut função
deve retornar a Donut e a Payment , você pode criar uma classe específica para
isso, como Purchase .

compra de classe(val rosquinha: rosquinha, val pagamento: pagamento)

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:

classe de dados Compra(val rosquinha: rosquinha, val pagamento: pagamento)

A Purchase classe já não precisa de um construtor e getters explícitos. Ao adicio-


nar a palavra- data chave na frente da definição de classe, o Kotlin também for-
nece implementações de equals , hashCode , toString e copy . Mas você deve
aceitar as implementações padrão. Duas instâncias de uma classe de dados serão
iguais se todas as propriedades forem iguais. Se isso não for o que você precisa,
você pode substituir qualquer uma dessas funções com suas próprias
implementações.

fun buyDonut(creditCard: CreditCard): Compra {


val rosquinha = rosquinha()
val payment = Payment(creditCard, Donut.price)
devolução da compra(donut, pagamento)
}

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.)

Listagem 1.3 Compondo vários pagamentos em um único

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> .

Listagem 1.4 Comprando vários donuts de uma só vez


pacote com.fpinkotlin.introduction.listing05
data class Purchase(val donuts: List<Donut>, val payment: Payment)
fun buyDonuts(quantidade: Int = 1, creditCard: CreditCard): Compra =
Compra(Lista(quantidade) {
Rosquinha()
}, Pagamento(cartão de crédito, Rosquinha.preço * quantidade))

Aqui List(quantity) { Donut() } cria uma lista de quantity elementos


aplicando sucessivamente a função { Donut() } a valores 0 para quantity -
1 . A { Donut() } função é equivalente a

{ índice -> Rosquinha{} }

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

Observe também que o quantity parâmetro recebe um valor padrão de 1 . Isso


permite chamar a buyDonuts função com a seguinte sintaxe sem especificar a
quantidade:

buyDonuts(creditCard = cc)
Em Java, você teria que sobrecarregar o método com uma segunda implementa-
ção, como

public static Compra buyDonuts(CreditCard creditCard) {


return buyDonuts(1, creditCard);
}
public static Compra buyDonuts(int quantidade,
Cartão de créditoCartão de crédito) {
return new Purchase(Collections.nCopies(quantity, new Donut()),
novo Pagamento(cartão de crédito, Rosquinha.preço * quantidade))
}

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:

groupBy(f: (A) -> B): Map<B, List<A>> -Levacomo parâmetro uma


função de A para B e retorna um mapa de chaves e pares de valores, com cha-
ves sendo do tipo B e valores do tipo List<A> . Você o usará para agrupar pa-
gamentos com cartões de crédito.
values: List<A> -Umfunção de instância de Map que retorna uma lista de
todos os valores no mapa.
map(f: (A) -> B): List<B> -Umfunção de instância de List que pega
uma função de A a B e a aplica a todos os elementos de uma lista de A , retor-
nando uma lista de B .
reduce(f: (A, A) -> A): A -Uma funçãoof List that usa uma operação
(representada por uma função f: (A, A) -> A ) para reduzir a lista a um
único valor. A operação pode ser, por exemplo, adição. Nesse caso, significaria
uma função como f(a, b) = a + b .

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.

Listagem 1.5 Agrupando pagamentos com cartão de crédito

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) } ③

}
}

① Altera List<Payment> para um Map<CreditCard, List<Payment>>, onde cada lista


contém todos os pagamentos de um cartão de crédito específico

② Muda Map<CreditCard, List<Payment>> para List<List<Payment>>

③ Reduz cada List<Payment> em um único Payment, levando ao resultado geral de


um List<Payment>

Observe o uso de uma referência de função na última linha da groupByCard fun-


ção. As referências de função são semelhantes às referências de método em Java.
Se este exemplo não estiver claro, bem, é para isso que serve este livro! Quando
chegar ao fim, você será um especialista em compor taiscódigo.
1.2.3Levando a abstração ao limite

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.

Considere também a groupByCard função. Parece uma função comercial que só


pode ser usada para agrupar pagamentos com cartões de crédito. Mas isso não!
Você pode usar esta função para agrupar os elementos de qualquer lista por qual-
quer uma de suas propriedades. Essa função então deve ser abstraída e colocada
dentro da List classe de forma que possa ser reutilizada facilmente. (Está defi-
nido no Kotlin List aula.)

Levar a abstração ao limite permite tornar os programas mais seguros porque a


parte abstraída será escrita apenas uma vez. Como consequência, uma vez total-
mente testado, não haverá risco de produzir novos bugs ao reimplementá-lo.

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

Você pode tornar os programas mais seguros separando claramente as fun-


ções, que retornam valores, dos efeitos, que interagem com o mundo exterior.
As funções são mais fáceis de raciocinar e testar porque seu resultado é deter-
minístico e não depende de um estado externo.
Levar a abstração para um nível mais alto melhora a segurança, capacidade de
manutenção, capacidade de teste e capacidade de reutilização.
A aplicação de princípios seguros como imutabilidade e transparência referen-
cial protege os programas contra o compartilhamento acidental de um estado
mutável, que é uma grande fonte de bugs em ambientes multithread.

1  Projeto de Desenvolvimento Comunitário do Banco da Reserva Federal de Minne-


apolis. “Índice de preços ao consumidor (estimativa) 1800–”
https://www.minneapolisfed.org/community/teaching-aids/cpi-calculator-
information/consumer-price-index-1800 .

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.

Você também pode gostar