ما افتقده في Java بعد العمل مع Kotlin / Scala

في الآونة الأخيرة ، أسمع غالبًا أن Java أصبحت لغة عفا عليها الزمن يصعب فيها إنشاء تطبيقات كبيرة مدعومة. بشكل عام ، أنا لا أتفق مع وجهة النظر هذه. في رأيي ، لا تزال اللغة مناسبة لكتابة الطلبات سريعة التنظيم. ومع ذلك ، أعترف أنه يحدث أيضًا أنه عندما تكتب الشفرة يوميًا ، فأنت تفكر أحيانًا: "إلى أي مدى يمكن حلها باستخدام هذا الشيء من لغة أخرى". في هذا المقال ، أردت أن أشارك ألمي وتجربتي. سنبحث في بعض مشكلات Java وكيف يمكن حلها في Kotlin / Scala. إذا كان لديك شعور مماثل أو كنت تتساءل فقط عما يمكن أن تقدمه اللغات الأخرى ، فإنني أسألك تحت القطة.



تمديد الطبقات الحالية


يحدث في بعض الأحيان أنه من الضروري توسيع فئة موجودة دون تغيير محتواها الداخلي. وهذا هو ، بعد إنشاء الفصل ، نحن نكملها مع فئات أخرى. النظر في مثال صغير. لنفترض أن لدينا فئة تمثل نقطة في الفضاء ثنائي الأبعاد. في أماكن مختلفة في الكود ، نحتاج إلى إجراء تسلسل في كل من Json و XML.

دعونا نرى كيف يمكن أن ننظر في جافا باستخدام نمط الزائر
public 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())); } } 

المزيد عن النمط واستخدامه

يبدو ضخمة جدا ، أليس كذلك؟ هل من الممكن حل هذه المشكلة بشكل أكثر أناقة بمساعدة أدوات اللغة؟ إيماءة سكالا وكوتلين بشكل إيجابي. يتم تحقيق ذلك باستخدام آلية تمديد الطريقة. دعونا نرى كيف يبدو.

ملحقات في Kotlin
 data class Dot (val x: Int, val y: Int) //      fun Dot.convertToJson(): String = "{\"x\"=$x, \"y\"=$y}" fun Dot.convertToXml(): String = """<dot> <x>$x</x> <y>$y</y> </dot>""" fun main() { val dot = Dot(1, 2) println("-------- JSON -----------") println(dot.convertToJson()) println("-------- XML -----------") println(dot.convertToXml()) } 


ملحقات في سكالا
 object DotDemo extends App { // val is default case class Dot(x: Int, y: Int) implicit class DotConverters(dot: Dot) { def convertToJson(): String = s"""{"x"=${dot.x}, "y"=${dot.y}}""" def convertToXml(): String = s"""<dot> <x>${dot.x}</x> <y>${dot.y}</y> </dot>""" } val dot = Dot(1, 2) println("-------- JSON -----------") println(dot.convertToJson()) println("-------- XML -----------") println(dot.convertToXml()) } 


يبدو أفضل بكثير. هذا في بعض الأحيان لا يكفي في الواقع مع خرائط وفيرة والتحولات الأخرى.

سلسلة الحوسبة متعددة الخيوط


الآن الجميع يتحدث عن الحوسبة غير المتزامنة والمحظورات على قفل مؤشرات الترابط في التنفيذ. دعونا نتخيل المشكلة التالية: لدينا عدة مصادر للأرقام ، حيث يقوم الأول فقط بإرجاع الرقم ، والثاني - يُرجع الإجابة بعد حساب الأول. نتيجة لذلك ، يجب علينا إرجاع سلسلة ذات رقمين.

من الناحية التخطيطية ، يمكن تمثيل ذلك على النحو التالي


دعونا نحاول حل المشكلة في 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 ) ) ); } 


في هذا المثال ، يتم لف الرقم لدينا في اختياري للتحكم في النتيجة. بالإضافة إلى ذلك ، يتم تنفيذ جميع الإجراءات داخل CompletableFuture من أجل العمل المريح مع الخيوط. يحدث الإجراء الرئيسي في طريقة thenApplyAsync. في هذه الطريقة ، نحصل على اختياري كوسيطة. بعد ذلك ، يتم استدعاء flatMap للتحكم في السياق. في حالة إرجاع اختياري المستلم كـ اختياري. فارغ ، فلن نذهب إلى الخدمة الثانية.

المجموع الذي تلقيناه؟ باستخدام CompleteableFuture وميزات اختيارية مع flatMap و map ، تمكنا من حل المشكلة. على الرغم من أن الحل ، في رأيي ، لا يبدو الطريقة الأكثر أناقة: قبل أن تفهم الأمر ، تحتاج إلى قراءة الكود. وماذا سيحدث مع اثنين أو أكثر من مصادر البيانات؟

هل يمكن أن تساعدنا اللغة بطريقة ما في حل المشكلة. ومرة أخرى ، أنتقل إلى Scala. إليك كيف يمكنك حلها باستخدام أدوات 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" ) )} } 


