As tão famosas unidades de medida são as quantidades de determinada grandeza física e que servem de padrão para eventuais comparações e medidas. E assim como no mundo real, esses elementos também são utilizados no mundo da tecnologia da informação. Porém, sempre surgem várias questões: Qual é a melhor forma de utilizar esses padrões em um software? Existem boas práticas? Quais são os impactos em caso de erro? O objetivo desse artigo é falar um pouco sobre a especificação de unidade de medida em Java.
No nosso cotidiano, utilizamos as unidades de medida por diversos motivos, por exemplo, indicar a distância, o peso, a velocidade, dentre outras coisas. De fato, esse tipo de unidade de medida vem sendo utilizado pela humanidade em larga escala e na área da tecnologia isso não é diferente. Muito softwares utilizam tais recursos em diversos locais, por exemplo, o peso de um produto comprado num e-commerce. No entanto, surgem diversas perguntas, por exemplo, qual é a melhor forma de representar as unidades medidas? Afinal, por que não podemos apenas utilizar um tipo numérico e deixar implícito sua unidade? É impossível dar problema, correto? Para falar um pouco sobre isso, falaremos aqui de apenas três exemplos:
- A NASA teve uma iniciativa chamada "Star wars" 1983: A Iniciativa de Defesa Estratégica (SDI) foi um sistema de defesa antimíssil proposto para proteger os Estados Unidos de ataques estratégicos de armas nucleares. A tripulação do ônibus espacial deveria posicioná-lo apontado para um espelho montado de lado no qual iria refletir um laser no topo de uma montanha a 1023 metros acima do nível do mar. O experimento falhou porque o programa de computador que controlava os movimentos do ônibus espacial interpretou as informações recebidas no local do laser indicando a elevação em milhas náuticas ao invés de pés. Como resultado, o programa posicionou o ônibus espacial para receber um raio de uma montanha inexistente, a 10.023 milhas náuticas acima do nível do mar. Texto retirado do "O desenvolvimento de software para defesa de mísseis balísticos", por H. Lin, Scientific American, vol. 253, n. 6 (dezembro de 1985), p. 51.
- O Mars Climate Orbiter da NASA caiu em setembro de 1999 por causa de um "simples erro": As unidades de medida estavam erradas em um programa.
- Gimli Glider: O voo 143 da Air Canada foi um voo doméstico canadense de passageiros entre Montreal e Edmonton que ficou sem combustível em 23 de julho de 1983, a uma altitude de 41.000 pés, no meio do caminho. O motivo, erro de unidade de medidas.
Como o intuito deste artigo não é ser uma história de terror de softwares com "erros simples" vamos parar por aqui. Porém, vale salientar que esses "erros simples" podem gerar desastres em nosso negócio, desde prejuízos financeiros, até a possibilidade de custar vidas. E o grande ponto é como utilizar e representar essas unidades de medida no software.
Para demonstrar esse uso, vamos criar um sistema simples de viagens, para fazer algo realmente simples e não sair do foco do artigo, a aplicação REST terá como objetivo gerenciar uma viagem, sendo o conjunto da viagem três atributos: cidade de origem, cidade destino e a distância, logicamente.
Eis que surge a primeira dúvida na modelagem, qual é a maneira de representar a distância.
public class Travel {
private String to;
private String from;
private ? distance;
}
A primeira opção certamente, será representar o tipo como apenas um valor numérico, que no nosso caso, será `long`.
public class Travel {
private String to;
private String from;
private long distance;
}
Como já mencionamos, essa abordagem gera diversos problemas, dentre eles, como saberemos qual unidade de medida que o software está utilizando? Seria em metros, quilômetros, milhas, etc.
Uma boa e simples opção, é utilizar o sistema internacional de medidas, que no nosso caso, iremos utilizar os metros. Porém, ainda não está explícito e faremos isso colocando a unidade de medida como sufixo.
public class Travel {
private String to;
private String from;
private long distanceMeters;
}
Deixando a unidade de medida como parte da variável, fica explícito que unidade de medida o software está utilizando, porém, não existe nenhuma garantia que ele será adicionado ou convertido corretamente em nosso código.
Unit-API
Em situações em que o tipo é complexo, existe o clássico e super famoso "When Make a Type" do Martin Followers. Uma grande vantagem da criação do tipo é a garantia e a segurança no uso da nossa API por parte do usuário, ou seja, estamos seguindo a estratégia de se ter uma API a prova de falhas. Anteriormente, havia escrito um artigo falando sobre as vantagens de se utilizar uma API to tipo dinheiro (money) (também escrevi um livro bem legal para a comunidade, sobre o mesmo tema). Seguindo a mesma linha de raciocínio, utilizaremos a especificação Java para se trabalhar com unidade de medidas usando a unit-api.
No caso deste artigo, iremos utilizar um projeto Maven, assim, o primeiro passo para usar a Unit-API é simplesmente adicionar a dependência dentro do pom.xml.
<dependency>
<groupId>tech.units</groupId>
<artifactId>indriya</artifactId>
<version>2.0.2</version>
</dependency>
Existe um post interessante que fala de alguns recursos importantes que existem dentro do Unit-API, porém, de maneira resumida, essa API nos garantirá segurança e estabilidade para lidar e manipular unidades de medida sem se preocupar com elas, uma vez que existirá um componente que fará esse trabalho para nós.
import tech.units.indriya.quantity.Quantities;
import javax.measure.MetricPrefix;
import javax.measure.Quantity;
import javax.measure.quantity.Length;
import static tech.units.indriya.unit.Units.METRE;
public class App {
public static void main(String[] args) {
Quantity<Length> distance = Quantities.getQuantity(1_000, METRE);
Quantity<Length> distanceB = Quantities.getQuantity(54_000, METRE);
final Quantity<Length> result = distance.add(distanceB);
final Quantity<Length> kiloDistance = result.to(MetricPrefix.KILO(METRE));
System.out.println(result);
System.out.println(kiloDistance);
}
}
Assim, o nosso atributo de distância será representado pelo tipo `Quantity` da especificação. Uma coisa importante, o MongoDB não tem suporte para armazenar esse tipo, portanto, é necessário que criemos também uma conversão para armazenar o tipo de alguma forma, para facilitar, o armazenaremos como `String`. O Jakarta NoSQL possui anotações muito semelhante ao JPA uma vez que algumas anotações são agnósticas e persistentes aos banco de dados.
import jakarta.nosql.mapping.Column;
import jakarta.nosql.mapping.Entity;
import jakarta.nosql.mapping.Id;
import java.time.LocalDate;
import java.util.List;
@Entity
public class Trip {
@Id
private String trip;
@Column
private List<String> friends;
@Column
private LocalDate start;
@Column
private LocalDate end;
@Column
private List<Travel> travels;
}
import jakarta.nosql.mapping.Column;
import jakarta.nosql.mapping.Convert;
import jakarta.nosql.mapping.Entity;
import javax.measure.Quantity;
import javax.measure.quantity.Length;
@Entity
public class Travel {
@Column
private String to;
@Column
private String from;
@Column
@Convert(LengthConverter.class)
private Quantity<Length> distance;
}
import jakarta.nosql.mapping.AttributeConverter;
import javax.measure.Quantity;
import javax.measure.format.QuantityFormat;
import javax.measure.quantity.Length;
import javax.measure.spi.ServiceProvider;
public class LengthConverter implements AttributeConverter<Quantity<Length>, String> {
private static final QuantityFormat FORMAT = ServiceProvider.current().getFormatService().getQuantityFormat();
@Override
public String convertToDatabaseColumn(Quantity<Length> attribute) {
if (attribute == null) {
return null;
}
return FORMAT.format(attribute);
}
@Override
public Quantity<Length> convertToEntityAttribute(String dbData) {
if (dbData == null) {
return null;
}
return (Quantity<Length>) FORMAT.parse(dbData);
}
}
Neste artigo, utilizaremos o conceito de DTO, para saber mais detalhes sobre o trade-off do DTO, existe um artigo que fala de maneira bem detalhada. Um ponto importante é que conseguimos colocar algumas validações com o DTO utilizando o Bean Validation.
import javax.validation.constraints.NotBlank;
import java.util.List;
public class TripDTO {
@NotBlank
private String trip;
private List<String> friends;
@NotBlank
private String start;
@NotBlank
private String end;
private List<TravelDTO> travels;
private int totalDays;
private QuantityDTO distance;
}
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
public class TravelDTO {
@NotBlank
private String to;
@NotBlank
private String from;
@NotNull
private QuantityDTO distance;
}
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
public class QuantityDTO {
@NotBlank
private String unit;
@NotNull
private Number value;
}
A última etapa da aplicação é disponibilizar o recurso graças a classe `TripResource`. Temos o mapper que faz a conversão e o isolamento da entidade além da integração com o banco de dados, graças ao `TripRepository`.
import jakarta.nosql.mapping.Repository;
import javax.enterprise.context.ApplicationScoped;
import java.util.stream.Stream;
@ApplicationScoped
public interface TripRepository extends Repository<Trip, String> {
Stream<Trip> findAll();
}
import jakarta.nosql.mapping.Repository;
import javax.enterprise.context.ApplicationScoped;
import java.util.stream.Stream;
@ApplicationScoped
public interface TripRepository extends Repository<Trip, String> {
Stream<Trip> findAll();
}
import org.modelmapper.ModelMapper;
import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
import javax.validation.Valid;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.util.List;
import java.util.stream.Collectors;
@Path("trips")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
@ApplicationScoped
public class TripResource {
@Inject
private TripRepository repository;
@Inject
private ModelMapper mapper;
@GET
public List<TripDTO> findAll() {
return repository.findAll()
.map(t -> mapper.map(t, TripDTO.class))
.collect(Collectors.toList());
}
@GET
@Path("{id}")
public TripDTO findById(@PathParam("id") String id) {
return repository.findById(id)
.map(d -> mapper.map(d, TripDTO.class))
.orElseThrow(
() -> new WebApplicationException(Response.Status.NOT_FOUND));
}
@POST
public TripDTO insert(@Valid TripDTO tripDTO) {
final Trip trip = mapper.map(tripDTO, Trip.class);
return mapper.map(repository.save(trip), TripDTO.class);
}
@DELETE
@Path("{id}")
public void deleteById(@PathParam("id") String id) {
repository.deleteById(id);
}
}
Um ponto importante está na funcionalidade das novas APIs tanto para a unidades de medida quanto para unidades, onde existem vários recursos interessantes, por exemplo, dentro da entidade temos dois métodos para leitura: Um para retornar o total da distância e outro para retornar o total de dias utilizado na viagem.
@Entity
public class Trip {
....
public Quantity<Length> getDistance() {
return getTravels()
.stream()
.map(Travel::getDistance)
.reduce((a, b) -> a.add(b))
.orElse(Quantities.getQuantity(0, Units.METRE));
}
public long getTotalDays() {
return ChronoUnit.DAYS.between(start, end);
}
}
É possível criar unidades de medida baseado na SI de distância, no caso a milha:
Unit<Length> mile = Units.METRE.multiply(1609.344).asType(Length.class);
E é possível usá-la e manipulá-la de maneira segura sem que exista pequenos erros e grandes desastres, como mencionado no início do artigo.
Uma vez a aplicação pronta, o próximo passo é subí-la e realizar os testes.
mvn clean package kumuluzee:repackage
java -jar target/microprofile.jar
Aplicação de pé, o próximo passo é inserir os dados e verificar o resultado:
curl --location --request POST 'http://localhost:8080/trips' \
--header 'Content-Type: application/json' \
--header 'Content-Type: application/json' \
--data-raw '{"trip": "euro-trip", "friends": ["Otavio", "Edson", "Bruno"], "start": "2010-03-01", "end": "2010-04-01", "travels": [
{"to": "London", "from": "São Paulo", "distance": {"unit": "km", "value": 9496.92}}, {"to": "London", "from": "Paris", "distance": {"unit": "km", "value": 342.74}},
{"to": "Paris", "from": "Rome", "distance": {"unit": "km", "value": 1106.27}}]}'
curl 'http://localhost:8080/trips'
Movendo para a nuvem
Existem várias maneiras para colocar a aplicação no ar, atualmente, o termo cloud já não é novidade para a grande maioria das pessoas, afinal, não se preocupar com o gerenciamento de hardware é uma facilidade de escalabilidade. Recentemente o termo cloud-native se tornou bastante popular e muito discutido. Um dos maiores problemas com esse conceito é que existem diversos livros e artigos tentando explicá-los. O termo que utilizo com base nesses materiais é que cloud-native é uma coleção de boas práticas para otimizar uma aplicação cloud através de container, orquestração e automatização.
Uma maneira bastante simples de levar nossa aplicação ao cloud-native é através da Platform.sh, que de uma maneira geral é uma plataforma como serviço, PaaS, que através do conceito de infraestrutura como código gerará containers orquestrados de maneira automática para o desenvolvedor. Como mencionado nesse artigo, para realizar o deploy são necessário, no mínimo, três arquivos: Um para aplicação, um para os serviços e outro para as rotas.
mongodb:
type: mongodb:3.6
disk: 1024
Nesse artigo nós falamos um pouco sobre a unidade de medidas, a motivação de se utilizar boas práticas, como utilizar tipos quando a variável requer um alto grau de complexidade como as unidades de medidas. Criar uma aplicação que é facilmente portável para nuvem é possível graças a tecnologia Java que está madura e cada vez mais preparada para o mundo cloud-native.
Sobre o autor
Otávio Santana é engenheiro de software, com grande experiência em desenvolvimento open source, com diversas contribuições ao JBoss Weld, Hibernate, Apache Commons e outros projetos. Focado em desenvolvimento poliglota e aplicações de alto desempenho, Otávio trabalhou em grandes projetos nas áreas de finanças, governo, mídias sociais e e-commerce. Membro do comitê executivo do JCP e de vários Expert Groups de JSRs, é também Java Champion e recebeu os prêmios JCP Outstanding Award e Duke's Choice Award.