Resolva um problema do mundo real usando Java
Veja como o Java difere do Python e do Groovy, pois é usado para resolver problemas reais de uma instituição de caridade.
Como escrevi nos dois primeiros artigos desta série, gosto de resolver pequenos problemas escrevendo pequenos programas em diferentes linguagens, para poder comparar as diferentes maneiras como eles abordam a solução. O exemplo que estou usando nesta série é dividir suprimentos a granel em cestas de valor semelhante para distribuir aos vizinhos em dificuldades em sua comunidade, sobre o qual você pode ler no primeiro artigo desta série.
No primeiro artigo, resolvi esse problema usando a linguagem de programação Groovy, que é semelhante ao Python em muitos aspectos, mas sintaticamente é mais parecida com C e Java. No segundo artigo resolvi em Python com design e esforço bem parecidos, o que demonstra a semelhança entre as linguagens.
Agora vou tentar em Java.
A solução Java
Ao trabalhar em Java, me pego declarando classes utilitárias para armazenar tuplas de dados (o novo recurso de registro será ótimo para isso), em vez de usar o suporte de linguagem para mapas oferecido em Groovy e Python. Isso ocorre porque Java incentiva a criação de mapas que mapeiam um tipo específico para outro tipo específico, mas em Groovy ou Python, é legal ter um mapa com chaves e valores de tipo misto.
A primeira tarefa é definir essas classes utilitárias, e a primeira é a classe Unit
:
class Unit {
private String item, brand;
private int price;
public Unit(String item, String brand, int price) {
this.item = item;
this.brand = brand;
this.price = price;
}
public String getItem() { return this.item; }
public String getBrand() { return this.brand; }
public int getPrice() { return this.price; }
@Override
public String toString() { return String.format("item: %s brand: %s price: %d",item,brand,price); }
}
Não há nada muito surpreendente aqui. Eu efetivamente criei uma classe cujas instâncias são imutáveis, pois não há setters para os campos item
, brand
ou price
e eles são declarados privado
. Como regra geral, não vejo valor em criar uma estrutura de dados mutável, a menos que eu a modifique; e neste aplicativo, não vejo nenhum valor na mutação da classe Unit
.
Embora seja necessário mais esforço para criar essas classes utilitárias, criá-las incentiva um pouco mais de esforço de design do que apenas usar um mapa, o que pode ser uma coisa boa. Neste caso, percebi que um pacote em massa é composto por um número de unidades individuais, então criei a classe Pack
:
class Pack {
private Unit unit;
private int count;
public Pack(String item, String brand, int unitCount, int packPrice) {
this.unit = new Unit(item, brand, unitCount > 0 ? packPrice / unitCount : 0);
this.count = unitCount;
}
public String getItem() { return unit.getItem(); }
public String getBrand() { return unit.getBrand(); }
public int getUnitPrice() { return unit.getPrice(); }
public int getUnitCount() { return count; }
public List<Unit> unpack() { return Collections.nCopies(count, unit); }
@Override
public String toString() { return String.format("item: %s brand: %s unitCount: %d unitPrice: %d",unit.getItem(),unit.getBrand(),count,unit.getPrice()); }
}
Semelhante à classe Unit
, a classe Pack
é imutável. Algumas coisas que valem a pena mencionar aqui:
- Eu poderia ter passado uma instância de
Unit
para o construtorPack
. Optei por não fazê-lo porque a natureza física de um pacote a granel me encorajou a pensar na “unidade” como uma coisa interna não visível do lado de fora, mas que requer desempacotamento para expor as unidades. Esta é uma decisão importante neste caso? Provavelmente não, mas para mim, pelo menos, é sempre bom pensar nesse tipo de consideração. - O que leva ao método
unpack()
. A classePack
cria a lista de instâncias deUnit
somente quando você chama esse método – ou seja, a classe é preguiçosa. Como princípio geral de design, descobri que vale a pena decidir se o comportamento de uma classe deve ser ansioso ou preguiçoso, e quando isso não parece importar, opto pela preguiça. Esta é uma decisão importante neste caso? Talvez - esse design lento permite que uma nova lista de instâncias deUnit
seja gerada em cada chamada deunpack()
, o que pode ser uma coisa boa no futuro. De qualquer forma, adquirir o hábito de sempre pensar em comportamento ansioso versus comportamento preguiçoso é um bom hábito.
O leitor mais atento notará que, ao contrário dos exemplos Groovy e Python, onde eu estava focado principalmente em código compacto e gastei muito menos tempo pensando em decisões de design, aqui separei a definição de um Pack
do número de instâncias de Pack
compradas. Novamente, do ponto de vista do design, isso pareceu uma boa ideia, já que o Pack
é conceitualmente bastante independente do número de instâncias do Pack
adquiridas.
Diante disso, preciso de mais uma classe utilitária: a classe Bought
:
class Bought {
private Pack pack;
private int count;
public Bought(Pack pack, int packCount) {
this.pack = pack;
this.count = packCount;
}
public String getItem() { return pack.getItem(); }
public String getBrand() { return pack.getBrand(); }
public int getUnitPrice() { return pack.getUnitPrice(); }
public int getUnitCount() { return pack.getUnitCount() * count; }
public List<Unit> unpack() { return Collections.nCopies(count, pack.unpack()).stream().flatMap(List::stream).collect(Collectors.toList()); }
@Override
public String toString() { return String.format("item: %s brand: %s bought: %d pack(s) totalUnitCount: %d unitPrice: %d",pack.getItem(),pack.getBrand(),count,pack.getUnitCount() * count,pack.getUnitPrice()); }
}
Notavelmente :
- Decidi passar um
Pack
para o construtor. Por que? Porque na minha opinião a estrutura física das embalagens a granel adquiridas é externa, e não interna, como é o caso das embalagens a granel individuais. Mais uma vez, pode não ser importante nesta aplicação, mas acredito que é sempre bom pensar nessas coisas. No mínimo, observe que não sou casado com a simetria! - Mais uma vez o método
unpack()
demonstra o princípio do design lento. Isso exige mais esforço para gerar uma lista de instâncias deUnit
(em vez de uma lista de listas de instâncias deUnit
, o que seria mais fácil, mas exigiria achatando ainda mais no código).
OK! É hora de seguir em frente e resolver o problema. Primeiro, declare os pacotes adquiridos:
var packs = new Bought[] {
new Bought(new Pack("Rice","Best Family",10,5650),1),
new Bought(new Pack("Spaghetti","Best Family",1,327),10),
new Bought(new Pack("Sardines","Fresh Caught",3,2727),3),
new Bought(new Pack("Chickpeas","Southern Style",2,2600),5),
new Bought(new Pack("Lentils","Southern Style",2,2378),5),
new Bought(new Pack("Vegetable oil","Crafco",12,10020),1),
new Bought(new Pack("UHT milk","Atlantic",6,4560),2),
new Bought(new Pack("Flour","Neighbor Mills",10,5200),1),
new Bought(new Pack("Tomato sauce","Best Family",1,190),10),
new Bought(new Pack("Sugar","Good Price",1,565),10),
new Bought(new Pack("Tea","Superior",5,2720),2),
new Bought(new Pack("Coffee","Colombia Select",2,4180),5),
new Bought(new Pack("Tofu","Gourmet Choice",1,1580),10),
new Bought(new Pack("Bleach","Blanchite",5,3550),2),
new Bought(new Pack("Soap","Sunny Day",6,1794),2)
};
Isso é muito bom do ponto de vista da legibilidade: há um pacote de Best Family Rice contendo 10 unidades que custa 5.650 (usando aquelas unidades monetárias malucas, como nos outros exemplos). É fácil ver que, além de um pacote a granel de 10 sacos de arroz, a organização adquiriu 10 pacotes a granel de um saco de espaguete cada. As classes utilitárias estão fazendo algum trabalho nos bastidores, mas isso não é importante neste momento por causa do excelente trabalho de design!
Observe que a palavra-chave var
é usada aqui; é um dos recursos interessantes nas versões recentes do Java que ajuda a tornar a linguagem um pouco menos detalhada (o princípio é chamado DRY - não se repita), permitindo que o compilador infira o tipo de dados da variável a partir de o tipo da expressão do lado direito. Isso é semelhante à palavra-chave def
do Groovy, mas como o Groovy, por padrão, é digitado dinamicamente e o Java é digitado estaticamente, as informações de digitação inferidas em Java por var
persistem em todo o vida útil dessa variável.
Por fim, vale a pena mencionar que packs
aqui é um array e não uma instância de List
. Se você estivesse lendo esses dados de um arquivo separado, provavelmente preferiria criá-los como uma lista.
Em seguida, descompacte os pacotes a granel. Como a descompactação de instâncias Pack
é delegada em listas de instâncias Unit
, você pode usar isso assim:
var units = Stream.of(packs)
.flatMap(bought -> {
return bought.unpack().stream(); })
.collect(Collectors.toList());
Isso usa alguns dos recursos interessantes de programação funcional introduzidos em versões posteriores do Java. Converta o array packs
declarado anteriormente em um fluxo Java, use flatmap()
com um lambda para nivelar as sublistas de unidades geradas pelo unpack()
da classe Bought
e colete os elementos de fluxo resultantes de volta em uma lista.
Assim como nas soluções Groovy e Java, a etapa final é reembalar as unidades nos cestos para distribuição. Aqui está o código - não é muito mais prolixo do que a versão Groovy (deixando os pontos e vírgulas cansativos de lado) nem é tão diferente assim:
var valueIdeal = 5000;
var valueMax = Math.round(valueIdeal * 1.1);
var rnd = new Random();
var hamperNumber = 0; // [1]
while (units.size() > 0) { // [2]
hamperNumber++;
var hamper = new ArrayList<Unit>();
var value = 0; // [2.1]
for (boolean canAdd = true; canAdd; ) { // [2.2]
var u = rnd.nextInt(units.size()); // [2.2.1]
canAdd = false; // [2.2.2]
for (int o = 0; o < units.size(); o++) { // [2.2.3]
var uo = (u + o) % units.size();
var unit = units.get(uo); // [2.2.3.1]
if (units.size() < 3 ||
!hamper.contains(unit) &&
(value + unit.getPrice()) < valueMax) { // [2.2.3.2]
hamper.add(unit);
value += unit.getPrice();
units.remove(uo); // [2.2.3.3]
canAdd = units.size() > 0;
break; // [2.2.3.4]
}
}
} // [2.2.4]
System.out.println();
System.out.printf("Hamper %d value %d:\n",hamperNumber,value);
hamper.forEach(unit -> {
System.out.printf("%-25s%-25s%7d\n", unit.getItem(), unit.getBrand(),
unit.getPrice());
}); // [2.3]
System.out.printf("Remaining units %d\n",units.size()); // [2.4]
Alguns esclarecimentos, com números entre colchetes nos comentários acima (por exemplo, [1]) correspondendo aos esclarecimentos abaixo:
- 1. Configure os valores ideais e máximos a serem carregados em qualquer cesto, inicialize o gerador de números aleatórios do Java e o número do cesto.
- 2.1 Aumente o número do cesto, obtenha um novo cesto vazio (uma lista de instâncias de
Unit
) e defina seu valor como 0.
- 2.1 Aumente o número do cesto, obtenha um novo cesto vazio (uma lista de instâncias de
- 2.2.1 Obtenha um número aleatório entre zero e o número de unidades restantes menos 1.
- 2.2.2 Suponha que você não consiga encontrar mais unidades para adicionar.
- 2.2.3.1 Descubra qual unidade observar.
- 2.2.3.2 Adicione esta unidade ao cesto se restarem apenas algumas ou se o valor do cesto não for muito alto depois que a unidade for adicionada e essa unidade ainda não estiver no cesto .
- 2.2.3.3 Adicione a unidade à cesta, aumente o valor da cesta pelo preço unitário e remova a unidade da lista de unidades disponíveis.
- 2.2.3.4 Enquanto houver unidades restantes, você poderá adicionar mais, então saia desse ciclo para continuar procurando.
for {}
, se você inspecionou todas as unidades restantes e não conseguiu encontrar uma para adicionar ao cesto, o cesto está completo; caso contrário, você encontrou um e pode continuar procurando por mais.Quando você executa este código, a saída é bastante semelhante à saída dos programas Groovy e Python:
Hamper 1 value 5465:
Tofu Gourmet Choice 1580
Bleach Blanchite 710
Coffee Colombia Select 2090
Flour Neighbor Mills 520
Sugar Good Price 565
Remaining units 150
Hamper 2 value 5482:
Sardines Fresh Caught 909
Tomato sauce Best Family 190
Vegetable oil Crafco 835
UHT milk Atlantic 760
Chickpeas Southern Style 1300
Lentils Southern Style 1189
Soap Sunny Day 299
Remaining units 143
Hamper 3 value 5353:
Soap Sunny Day 299
Rice Best Family 565
UHT milk Atlantic 760
Flour Neighbor Mills 520
Vegetable oil Crafco 835
Bleach Blanchite 710
Tomato sauce Best Family 190
Sardines Fresh Caught 909
Sugar Good Price 565
Remaining units 134
…
Hamper 23 value 5125:
Sardines Fresh Caught 909
Rice Best Family 565
Spaghetti Best Family 327
Lentils Southern Style 1189
Chickpeas Southern Style 1300
Vegetable oil Crafco 835
Remaining units 4
Hamper 24 value 2466:
UHT milk Atlantic 760
Spaghetti Best Family 327
Vegetable oil Crafco 835
Tea Superior 544
Remaining units 0
O último cesto é abreviado em conteúdo e valor.
Pensamentos finais
As semelhanças no "código funcional" com o original do Groovy são óbvias - a estreita relação entre Groovy e Java é evidente. Groovy e Java divergiram de algumas maneiras em coisas que foram adicionadas ao Java após o lançamento do Groovy, como as palavras-chave var
vs. def
e as semelhanças e diferenças superficiais entre Groovy fechamentos e lambdas Java. Além disso, toda a estrutura de fluxos Java adiciona muito poder e expressividade à plataforma Java (divulgação completa, caso não seja óbvio - sou apenas um bebê na floresta de fluxos Java).
A intenção do Java de usar mapas para mapear instâncias de um único tipo para instâncias de outro tipo único leva você a usar classes utilitárias, ou tuplas, em vez das intenções mais inerentemente flexíveis em mapas Groovy (que são basicamente apenas Map
mais muito açúcar sintático para eliminar os tipos de casting e problemas de instanceof
que você criaria em Java) ou em Python. O bônus disso é a oportunidade de aplicar algum esforço real de design a essas classes utilitárias, o que compensa pelo menos na medida em que instila bons hábitos no programador.
Além das classes utilitárias, não há muita cerimônia adicional nem clichê no código Java em comparação com o código Groovy. Bem, exceto que você precisa adicionar um monte de importações e agrupar o "código de trabalho" em uma definição de classe, que pode ser assim:
import java.lang.*;
import java.util.*;
import java.util.Collections.*;
import java.util.stream.*;
import java.util.stream.Collectors.*;
import java.util.Random.*;
public class Distribute {
static public void main(String[] args) {
// the working code shown above
}
}
class Unit { … }
class Pack { … }
class Bought { … }
Os mesmos bits complicados são necessários em Java e em Groovy e Python quando se trata de pegar coisas da lista de instâncias de Unit
para os dificultadores, envolvendo números aleatórios, loops pelas unidades restantes, etc.
Outra questão que vale a pena mencionar: esta não é uma abordagem particularmente eficiente. Remover elementos de ArrayLists
, ser descuidado com expressões repetidas e algumas outras coisas tornam isso menos adequado para um enorme problema de redistribuição. Fui um pouco mais cuidadoso aqui para me limitar a dados inteiros. Mas pelo menos é bastante rápido de executar.
Sim, ainda estou usando os temidos while { … }
e for { … }
. Ainda não pensei em uma maneira de usar mapear e reduzir o processamento de fluxo de estilo em conjunto com uma seleção aleatória de unidades para reembalagem. Você pode?
Fique ligado nos próximos artigos desta série, com versões em Julia e Go.