company_banner

Чего мне не хватает в Java после работы с Kotlin/Scala

    В последнее время я часто слышу о том, что Java стала устаревшим языком, на котором сложно строить большие поддерживаемые приложения. В целом, я не согласен с этой точкой зрения. На мой взгляд, язык все еще подходит для написания быстрых и хорошо организованных приложений. Однако, признаюсь, бывает и такое, что при повседневном написании кода иногда думаешь: “как бы хорошо это решилось вот этой штукой из другого языка”. В этой статье я хотел поделиться своей болью и опытом. Мы посмотрим на некоторые проблемы Java и как они могли бы разрешиться в Kotlin/Scala. Если у вас возникает похожее чувство или вам просто интересно, что могут предложить другие языки, — прошу под кат.



    Расширение существующих классов


    Иногда бывает так, что необходимо расширить существующий класс без изменения его внутреннего содержимого. То есть уже после создания класса мы дополняем его другими классами. Рассмотрим небольшой пример. Пусть у нас есть класс, который представляет собой точку в двумерном пространстве. В разных местах нашего кода нам необходимо сериализовать его и в Json, и в XML.

    Посмотрим, как это может выглядеть в Java с помощью паттерна Visitor
    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()));
        }
    }
    

    Более подробно о паттерне и его использовании

    Выглядит достаточно объемно, не так ли? Можно ли решить данную задачу более элегантно с помощью вспомогательных средств языка? Scala и Kotlin кивают положительно. Это достигается с помощью механизма method extension. Посмотрим, как это выглядит.

    Расширения в 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())
    }


    Расширения в Scala
    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

    Пример на 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
                                    )
                            )
                    );
        }


    В этом примере наше число оборачивается в Optional для управления результатом. Кроме того, все действия выполняются внутри CompletableFuture для удобной работы с потоками. Основное действие разворачивается в методе thenApplyAsync. В этом методе мы в качестве аргумента получаем Optional. Далее вызывается flatMap для управления контекстом. Если полученный Optional вернулся как Optional.empty, то во второй сервис мы уже не пойдем.

    Итого, что мы получили? С помощью CompletableFuture и возможностей Optional c flatMap и map нам удалось решить поставленную задачу. Хотя, на мой взгляд, решение выглядит не самым элегантным образом: прежде чем понять, в чем дело, необходимо вчитываться в код. А что было бы в случае с двумя и более источниками данных?

    Мог ли нам как-то помочь решить проблему язык. И снова обратимся к Scala. Вот как это можно решить инструментами 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. Хорошо, а чем еще нам может помочь язык Scala? Дело в том, что цепочки вида flatMap, …, map можно представить в виде последовательности в for.

    Вторая версия пример на 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"
          }
        }


    Стало лучше, но давайте попробуем еще изменить наш код. Подключим библиотеку cats.

    Третья версия примера 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


    Сейчас не так важно, что означает OptionT. Я просто хочу показать, насколько простой и короткой может быть данная операция.

    А как же Kotlin? Давайте попробуем сделать что-то подобное на корутинах.

    Пример на Kotlin
    val result = async {
            withContext(Dispatchers.Default) { getResultFromFirstService() }?.let { first ->
                withContext(Dispatchers.Default) { getResultFromSecondService(first) }?.let { second ->
                    "$first $second"
                }
            }
        }


    В этом коде есть свои особенности. Во-первых, он использует механизм Kotlin корутин. Задачи внутри async выполняются в особом пуле потоков (не ForkJoin) с механизмом work stealing. Во-вторых, данный код требует особого контекста, из которого и берутся ключевые слова вроде async и withContext.

    Если вам понравились Scala Future, но вы пишете на Kotlin, то можете обратить внимание на похожие Scala обертки. Типа такой.

    Работа со стримами


    Чтобы подробнее показать проблему выше, давайте попробуем расширить прошлый пример: обратимся к наиболее популярным инструментам программирования на Java — Reactor, на Scala — fs2.

    Рассмотрим построчное чтение 3 файлов в стриме и попробуем найти там же совпадения.
    Вот самый простой способ сделать это с Reactor на Java.

    Пример с 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
                                )
                    )
            );
        }


    Не самый оптимальный путь, но показательный. Не трудно догадаться, что при бо́льшем количестве логики и обращений к сторонним ресурсам сложность кода будет расти. Посмотрим альтернативу с синтаксическом сахаром for-comprehension.

    Пример с 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 и implicit


    Пойдем дальше и посмотрим, как еще мы можем улучшить наш код. Хочу предупредить, что следующая часть может быть понятной не сразу. Я хочу показать возможности, а способ реализации пока оставить за скобками. Подробное объяснение требует, как минимум, отдельной статьи. Если есть желание/замечания — буду следить в комментариях, чтобы ответить на вопросы и написать вторую часть с более подробным описанием :)

    Итак, представьте себе мир, в котором мы можем задавать бизнес логику независимо от технических эффектов, которые могут возникнуть в ходе разработки. Например, мы можем сделать так, чтобы каждый следующий запрос к СУБД или стороннему сервису выполнялся в отдельном потоке. В юнит тестах нам необходимо сделать глупый мок, в котором ничего не происходит. И так далее.

    Возможно, некоторые подумали о BPM движке, но сегодня не про него. Оказывается, такую проблему можно решить с помощью некоторых паттернов функционального программирования и поддержки языка. В одном месте мы можем описать логику примерно вот так.

    В одном месте мы можем описать логику примерно вот так
      def makeCatHappy[F[_]: Monad: CatClinicClient](): F[Unit] =
        for {
          catId <- CatClinicClient[F].getHungryCat
          memberId <- CatClinicClient[F].getFreeMember
          _ <- CatClinicClient[F].feedCatByFreeMember(catId, memberId)
        } yield ()


    Здесь F[_] (читается как «эф с дыркой») означает тип над типом (иногда в русскоязычной литературе его называют видом). Это может быть List, Set, Option, Future и т.д. Все то, что является контейнером другого типа.

    Далее просто меняем контекст выполнения кода. Например, для прод среды мы можем сделать что-то вроде такого.

    Как может выглядеть боевой код
    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
      }
    }


    Наша бизнес логика теперь не зависит от того, какими фреймворками, http-клиентами и серверами мы пользовались. В любой момент мы можем поменять контекст, и инструмент изменится.

    Достигается это такими особенностями, как higherKind и implicit. Рассмотрим первое, а для этого вернемся к 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);
        }
    }


    Но что, если вызов данного метода скрыт, а мы хотим провести тестирование в однопоточной среде? Или что, если мы хотим поменять реализацию класса, убрав/заменив CompletableFuture. К сожалению, в Java мы бессильны и должны поменять API метода. Взглянем на альтернативу в Scala.

    Рассмотрим trait
    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)
    


    Кроме того, есть такая интересная штука, как Implicit. Она позволяет создать контекст нашего окружения и неявно подбирать реализацию трейта его основе.

    Например так
      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()


    Упрощенно implicit перед val — добавление переменной в текущее окружение, а implicit в качестве аргумента функции означает забор переменной из окружения. Это чем-то напоминает неявное замыкание.

    В совокупности у нас получается, что мы можем создать боевое и тестовое окружение достаточно лаконично без использования сторонних библиотек.
    А как же 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 явно помечаем это.
    Спасибо Beholder за пример.


    Вывод


    В целом, это не все мои боли. Есть и еще. Я думаю, что у каждого разработчика накопились свои. Для себя я понял, что главное понимать, что действительно необходимо для пользы проекта. К примеру, на мой взгляд, если у нас есть rest сервис, который выступает в качестве некого адаптера с кучей маппинга и несложной логикой, то весь функционал выше не особо и полезен. Для таких задач отлично подойдет Spring Boot + Java/Kotlin. Бывают и другие случаи с большим количеством интеграций и агрегацией какой-то информации. Для таких задач, на мой взгляд, последний вариант смотрится очень хорошо. В общем, классно, если вы можете выбирать инструмент отталкиваясь от задачи.

    Полезные ресурсы:

    1. Ссылка на все полные версии примеров выше
    2. Более подробно о корутинах в Kotlin
    3. Неплохая вводная книга по функциональному программированию на Scala
    Райффайзенбанк
    312,69
    Развеиваем мифы об IT в банках
    Поделиться публикацией

    Комментарии 91

      +6
      Иногда бывает так, что необходимо расширить существующий класс без изменения его внутреннего содержимого. То есть уже после создания класса мы дополняем его другими классами.

      Возможно Вы выбрали не удачный пример чтобы донести мысль, потому что появление такой необходимости и предложенное Вами решение говорит о плохом дизайне кода. Как минимум, сериализация не должна быть частью поведения класса «точка», как максимум «The Open Closed Principle» диктует другой подход к реализации ожидаемого поведения.
        0
        Ну вот далее:
        Но что, если вызов данного метода скрыт, а мы хотим провести тестирование в однопоточной среде? Или что, если мы хотим поменять реализацию класса, убрав/заменив CompletableFuture. К сожалению, в Java мы бессильны и должны поменять API метода.

        Это «мы» бессильный, если строим код таким образом, что он не позволяет отделить бизнес логику от интеграционной.

        Простите, но мне кажется, что вы описываете ситуацию, когда плохо написанный код на одном языке более удобен/гибок, чем плохо написанный на другом.
          0
          Простите, а вы всегда отделяете контейнеры типов (List, Set, Optional, Completable Future и т.д.) от их содержимого? :)
            0
            Я вопрос не понял, но если говорить примерами, то я бы вместо
            private CompletableFuture<Integer> getCalc(int x, int y) {
            }
            

            написал
            private Integer getCalc(int x, int y) {
            }
            

            и это метод бы вызывал при необходимости.
          0
          Спасибо. Дополнил пример.
            0
            Дополнили, но мой поинт бы в другом
              0
              Давайте обсудим :)
          0
          .
            0
            Ну хорошо, представим, что так. Но ведь где-то вы будете оборачивать вызов метода во фьючер? И если у вас асинхронный код, то наверняка вы заходите из каких-то методов возвращать фьючу, ну или хотя бы использовать?
              +1
              Вы выше привели пример метода, который вызывается в асинхронном контексте и знает про это, что уже вызывает вопрос — почему метод знает контекст вызова? А потом вы сами справедливо говорите
              Но что, если вызов данного метода скрыт, а мы хотим провести тестирование в однопоточной среде? Или что, если мы хотим поменять реализацию класса, убрав/заменив CompletableFuture. К сожалению, в Java мы бессильны и должны поменять API метода.

              Так тут не java виновата, а вы, что написали такой метод, который знает (зачем?) контекст вызова и неудобен при вызове в другом контексте.
                –1
                Во-первых, здесь я хотел показать, что так можно в принципе. При простом сложении такого точно не нужно. Вы слишком цепляетесь к конкретному небольшому примеру, но не видите сути.
                Во-вторых, вы же согласны, что возращать контейнеры типов можно и это нормально? Тот же Optional.
                  0
                  Вы слишком цепляетесь к конкретному небольшому примеру, но не видите сути.

                  А зачем вы приводите в пример свой идеи код, который написан не правильно с точки зрения дизайна?
                    0
                    Мало того, вы в этом еще и java вините
                      0
                      И я не в чем не обвиняю Java. Просто в разных языках разные инструменты достижения одного результата.
                      0
                      Мне показалось очевидным странность сложения двух чисел в отдельном потоке. Одно выделение времени планировщика будет дороже. Представьте, что внутри более сложные операции, которые выполняются асинхронно. Ну, например, у нас в методе не сложение, а логика вызова 20 других сервисов, а ваше приложение под нагрузкой. Вряд ли вы захотите возвращать просто результат выполнения. Наверняка вы будете внутри проводить манипуляции с фьючерами и в итоге вернете фьючер для максимальной нагрузки ЦП без простоя на IO. Нет?

                      Ну или можете заменить CompletableFuture на Optional, если вы совсем придирчивы. Можем даже придумать требование: возвращение empty, если сложение привело к битовому переполнению.

                      Суть от этого не меняется, но пример усложняется.
                        0
                        Мне показалось очевидным странность сложения двух чисел в отдельном потоке.

                        Вы обращали внимание, как выглядит код, позволяющий использовать стримы в Java коде? Он позволяет вам обстрагировать бизнес логику «сложения двух чисел» от интеграционного кода, который метод с этой логикой вызывает.
                        И я вообще не вижу причин эти два кода мешать в один. Иначе это приведет к программистскому бессилию.
                        Но что, если вызов данного метода скрыт, а мы хотим провести тестирование в однопоточной среде? Или что, если мы хотим поменять реализацию класса, убрав/заменив CompletableFuture. К сожалению, в Java мы бессильны
                          0
                          Не вижу смысла вести диалог в таком виде. Я пишу большое сообщение, вы отвечаете на первое предложение.
                            0
                            Я отвечаю на комментарий в целом, но соблюдая контекст треда
                              0

                              Ну так, а что вы ответите про Optional или асинхронные вычисления??? :)

                                0
                                А я же вам ответил, вы не стали читать или не поняли?
                                  0

                                  Не понял вашу мысль (

                                    0
                                    Я имел ввиду, чтобы дизайн кода должен быть таким, чтобы код бизнес логики был отделен от контекста ее использования и про него не знал.
                                    Например существует метод Math.max(), это хороший метод. Могу ли я его использовать в «асинхронных» вычислениях? Могу. Нужно ли методу в контракте иметь CompletableFuture или Optional? Нет, не нужно.
                                      0

                                      То есть, если я получаю optional с результатом запроса из базы, то мой код плох и надо было в случае отсутствия информации возвращать null?

                                        0
                                        Не понимаю, как «это» следует из моего утверждения. Но вы же понимаете, что не база вам optional возвращает, а вы в него ответ где-то завернете, так же?
                                          0
                                          Если я использую Spring Data, то получаю Optional прямо от спринга.
                                            0
                                            И?
                                              0
                                              То, что со временем мы можем захотеть изменить Optional на какой-нибудь Try и не сможем. Вот об этом и был пример.
                                                0
                                                Не понимаю вас. Вот вы получаете Optional из spring data, а я нет — я всегда объекты или null.
                                                И вы со временем хотите поменять что? реализацию spring data?
                                                  0
                                                  Я советую более внимательно ознакомиться со статьей. Не вижу смысла продолжать.
                                                    0
                                                    Согласен. И в комментариях к статье я выразил всё своё беспокойство касательно того, что в угоду доказательства «бессильности» java вы приводите в пример плохо написанный код.
                                                      0

                                                      Откройте почти любую документацию и вы увидите там игрушечные примеры. Так же и здесь. Просто вы не разобрались с материалом. А доказывать, что в Java нет kind'ов мне надоело. :)

                                                        0
                                                        Это не документация, это сравнение возможностей языка через призму примеров из плохо написанного кода. Профанация.
                                                        Вы это тут пишите, народ потом читает и начинает перешептываться. И в итоге мы имеем
                                                        В последнее время я часто слышу о том, что Java стала устаревшим языком, на котором сложно строить большие поддерживаемые приложения

                                                          0
                                                          Вы просто не поняли пример :)
                                                            0
                                                            Вы не хотите меня услышать — если ваши примеры написать на java грамотно, то в них не будет тех выдуманных проблем, что вы пытаетесь решить.
                                                            Я допускаю, что вы плохие примеры привели, хотя после исправления они не стали лучше :/
                +1
                Удалять/полностью менять свои сообщения после ответа не очень хорошо :)
                  0
                  Простите, я не на тот комментарий ответил, поэтому тут стер, а выше вставил.
                +1
                иногда думаешь: “как бы хорошо это решилось вот этой штукой из другого языка”

                А я часто, читая чужой код на Java, думаю: «как же хорошо, что это не Scala и тут я не буду ломать голову из какого implicit что-то прилетело и откуда взялся метод, которого не должно быть». И я не против Scala, если что. Это очень крутой язык. Просто он со своей философией и для другого. И вот поэтому я против того, чтобы все языки в конце концов превратились в один. Если на Котлине/Scala все круче, так может просто не использовать Java?
                  0
                  Никто же не говорит, что на скале все круче. Мне, например, дико не нравится sbt. Кроме того, меня самого устрашают сервисы, которые делаеют долго и упорно на скале, но которые могли бы появиться по щелчку пальцев на спринге. Тут все зависит от задачи.
                  +2

                  На 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))
                      }
                  }

                  Здесь тоже задаём разный контекст, но только явно, и не ломаем голову откуда он взялся.


                  Пример с визитором неудачный, так как для визиторов важен динамический диспатч, а методы-расширения дают статический.

                    0
                    Отличный пример! Добавлю его в статью, если вы не против?
                      +1

                      Пожалуйста.
                      Есть ещё более интересные примеры с алгебрами.

                        0
                        Вы правда считаете это пример отличным? Получается, что это нормально объявить интерфейс и ниже по коду расширить его. Какая цель такого дизайна? Заставить разработчика страдать от знания того, что чтобы понять, что интерфейс предоставляет, не достаточно посмотреть на его код. Нужно еще прочитать кучу кода вокруг, выискивая где хитрый автор расширил этот интерфейс еще.
                        Вы начали статью с
                        В последнее время я часто слышу о том, что Java стала устаревшим языком, на котором сложно строить большие поддерживаемые приложения

                        И правы лишь в одном — если писать код так, как описано в ваших примерах и как в примере уважаемого Beholder, то приложение далеко не поплывет.

                        В этом и была моя претензия к вашим примерам — вы приводите плохой код на java и говорите, что это ваша боль. Я вижу, что причина вашей боли не в java.
                          +1

                          Методы-расширения — это совсем не расширение интерфейса. С самим интерфейсом при этом ничего не случается и семантика его не меняется. Методы-расширения может написать программист-"клиент" для своего удобства. Это просто более выразительный способ вызова внешних методов. Вместо этого вы могли бы написать утильный класс


                          public class CalcUtil {
                              public static <T> T useCalculator(Calculator<T> calculator, int y) {
                                  return calculator.eval(1, y);
                              }
                          }

                          и "счастливо" его использовать. println(CalcUtil.useCalculator(calculator, 2)); Но ведь способ выше выглядит короче, к тому же есть возможность использовать неявный this.

                            0
                            Посыпаю голову пеплом, был не прав, спасибо что объяснили.
                            Я так понимаю, что в приведенном примере вы расширяете this класс. А какое это дает преимущество в сравнении с простым методом?
                      0
                      В последнее время я часто слышу о том, что Java стала устаревшим языком, на котором сложно строить большие поддерживаемые приложения. В целом, я не согласен с этой точкой зрения. На мой взгляд, язык все еще подходит для написания быстрых и хорошо организованных приложений.

                      Так все-таки, java, по вашему мнению, еще подходит для написания больших поддерживаемых приложений или нет?
                        0

                        Конечно! Можно писать такие приложения и на С. Посмотрите на JVM. :)


                        У меня есть знакомые, которые после работы на C/Python вообще не понимают зачем нужно ООП. Ведь можно и на основе модулей и без классов строить поддерживаемые приложения. Классы только код тормозят и ещё по памяти дорогие.


                        Вопрос не в можно/нельзя, а на сколько просто решается та или иная задача. Например, я бы не взял с собой Java при написании приложения на акторах. Ведь пользоваться Scala с Akka гораздо более приятно.

                          0
                          Ведь пользоваться Scala с Akka гораздо более приятно.

                          К чему эти полумеры? Берите Эрланг.
                            0

                            Тогда уж Elixir :)

                            0
                            У меня есть знакомые, которые после работы на C/Python вообще не понимают зачем нужно ООП
                            Моя дочь тоже не знает, зачем нужен ООП, ей 4 года и она любит пони. Программист должен знать зачем ООП нужен. Иначе он лишает себя возможности использовать его тогда, когда нужно.
                            Ведь можно и на основе модулей и без классов строить поддерживаемые приложения.
                            Что значит без классов? Может вы имеете ввиду без ООП? Без ООП можно, но стоимость поддержки и модификации будет дороже.
                            Классы только код тормозят и ещё по памяти дорогие.
                            Писать плохой код всегда дорого. Что вы делаете, чтобы ваш код из-за классов тормозил?
                              0
                              Классы только код тормозят и ещё по памяти дорогие.
                              Писать плохой код всегда дорого.


                              Эмммм… В С вообще нет классов и для многих задач это является оптимальным решением. Не очень понял как это связано с плохим кодом. Вообще, складывается впечатление, что вы называете плохим кодом все в чем не разобрались. Помимо ООП есть ещё много хороших практик, которые могут упростить поддержку.
                                0
                                Ваше утверждение о том, что классы тормозят код, совершенно абсурдно. Вы либо не понимаете зачем ООП нужен, либо покупаете красные машины, потому что «da red goez fasta”
                                  0
                                  что классы тормозят код совершенно абсурдно

                                  Почему абсурдно? Создайте переменную типа int и класс с одним полем типа int. И посмотрите сколько занимает в памяти оба варианта. Теперь представьте, что у вас коллекция на 100 млн таких объектов. Ну и еще на работу со структурой класса потребуется доп. процессорное время. Да, очень часто этим пренебрегают ради удобства, которое дает ООП и классы, но ультимативно говорить о том, что разницы нет — это странно.
                                  Вы либо не понимаете зачем ООП нужен либо покупаете красные машины, потому что «da red goez fasta”

                                  Так говорят люди, у которых ООП головного мозга.
                                    0
                                    А кто ультимативно говорит, что разницы нет? Можете процитировать?
                                      0
                                      Ваше утверждение о том, что классы тормозят код, совершенно абсурдно
                                        0
                                        Фраза говорит совершенно о другом, не противоположном, другом
                                      0
                                      Создайте переменную типа int и класс с одним полем типа int. И посмотрите сколько занимает в памяти оба варианта. Теперь представьте, что у вас коллекция на 100 млн таких объектов.

                                      А вы же понимаете, что если программист, например, выбирает вторую реализацию, то это ему необходимо или у него по умолчанию ООП головного мозга? Т.е вы хотите сказать, что выбор реализации зависит исключительно от желания микрооптимизаций в ущерб всему остальному?
                                        0
                                        Т.е вы хотите сказать, что выбор реализации зависит исключительно от желания микрооптимизаций в ущерб всему остальному?

                                        Я хочу сказать, что для разных задач существуют разные подходы. Помимо ООП есть и ФП и процедурное программирование. Желание везде и всюду видеть объекты и пытаться их применить — это и есть ООП головного мозга.
                                          0
                                          С этим утверждением я более чем согласен.
                                          Но не нужно пытаться принимать желаемое за действительное — если программист пишет код таким образом, что «классы его тормозят», виноват ли ООП? Я считаю, что нет.
                                            0
                                            Но не нужно пытаться принимать желаемое за действительное — если программист пишет код таким образом, что «классы его тормозят»

                                            Какое еще желаемое? Я не писал про тормоза. Я писал про оверхед.
                                            виноват ли ООП

                                            ООП не виноват, виновата его реализация. Чужеродные для железа абстракции производительности не прибавляют. Еще виновата голова, которая сует абстракции куда не нужно, только и всего.
                                              0
                                              ООП не виноват, виновата его реализация

                                              Мы же с вами на одной волне!
                                              если программист пишет код таким образом, что «классы его тормозят», виноват ли ООП? Я считаю, что нет.
                                                0
                                                Под реализацией я имел в виду, например, JVM, а не код прикладного программиста. Но переобуваетесь вы ловко)
                                                  0
                                                  Не придумывайте, тогда я не с вами.
                                      0

                                      Я возможно вас удивлю, но существует не только Энтерпрайз разработка. Например, в разработке под МК постоянные аллокации памяти часто нежелательны. Поэтому там редко делается выбор в сторону C++.

                                        0
                                        Вы тогда пожалуйста конкретизируйте на счет какого случая вы пишите. А то пишите абстрактно, я абстрактно отвечаю, а оказывается вы про МК все это время имели ввиду.
                                          +1

                                          C++ отлично живёт и без динамического выделения памяти. Из всех проектов под МК я только в одном использовал голый C.

                                      0
                                      Что значит без классов? Может вы имеете ввиду без ООП?

                                      ООП может быть и без классов.
                                        0
                                        А это например как?
                                          0

                                          ООП не про классы, а про объекты :)

                                            0
                                            Приведите плз пример объекта, который не является экземпляром класса
                                              0

                                              Модули в Python

                                                0
                                                Т.е с модулями возможно наследование/полиморфизм/инкапсуляция?
                                                  +2
                                                  Прототипное наследование в js устроит? Классов там нет, сразу объекты.
                                                    0
                                                    Зачем передергивать? Вопросы был про Python.
                                                    И изначально я спрашивал, чтобы уточнить что имел ввиду автор в конкретном комментарии статьи о java, а не чтобы углубляться в разнообразия языков, где фраза «ООП может быть и без классов» местами имеет право на жизнь, когда как в java нет.
                                                      +1
                                                      Ну модули можно наследовать импортом, динамика типов даёт полиморфизм, ну и приватные методы и переменные тоже можно делать (на сколько это допустимо в языке). Я мог бы написать пример, но обучение вас питону не входит сейчас в мои планы. :)
                                                        –1
                                                        Вряд ли я у вас захочу чему-то учиться.
                                                          0

                                                          Я и не смел надеяться :)

                                                        +1
                                                        Я не передергиваю, а говорю про ООП, а не про питон.
                                              0
                                              Это, например, в JavaScript, где классы появились совсем недавно, до этого отлично без них жили.
                                                0

                                                А да, ещё же старый добрый js с замыканиями вместо классов. :)

                                                  0
                                                  Классы в JS не появились. Появилось слово класс, но внутри это тот самый старый JS.
                                                    0
                                                    Да, но я не стал об этом писать, чтобы не усложнять и не вызывать споров)
                                                    0
                                                    Под капотом там все тоже старое, доброе, выворачивающее мозг, прототипное наследование.
                                                    0
                                                    ООП, это в первую очередь «объектно», а для наличия объекта не обязательно нужен класс. Взять java script в том виде, как он появился — там нет классов, там сплошные объекты. Которые можно наследовать, инкапсулировать и так далее. Прототипное наследование в классах не нуждается. Или взять Эрланг — в нем нет классов от слова «совсем», при этом он считается вполне себе ООП-языком. Надеюсь направление мысли понятно.
                                                      0
                                                      Я теперь понял, что вы имели ввиду.
                                                        0
                                                        AstarothAst, ну а вы не находите, что в статье про java фраза вида
                                                        ООП может быть и без классов.
                                                        выглядит как вброс, могущий породить замешательство и привести к не нужному обсуждению?
                                                          0
                                                          что в статье про java фраза вида

                                                          Вы прекрасно поняли о чем речь и обсуждение выше про Питон это доказывает)
                                                            0
                                                            Ну вот оно и породило, теперь мы там за ООП в питоне трем.
                                                            0
                                                            Какой же это вброс, когда речь про ООП, которое не имеет привязки к конкретному языку программирования? Заговорили вы про ООП в статье по java, или подняли бы тот же вопрос в статье по питону — разницы никакой. Запутать никого не хотел, хорошо, что разобрались.
                                              0
                                              Пишу на Котлин на новой работе примерно уже пару месяцев, до этого его вообще не знал (любопытствовал, но даже до хелловорда не доходил). Пока что главный плюс для меня в сравнении с Явой — более чистый, визуально, код. Тупо «меньше букв», читается намного приятнее. Сам по себе язык менее многословный, плюс необязательность объявления типов в тех местах где они могут быть выведены компилятором.
                                                +1
                                                1. Странно, что автор сравнивает Котлиновские корутины и Джавовые фьючи.
                                                  Корутины не входят в ядро Котлина. Это отдельная либа, которая имплементится в Гредл-файле. В противовес Корутинам автор мог бы использовать Rx в Джаве;
                                                  Кроме того, согласно этой статье корутины не о многопоточности, а о конкурентности. Улавливаете разницу?
                                                2. Большая часть проблем решается использованием библиотек. Лямбды например уже есть в Джаве, так что уже их за преимущество/недостаток можно не считать. Про описание классов — можно использовать AutoValue-Parcel или AutoValue.
                                                  0
                                                  Что же вы про еще один JVM язык забыли — про Clojure? :)
                                                  Если не брать либу clojure.core.async с горутинами, то в стдлибе асинк выглядит как-то так

                                                  (def getResultFromFirstService (promise))
                                                  (defn getResultFromSecondService [v] (println v (+ 100 v)))
                                                  (future (getResultFromSecondService @getResultFromFirstService))
                                                  (deliver getResultFromFirstService 42)
                                                  

                                                  Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                                  Самое читаемое