Baru-baru ini, saya sering mendengar bahwa Java telah menjadi bahasa yang usang di mana sulit untuk membangun aplikasi yang didukung besar. Secara umum, saya tidak setuju dengan sudut pandang ini. Menurut pendapat saya, bahasanya masih cocok untuk menulis aplikasi yang cepat dan teratur. Namun, saya akui, itu juga terjadi ketika menulis kode setiap hari, Anda kadang berpikir: "seberapa baik hal ini akan diputuskan dari bahasa lain ini." Dalam artikel ini, saya ingin berbagi rasa sakit dan pengalaman saya. Kami akan melihat beberapa masalah Jawa dan bagaimana mereka dapat diselesaikan di Kotlin / Scala. Jika Anda memiliki perasaan yang sama atau hanya ingin tahu apa yang bisa ditawarkan bahasa lain, saya bertanya kepada Anda.

Memperluas kelas yang ada
Kadang-kadang terjadi bahwa perlu untuk memperluas kelas yang ada tanpa mengubah konten internalnya. Yaitu, setelah membuat kelas, kami menambahkannya dengan kelas lain. Pertimbangkan sebuah contoh kecil. Misalkan kita memiliki kelas yang merupakan titik dalam ruang dua dimensi. Di tempat yang berbeda dalam kode kita, kita perlu membuat cerita bersambung dalam Json dan XML.
Mari kita lihat bagaimana tampilannya di Jawa menggunakan pola Pengunjungpublic 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())); } }
Lebih lanjut tentang pola dan penggunaannya Terlihat cukup tebal, bukan? Apakah mungkin untuk menyelesaikan masalah ini dengan lebih elegan dengan bantuan alat bahasa? Scala dan Kotlin mengangguk positif. Ini dicapai dengan menggunakan mekanisme perluasan metode. Mari kita lihat tampilannya.
Ekstensi di Kotlin data class Dot (val x: Int, val y: Int)
Ekstensi dalam Scala object DotDemo extends App {
Terlihat jauh lebih baik. Terkadang ini benar-benar tidak cukup dengan pemetaan yang melimpah dan transformasi lainnya.
Rantai komputasi multi-utas
Sekarang semua orang berbicara tentang komputasi asinkron dan larangan mengunci utas eksekusi. Mari kita bayangkan masalah berikut: kita memiliki beberapa sumber angka, di mana yang pertama hanya mengembalikan angka, yang kedua - mengembalikan jawaban setelah menghitung yang pertama. Akibatnya, kita harus mengembalikan string dengan dua angka.
Secara skematis, ini dapat direpresentasikan sebagai berikut Mari kita coba memecahkan masalah di Jawa terlebih dahulu
Contoh Jawa 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 ) ) ); }
Dalam contoh ini, nomor kami dibungkus Opsional untuk mengontrol hasilnya. Selain itu, semua tindakan dilakukan di dalam CompletableFuture untuk pekerjaan nyaman dengan utas. Tindakan utama terjadi dalam metode thenApplyAsync. Dalam metode ini, kita mendapatkan Opsional sebagai argumen. Selanjutnya, flatMap dipanggil untuk mengontrol konteks. Jika Optional yang diterima dikembalikan sebagai Optional.empty, maka kami tidak akan pergi ke layanan kedua.
Total yang kami terima? Menggunakan CompletableFuture dan fitur Opsional dengan flatMap dan peta, kami dapat menyelesaikan masalah. Meskipun, menurut saya, solusinya tidak terlihat dengan cara yang paling elegan: sebelum Anda memahami apa masalahnya, Anda perlu membaca kodenya. Dan apa yang akan terjadi dengan dua atau lebih sumber data?
Apakah bahasa dapat membantu kita menyelesaikan masalah. Dan lagi, buka Scala. Inilah cara Anda dapat menyelesaikannya dengan alat Scala.
Contoh 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" ) )} }
Itu terlihat akrab. Dan ini bukan kebetulan. Ia menggunakan pustaka scala.concurrent, yang terutama merupakan pembungkus dari java.concurrent. Nah, apa lagi yang bisa Scala bantu kami? Faktanya adalah bahwa rantai bentuk flatMap, ..., peta dapat direpresentasikan sebagai urutan untuk.
Contoh versi kedua pada 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" } }
Sudah lebih baik, tapi mari kita coba ganti kode kita lagi. Hubungkan perpustakaan kucing.
Contoh Scala versi ketiga 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
Sekarang tidak begitu penting apa arti OptionT. Saya hanya ingin menunjukkan betapa sederhananya dan singkatnya operasi ini.
Tapi bagaimana dengan Kotlin? Mari kita coba melakukan hal serupa pada coroutine.
Contoh Kotlin val result = async { withContext(Dispatchers.Default) { getResultFromFirstService() }?.let { first -> withContext(Dispatchers.Default) { getResultFromSecondService(first) }?.let { second -> "$first $second" } } }
Kode ini memiliki kekhasan tersendiri. Pertama, ia menggunakan mekanisme korutin Kotlin. Tugas di dalam async dilakukan di kumpulan utas khusus (bukan ForkJoin) dengan mekanisme pencurian pekerjaan. Kedua, kode ini memerlukan konteks khusus, dari mana kata kunci seperti async dan withContext diambil.
Jika Anda menyukai Scala Future, tetapi Anda menulis di Kotlin, maka Anda dapat memperhatikan pembungkus Scala yang sama.
Ketik seperti itu.Bekerja dengan stream
Untuk menunjukkan masalah secara lebih rinci di atas, mari kita coba untuk memperluas contoh sebelumnya: kita beralih ke alat pemrograman Java paling populer -
Reactor , di Scala -
fs2 .
Pertimbangkan membaca 3 file baris demi baris dalam aliran dan coba temukan kecocokan di sana.
Ini adalah cara termudah untuk melakukan ini dengan Reactor di Jawa.
Contoh reaktor di Jawa 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 ) ) ); }
Bukan cara yang paling optimal, tetapi indikatif. Tidak sulit untuk menebak bahwa dengan lebih banyak logika dan akses ke sumber daya pihak ketiga, kompleksitas kode akan tumbuh. Mari kita lihat alternatif sintaksis untuk memahami gula.
Contoh dari fs2 di 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
Tampaknya tidak banyak perubahan, tetapi terlihat jauh lebih baik.
Memisahkan logika bisnis dengan yang lebih tinggi dan implisit
Mari kita lanjutkan dan lihat bagaimana lagi kita dapat meningkatkan kode kita. Saya ingin memperingatkan bahwa bagian selanjutnya mungkin tidak segera dapat dimengerti. Saya ingin menunjukkan kemungkinan, dan meninggalkan metode implementasi dari kurung untuk saat ini. Penjelasan terperinci membutuhkan setidaknya artikel terpisah. Jika ada keinginan / komentar - saya akan mengikuti komentar untuk menjawab pertanyaan dan menulis bagian kedua dengan deskripsi yang lebih rinci :)
Jadi, bayangkan sebuah dunia di mana kita dapat menetapkan logika bisnis terlepas dari efek teknis yang mungkin timbul selama pengembangan. Misalnya, kami dapat membuat setiap permintaan berikutnya ke DBMS atau layanan pihak ketiga dilakukan di utas terpisah. Dalam unit test, kita perlu membuat mok bodoh di mana tidak ada yang terjadi. Dan sebagainya.
Mungkin beberapa orang berpikir tentang mesin BPM, tetapi hari ini ini bukan tentang dia. Ternyata masalah ini dapat diselesaikan dengan bantuan beberapa pola pemrograman fungsional dan dukungan bahasa. Di satu tempat kita bisa menggambarkan logika seperti ini.
Di satu tempat, kita bisa menggambarkan logika seperti ini def makeCatHappy[F[_]: Monad: CatClinicClient](): F[Unit] = for { catId <- CatClinicClient[F].getHungryCat memberId <- CatClinicClient[F].getFreeMember _ <- CatClinicClient[F].feedCatByFreeMember(catId, memberId) } yield ()
Di sini F [_] (dibaca sebagai "ef dengan lubang") berarti tipe di atas tipe (kadang-kadang disebut spesies dalam literatur Rusia). Itu bisa Daftar, Set, Opsi, Masa Depan, dll. Semua itu adalah wadah dari jenis yang berbeda.
Selanjutnya, kita cukup mengubah konteks eksekusi kode. Sebagai contoh, untuk lingkungan prod kita dapat melakukan sesuatu seperti ini.
Seperti apa kode tempur itu? class RealCatClinicClient extends CatClinicClient[Future] { override def getHungryCat: Future[Int] = Future { Thread.sleep(1000)
Seperti apa bentuk kode tes 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!")
Logika bisnis kami sekarang tidak tergantung pada kerangka kerja, http-klien dan server yang kami gunakan. Kapan saja, kita dapat mengubah konteksnya, dan alat akan berubah.
Ini dicapai oleh fitur-fitur seperti lebih tinggi dan tersirat. Mari kita pertimbangkan dulu, dan untuk ini kita akan kembali ke Jawa.
Mari kita lihat kodenya public class Calcer { private CompletableFuture<Integer> getCalc(int x, int y) { } }
Berapa banyak cara untuk mengembalikan hasilnya? Cukup banyak. Kami dapat mengurangi, menambah, menukar, dan banyak lagi. Sekarang bayangkan kita telah diberikan persyaratan yang jelas. Kita perlu menambahkan angka pertama ke yang kedua. Berapa banyak cara yang bisa kita lakukan ini?
jika Anda berusaha keras dan memperbaiki diri sendiri ... secara umum, hanya satu.
Ini dia public class Calcer { private CompletableFuture<Integer> getCalc(int x, int y) { return CompletableFuture.supplyAsync(() -> x + y); } }
Tetapi bagaimana jika panggilan ke metode ini disembunyikan, dan kami ingin menguji dalam lingkungan berulir tunggal? Atau bagaimana jika kita ingin mengubah implementasi kelas dengan menghapus / mengganti CompletableFuture. Sayangnya, di Java kita tidak berdaya dan harus mengubah metode API. Lihatlah alternatif di Scala.
Pertimbangkan sifat trait Calcer[F[_]] { def getCulc(x: Int, y: Int): F[Int] }
Kami membuat sifat-sifat (analog terdekat adalah antarmuka di Jawa) tanpa menentukan jenis wadah dari nilai integer kami.
Selanjutnya kita bisa membuat berbagai implementasi jika perlu.
Seperti itu val futureCalcer: Calcer[Future] = (x, y) => Future {x + y} val optionCalcer: Calcer[Option] = (x, y) => Option(x + y)
Selain itu, ada hal yang menarik seperti Implisit. Ini memungkinkan Anda untuk membuat konteks lingkungan kami dan secara implisit memilih penerapan sifat berdasarkan itu.
Seperti itu 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()
Implisit sederhana sebelum val - menambahkan variabel ke lingkungan saat ini, dan implisit sebagai argumen ke fungsi berarti mengambil variabel dari lingkungan. Ini agak mengingatkan pada penutupan implisit.
Secara agregat, ternyata kita dapat membuat lingkungan pertarungan dan pengujian dengan lebih ringkas tanpa menggunakan perpustakaan pihak ketiga.
Tapi bagaimana dengan kotlinSebenarnya dengan cara yang sama bisa kita lakukan di 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)) } }
Di sini kami juga mengatur konteks eksekusi kode kami, tetapi tidak seperti Scala, kami secara eksplisit menandai ini.
Terima kasih kepada
Beholder untuk contohnya.
Kesimpulan
Secara umum, ini tidak semua rasa sakit saya. Masih ada lagi. Saya pikir masing-masing pengembang memiliki sendiri. Bagi saya sendiri, saya menyadari bahwa hal utama adalah memahami apa yang sebenarnya diperlukan untuk kepentingan proyek. Sebagai contoh, menurut saya, jika kita memiliki layanan istirahat yang bertindak sebagai semacam adaptor dengan sekelompok pemetaan dan logika sederhana, maka semua fungsi di atas tidak terlalu berguna. Spring Boot + Java / Kotlin sangat cocok untuk tugas seperti itu. Ada kasus lain dengan sejumlah besar integrasi dan agregasi dari beberapa informasi. Untuk tugas-tugas seperti itu, menurut saya, opsi terakhir terlihat sangat bagus. Secara umum, itu keren jika Anda dapat memilih alat berdasarkan tugas.
Sumber daya yang berguna:
- Tautan ke semua contoh lengkap di atas
- Lebih lanjut tentang Corotin di Kotlin
- Buku pengantar yang bagus tentang pemrograman fungsional di Scala