Disseminando conhecimento e inovação em desenvolvimento de software corporativo.

Contribuir

Tópicos

Escolha a região

Início Artigos Unidade de medidas em Java, como e o porquê

Unidade de medidas em Java, como e o porquê

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.