يبدو مألوفا. وهذه ليست صدفة. ويستخدم مكتبة scala.concurrent ، والتي هي في المقام الأول غلاف على java.concurrent. حسنا ، ماذا يمكن أن تساعدنا سكالا؟ والحقيقة هي أن سلاسل شكل flatMap ، ... ، يمكن تمثيل الخريطة كتسلسل في لـ.

مثال الإصدار الثاني على سكالا
  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" } } 


أصبح الأمر أفضل ، ولكن دعونا نحاول تغيير الرمز مرة أخرى. ربط مكتبة القطط.

الإصدار الثالث من مثال سكالا
 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 


الآن ليس من الأهمية بمكان ماذا يعني OptionT. أريد فقط أن أوضح كم يمكن أن تكون هذه العملية بسيطة وقصيرة.

ولكن ماذا عن Kotlin؟ دعنا نحاول أن نفعل شيئًا مماثلاً في coroutines.

مثال Kotlin
 val result = async { withContext(Dispatchers.Default) { getResultFromFirstService() }?.let { first -> withContext(Dispatchers.Default) { getResultFromSecondService(first) }?.let { second -> "$first $second" } } } 


هذا الرمز له خصائصه الخاصة. أولا ، يستخدم آلية Kotlin من corutin. يتم تنفيذ المهام داخل المتزامن في تجمع مؤشر ترابط خاص (وليس ForkJoin) مع آلية سرقة العمل. ثانياً ، تتطلب هذه الشفرة سياقًا خاصًا ، تؤخذ منه الكلمات الأساسية مثل المزامنة و withContext.

إذا كنت تحب Scala Future ، لكنك تكتب على Kotlin ، فيمكنك الانتباه إلى أغلفة Scala المماثلة. اكتب هذا.

العمل مع تيارات


لإظهار المشكلة بمزيد من التفصيل أعلاه ، دعونا نحاول توسيع المثال السابق: ننتقل إلى أكثر أدوات برمجة Java شيوعًا - Reactor ، على Scala - fs2 .

ضع في اعتبارك قراءة 3 ملفات في دفقًا سطريًا ومحاولة العثور على تطابقات هناك.
إليك أسهل طريقة للقيام بذلك باستخدام Reactor في 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 ) ) ); } 


ليست الطريقة المثلى ، ولكن الإرشادية. ليس من الصعب تخمين أنه مع مزيد من المنطق والوصول إلى موارد الطرف الثالث ، سيزداد تعقيد الكود. دعونا نرى بديل للسياق بناء الجملة لفهم.

مثال من fs2 على 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 


يبدو أنه لا توجد تغييرات كثيرة ، لكنها تبدو أفضل بكثير.

فصل منطق العمل مع higherKind والضمنية


دعونا نمضي قدمًا ونرى كيف يمكننا تحسين الكود الخاص بنا. أريد أن أحذر من أن الجزء التالي قد لا يكون مفهومًا على الفور. أريد أن أوضح الاحتمالات ، وأترك ​​طريقة التنفيذ خارج الأقواس في الوقت الحالي. شرح مفصل يتطلب على الأقل مادة منفصلة. إذا كانت هناك رغبة / تعليقات - سأتابع التعليقات للإجابة على الأسئلة وكتابة الجزء الثاني مع وصف أكثر تفصيلاً :)

لذا ، تخيل عالماً يمكننا من خلاله تحديد منطق الأعمال بغض النظر عن الآثار التقنية التي قد تنشأ أثناء التطوير. على سبيل المثال ، يمكننا أن نجعل كل طلب لاحق إلى DBMS أو خدمة طرف ثالث يتم تنفيذها في مؤشر ترابط منفصل. في اختبارات الوحدة ، نحتاج إلى صنع موك غبي لا يحدث فيه شيء. و هكذا.

ربما فكر بعض الأشخاص في محرك BPM ، لكن اليوم لا يتعلق به. اتضح أنه يمكن حل هذه المشكلة بمساعدة بعض أنماط البرمجة الوظيفية ودعم اللغة. في مكان واحد يمكننا وصف المنطق مثل هذا.

في مكان واحد ، يمكننا وصف المنطق مثل هذا
  def makeCatHappy[F[_]: Monad: CatClinicClient](): F[Unit] = for { catId <- CatClinicClient[F].getHungryCat memberId <- CatClinicClient[F].getFreeMember _ <- CatClinicClient[F].feedCatByFreeMember(catId, memberId) } yield () 


هنا F [_] (تُعرف باسم "ef with a hole") تعني نوعًا فوق نوع (يطلق عليه أحيانًا نوع في الأدب الروسي). يمكن أن يكون قائمة ، مجموعة ، خيار ، المستقبل ، إلخ. كل هذا هو حاوية من نوع مختلف.

بعد ذلك ، نقوم فقط بتغيير سياق تنفيذ التعليمات البرمجية. على سبيل المثال ، بالنسبة لبيئة prod ، يمكننا أن نفعل شيئًا كهذا.

