Recentemente, ouço frequentemente que o Java se tornou uma linguagem obsoleta na qual é difícil criar grandes aplicativos suportados. Em geral, não concordo com este ponto de vista. Na minha opinião, o idioma ainda é adequado para escrever aplicativos rápidos e bem organizados. No entanto, admito, também acontece que, ao escrever código todos os dias, às vezes você pensa: "quão bem isso seria decidido a partir dessa outra linguagem". Neste artigo, eu queria compartilhar minha dor e experiência. Veremos alguns problemas de Java e como eles podem ser resolvidos no Kotlin / Scala. Se você tem um sentimento semelhante ou está apenas se perguntando o que outras línguas podem oferecer, pergunto a você em cat.

Estendendo Classes Existentes
Às vezes acontece que é necessário expandir uma classe existente sem alterar seu conteúdo interno. Ou seja, depois de criar a classe, nós a complementamos com outras classes. Considere um pequeno exemplo. Suponha que tenhamos uma classe que é um ponto no espaço bidimensional. Em locais diferentes em nosso código, precisamos serializá-lo em Json e XML.
Vamos ver como ele pode parecer em Java usando o padrão Visitorpublic class DotDemo { public static class Dot { private final int x; private final int y; public Dot(int x, int y) { this.x = x; this.y = y; } public String accept(Visitor visitor) { return visitor.visit(this); } public int getX() { return x; } public int getY() { return y; } } public interface Visitor { String visit(Dot dot); } public static class JsonVisitor implements Visitor { @Override public String visit(Dot dot) { return String .format("" + "{" + "\"x\"=%d, " + "\"y\"=%d " + "}", dot.getX(), dot.getY()); } } public static class XMLVisitor implements Visitor { @Override public String visit(Dot dot) { return "<dot>" + "\n" + " <x>" + dot.getX() + "</x>" + "\n" + " <y>" + dot.getY() + "</y>" + "\n" + "</dot>"; } } public static void main(String[] args) { Dot dot = new Dot(1, 2); System.out.println("-------- JSON -----------"); System.out.println(dot.accept(new JsonVisitor())); System.out.println("-------- XML ------------"); System.out.println(dot.accept(new XMLVisitor())); } }
Mais sobre o padrão e seu uso Parece bastante volumoso, certo? É possível resolver esse problema de maneira mais elegante com a ajuda de ferramentas de linguagem? Scala e Kotlin assentem positivamente. Isso é obtido usando o mecanismo de extensão do método. Vamos ver como fica.
Extensões em Kotlin data class Dot (val x: Int, val y: Int)
Extensões em Scala object DotDemo extends App {
Parece muito melhor. Às vezes, isso realmente não é suficiente com mapeamento abundante e outras transformações.
Cadeia de computação multithread
Agora todo mundo está falando sobre computação assíncrona e as proibições de bloquear threads de execução. Vamos imaginar o seguinte problema: temos várias fontes de números, onde a primeira apenas retorna o número, a segunda - retorna a resposta após o cálculo da primeira. Como resultado, devemos retornar uma string com dois números.
Esquematicamente, isso pode ser representado da seguinte maneira Vamos tentar resolver o problema em Java primeiro
Exemplo Java private static CompletableFuture<Optional<String>> calcResultOfTwoServices ( Supplier<Optional<Integer>> getResultFromFirstService, Function<Integer, Optional<Integer>> getResultFromSecondService ) { return CompletableFuture .supplyAsync(getResultFromFirstService) .thenApplyAsync(firstResultOptional -> firstResultOptional.flatMap(first -> getResultFromSecondService.apply(first).map(second -> first + " " + second ) ) ); }
Neste exemplo, nosso número é agrupado em Opcional para controlar o resultado. Além disso, todas as ações são executadas no CompletableFuture para um trabalho conveniente com threads. A ação principal ocorre no método thenApplyAsync. Neste método, obtemos opcional como argumento. Em seguida, o flatMap é chamado para controlar o contexto. Se o Opcional recebido retornar como Opcional.empty, não iremos para o segundo serviço.
O total que recebemos? Usando os recursos CompletableFuture e Optional com flatMap e map, conseguimos resolver o problema. Embora, na minha opinião, a solução não pareça a maneira mais elegante: antes de entender qual é o problema, você precisa ler o código. E o que aconteceria com duas ou mais fontes de dados?
A linguagem poderia de alguma forma nos ajudar a resolver o problema. E, novamente, vire para Scala. Veja como você pode resolvê-lo com as ferramentas Scala.
Exemplo de Scala def calcResultOfTwoServices(getResultFromFirstService: Unit => Option[Int], getResultFromSecondService: Int => Option[Int]) = Future { getResultFromFirstService() }.flatMap { firsResultOption => Future { firsResultOption.flatMap(first => getResultFromSecondService(first).map(second => s"$first $second" ) )} }
Parece familiar. E isso não é coincidência. Ele usa a biblioteca scala.concurrent, que é principalmente um wrapper sobre java.concurrent. Bem, com o que mais Scala pode nos ajudar? O fato é que cadeias do formato flatMap, ..., map podem ser representadas como uma sequência para.
Exemplo da segunda versão no Scala def calcResultOfTwoServices(getResultFromFirstService: Unit => Option[Int], getResultFromSecondService: Int => Option[Int]) = Future { getResultFromFirstService() }.flatMap { firstResultOption => Future { for { first <- firstResultOption second <- getResultFromSecondService(first) } yield s"$first $second" } }
Melhorou, mas vamos tentar mudar nosso código novamente. Conecte a biblioteca de gatos.
Terceira versão do exemplo Scala import cats.instances.future._ def calcResultOfTwoServices(getResultFromFirstService: Unit => Option[Int], getResultFromSecondService: Int => Option[Int]): Future[Option[String]] = (for { first <- OptionT(Future { getResultFromFirstService() }) second <- OptionT(Future { getResultFromSecondService(first) }) } yield s"$first $second").value
Agora não é tão importante o que OptionT significa. Eu só quero mostrar o quão simples e curta essa operação pode ser.
Mas e Kotlin? Vamos tentar fazer algo semelhante nas corotinas.
Exemplo de Kotlin val result = async { withContext(Dispatchers.Default) { getResultFromFirstService() }?.let { first -> withContext(Dispatchers.Default) { getResultFromSecondService(first) }?.let { second -> "$first $second" } } }
Este código tem suas próprias peculiaridades. Primeiro, ele usa o mecanismo Kotlin da corutina. As tarefas dentro do assíncrono são executadas em um pool de threads especial (não no ForkJoin) com um mecanismo de roubo de trabalho. Em segundo lugar, esse código requer um contexto especial, do qual são usadas palavras-chave como assíncrono e withContext.
Se você gostou do Scala Future, mas escreve no Kotlin, pode prestar atenção a invólucros Scala semelhantes.
Digite tal.Trabalhar com fluxos
Para mostrar o problema com mais detalhes acima, vamos tentar expandir o exemplo anterior: passamos às ferramentas de programação Java mais populares -
Reactor , no Scala -
fs2 .
Considere a leitura linha a linha de 3 arquivos em um fluxo e tente encontrar correspondências lá.
Aqui está a maneira mais fácil de fazer isso com o Reactor em Java.
Exemplo de reator em Java private static Flux<String> glueFiles(String filename1, String filename2, String filename3) { return getLinesOfFile(filename1).flatMap(lineFromFirstFile -> getLinesOfFile(filename2) .filter(line -> line.equals(lineFromFirstFile)) .flatMap(lineFromSecondFile -> getLinesOfFile(filename3) .filter(line -> line.equals(lineFromSecondFile)) .map(lineFromThirdFile -> lineFromThirdFile ) ) ); }
Não é a maneira mais ideal, mas indicativa. Não é difícil adivinhar que, com mais lógica e acesso a recursos de terceiros, a complexidade do código aumentará. Vamos ver a alternativa de açúcar de sintaxe para compreensão.
Exemplo de fs2 no Scala def findUniqueLines(filename1: String, filename2: String, filename3: String): Stream[IO, String] = for { lineFromFirstFile <- readFile(filename1) lineFromSecondFile <- readFile(filename2).filter(_.equals(lineFromFirstFile)) result <- readFile(filename3).filter(_.equals(lineFromSecondFile)) } yield result
Parece que não há muitas mudanças, mas parece muito melhor.
Separando a lógica de negócios com upperKind e implícita
Vamos em frente e ver de que outra forma podemos melhorar nosso código. Quero avisar que a próxima parte pode não ser imediatamente compreensível. Quero mostrar as possibilidades e deixar o método de implementação fora dos colchetes por enquanto. Uma explicação detalhada requer pelo menos um artigo separado. Se houver um desejo / comentários - seguirei os comentários para responder às perguntas e escreverei a segunda parte com uma descrição mais detalhada :)
Então, imagine um mundo em que possamos definir a lógica de negócios, independentemente dos efeitos técnicos que possam surgir durante o desenvolvimento. Por exemplo, podemos fazer com que cada solicitação subsequente a um DBMS ou a um serviço de terceiros seja executada em um encadeamento separado. Nos testes de unidade, precisamos fazer um mok estúpido no qual nada acontece. E assim por diante
Talvez algumas pessoas pensem no mecanismo de BPM, mas hoje não é sobre ele. Acontece que esse problema pode ser resolvido com a ajuda de alguns padrões de programação funcional e suporte a idiomas. Em um lugar, podemos descrever a lógica assim.
Em um lugar, podemos descrever a lógica como esta def makeCatHappy[F[_]: Monad: CatClinicClient](): F[Unit] = for { catId <- CatClinicClient[F].getHungryCat memberId <- CatClinicClient[F].getFreeMember _ <- CatClinicClient[F].feedCatByFreeMember(catId, memberId) } yield ()
Aqui F [_] (lido como "ef com um buraco") significa um tipo sobre um tipo (às vezes é chamado de espécie na literatura russa). Pode ser Lista, Conjunto, Opção, Futuro, etc. Tudo isso é um contêiner de um tipo diferente.
Em seguida, apenas mudamos o contexto da execução do código. Por exemplo, para o ambiente de prod, podemos fazer algo assim.
Como será o código de combate? class RealCatClinicClient extends CatClinicClient[Future] { override def getHungryCat: Future[Int] = Future { Thread.sleep(1000)
Como o código de teste pode ser class MockCatClinicClient extends CatClinicClient[Id] { override def getHungryCat: Id[Int] = 40 override def getFreeMember: Id[Int] = 2 override def feedCatByFreeMember(catId: Int, memberId: Int): Id[Unit] = { println("so testy!")
Nossa lógica de negócios agora não depende de quais estruturas, clientes-http e servidores que usamos. A qualquer momento, podemos mudar o contexto e a ferramenta mudará.
Isso é conseguido por recursos como upperKind e implícito. Vamos considerar o primeiro e, para isso, retornaremos ao Java.
Vejamos o código public class Calcer { private CompletableFuture<Integer> getCalc(int x, int y) { } }
Quantas maneiras de retornar o resultado? Bastante. Podemos subtrair, adicionar, trocar e muito mais. Agora imagine que recebemos requisitos claros. Precisamos adicionar o primeiro número ao segundo. De quantas maneiras podemos fazer isso?
se você se esforçar e se refinar muito ... em geral, apenas um.
Aqui está ele public class Calcer { private CompletableFuture<Integer> getCalc(int x, int y) { return CompletableFuture.supplyAsync(() -> x + y); } }
Mas e se a chamada para esse método estiver oculta e quisermos testar em um ambiente de thread único? Ou, se quisermos alterar a implementação da classe removendo / substituindo CompletableFuture. Infelizmente, em Java, somos impotentes e precisamos alterar a API do método. Dê uma olhada na alternativa em Scala.
Considere a característica trait Calcer[F[_]] { def getCulc(x: Int, y: Int): F[Int] }
Criamos traços (o analógico mais próximo é a interface em Java) sem especificar o tipo de contêiner do nosso valor inteiro.
Além disso, podemos simplesmente criar várias implementações, se necessário.
Assim val futureCalcer: Calcer[Future] = (x, y) => Future {x + y} val optionCalcer: Calcer[Option] = (x, y) => Option(x + y)
Além disso, existe uma coisa interessante como implícita. Ele permite que você crie o contexto de nosso ambiente e selecione implicitamente a implementação da característica com base nela.
Assim def userCalcer[F[_]](implicit calcer: Calcer[F]): F[Int] = calcer.getCulc(1, 2) def doItInFutureContext(): Unit = { implicit val futureCalcer: Calcer[Future] = (x, y) => Future {x + y} println(userCalcer) } doItInFutureContext() def doItInOptionContext(): Unit = { implicit val optionCalcer: Calcer[Option] = (x, y) => Option(x + y) println(userCalcer) } doItInOptionContext()
Implícito simplificado antes de val - adicionar uma variável ao ambiente atual e implícito como argumento a uma função significa retirar a variável do ambiente. Isso lembra um fechamento implícito.
De maneira agregada, podemos criar um ambiente de combate e teste de forma bastante concisa sem usar bibliotecas de terceiros.
Mas e o kotlinDe maneira semelhante, podemos fazer no kotlin:
interface Calculator<T> { fun eval(x: Int, y: Int): T } object FutureCalculator : Calculator<CompletableFuture<Int>> { override fun eval(x: Int, y: Int) = CompletableFuture.supplyAsync { x + y } } object OptionalCalculator : Calculator<Optional<Int>> { override fun eval(x: Int, y: Int) = Optional.of(x + y) } fun <T> Calculator<T>.useCalculator(y: Int) = eval(1, y) fun main() { with (FutureCalculator) { println(useCalculator(2)) } with (OptionalCalculator) { println(useCalculator(2)) } }
Aqui também definimos o contexto de execução do nosso código, mas, diferentemente do Scala, sinalizamos isso explicitamente.
Obrigado ao
Beholder pelo exemplo.
Conclusão
Em geral, essas não são todas as minhas dores. Há mais. Eu acho que cada desenvolvedor tem o seu. Por mim, percebi que o principal é entender o que é realmente necessário para o benefício do projeto. Por exemplo, na minha opinião, se tivermos um serviço de descanso que funciona como um tipo de adaptador com um monte de mapeamento e lógica simples, toda a funcionalidade acima não será muito útil. Spring Boot + Java / Kotlin é perfeito para essas tarefas. Existem outros casos com um grande número de integrações e agregação de algumas informações. Para tais tarefas, na minha opinião, a última opção parece muito boa. Em geral, é legal se você pode escolher uma ferramenta com base em uma tarefa.
Recursos úteis:
- Link para todos os exemplos completos acima
- Mais sobre Corotin em Kotlin
- Um bom livro introdutório sobre programação funcional em Scala