ما قد يبدو رمز القتال؟
 class RealCatClinicClient extends CatClinicClient[Future] { override def getHungryCat: Future[Int] = Future { Thread.sleep(1000) // doing some calls to db (waiting 1 second) 40 } override def getFreeMember: Future[Int] = Future { Thread.sleep(1000) // doing some calls to db (waiting 1 second) 2 } override def feedCatByFreeMember(catId: Int, memberId: Int): Future[Unit] = Future { Thread.sleep(1000) // happy cat (waiting 1 second) println("so testy!") // Don't do like that. It is just for debug } } 


ما قد يبدو رمز الاختبار
 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!") // Don't do like that. It is just for debug } } 


لا يعتمد منطق أعمالنا الآن على الأطر والعملاء المتشعبين والخوادم التي استخدمناها. في أي وقت ، يمكننا تغيير السياق ، وسوف تتغير الأداة.

يتم تحقيق ذلك عن طريق ميزات مثل higherKind والضمنية. لننظر أولاً ، ولهذا سنعود إلى Java.

لنلقِ نظرة على الكود
 public class Calcer { private CompletableFuture<Integer> getCalc(int x, int y) { } } 


كم عدد الطرق لإرجاع النتيجة؟ كثير جدا. يمكننا طرح ، إضافة ، مبادلة وأكثر من ذلك بكثير. الآن تخيل أننا حصلنا على متطلبات واضحة. نحن بحاجة إلى إضافة الرقم الأول إلى الثاني. كم من الطرق يمكننا القيام بذلك؟ إذا حاولت جاهدة وصقل نفسك كثيرا ... بشكل عام ، واحد فقط.

ها هو ذا
 public class Calcer { private CompletableFuture<Integer> getCalc(int x, int y) { return CompletableFuture.supplyAsync(() -> x + y); } } 


ولكن ماذا لو كانت الدعوة إلى هذه الطريقة مخفية ، ونريد اختبارها في بيئة ذات مؤشرات ترابط واحدة؟ أو ماذا لو أردنا تغيير تطبيق الفصل عن طريق إزالة / استبدال CompleteableFuture. لسوء الحظ ، في Java نحن عاجزون وعلينا تغيير طريقة API. ألقِ نظرة على البديل في Scala.

النظر في سمة
 trait Calcer[F[_]] { def getCulc(x: Int, y: Int): F[Int] } 


نقوم بإنشاء سمات (أقرب تناظرية هي الواجهة في Java) دون تحديد نوع الحاوية من قيمة عددنا الصحيح.

علاوة على ذلك يمكننا ببساطة إنشاء تطبيقات مختلفة إذا لزم الأمر.

مثل ذلك
  val futureCalcer: Calcer[Future] = (x, y) => Future {x + y} val optionCalcer: Calcer[Option] = (x, y) => Option(x + y) 


بالإضافة إلى ذلك ، هناك شيء مثير للاهتمام مثل الضمني. يتيح لك إنشاء سياق بيئتنا وتحديد تطبيق السمة بشكل ضمني عليها.

مثل ذلك
  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() 


مبسط ضمني قبل val - إضافة متغير إلى البيئة الحالية ، وضمني كوسيطة للدالة يعني أخذ المتغير من البيئة. هذا يذكرنا إلى حد ما بالإغلاق الضمني.

في المجمل ، اتضح أنه يمكننا إنشاء بيئة قتالية واختبارية بإيجاز دون استخدام مكتبات الطرف الثالث.
ولكن ماذا عن kotlin
في الواقع بطريقة مماثلة يمكننا القيام به في 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)) } } 

نضع هنا أيضًا سياق تنفيذ التعليمات البرمجية الخاصة بنا ، ولكن على عكس Scala ، فإننا نعلم هذا بوضوح.
شكرا للناظر على سبيل المثال.


استنتاج


بشكل عام ، هذه ليست كل ما عندي من آلام. هناك المزيد. أعتقد أن كل مطور لديه الخاصة بهم. بالنسبة لي ، أدركت أن الشيء الرئيسي هو فهم ما هو ضروري حقًا لصالح المشروع. على سبيل المثال ، في رأيي ، إذا كانت لدينا خدمة راحة تعمل كنوع من أنواع المحولات مع مجموعة من التعيين والمنطق البسيط ، فإن كل الوظائف المذكورة أعلاه ليست مفيدة للغاية. Spring Boot + Java / Kotlin مثالي لمثل هذه المهام. هناك حالات أخرى مع عدد كبير من التكامل وتجميع بعض المعلومات. لمثل هذه المهام ، في رأيي ، فإن الخيار الأخير يبدو جيدا جدا. بشكل عام ، من الرائع أن تتمكن من اختيار أداة بناءً على مهمة ما.

موارد مفيدة:

  1. رابط لجميع الأمثلة الكاملة أعلاه
  2. المزيد عن كوروتين في كوتلين
  3. كتاب تمهيدي جيد عن البرمجة الوظيفية في سكالا

Source: https://habr.com/ru/post/ar456774/


All Articles