В последнее время я часто слышу о том, что 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

Комментарии (90)


  1. Avvero
    25.06.2019 20:40
    +3

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

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


    1. Avvero
      25.06.2019 20:55

      Ну вот далее:

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

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

      Простите, но мне кажется, что вы описываете ситуацию, когда плохо написанный код на одном языке более удобен/гибок, чем плохо написанный на другом.


      1. faoxy Автор
        25.06.2019 21:05

        Простите, а вы всегда отделяете контейнеры типов (List, Set, Optional, Completable Future и т.д.) от их содержимого? :)


        1. Avvero
          25.06.2019 21:21

          Я вопрос не понял, но если говорить примерами, то я бы вместо

          private CompletableFuture<Integer> getCalc(int x, int y) {
          }
          

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

          и это метод бы вызывал при необходимости.


    1. faoxy Автор
      25.06.2019 21:28

      Спасибо. Дополнил пример.


      1. Avvero
        25.06.2019 21:44

        Дополнили, но мой поинт бы в другом


        1. faoxy Автор
          25.06.2019 22:17

          Давайте обсудим :)


  1. Avvero
    25.06.2019 21:19

    .


    1. faoxy Автор
      25.06.2019 21:27

      Ну хорошо, представим, что так. Но ведь где-то вы будете оборачивать вызов метода во фьючер? И если у вас асинхронный код, то наверняка вы заходите из каких-то методов возвращать фьючу, ну или хотя бы использовать?


      1. Avvero
        25.06.2019 21:36
        +1

        Вы выше привели пример метода, который вызывается в асинхронном контексте и знает про это, что уже вызывает вопрос — почему метод знает контекст вызова? А потом вы сами справедливо говорите

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

        Так тут не java виновата, а вы, что написали такой метод, который знает (зачем?) контекст вызова и неудобен при вызове в другом контексте.


        1. faoxy Автор
          25.06.2019 21:42

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


          1. Avvero
            25.06.2019 21:47

            Вы слишком цепляетесь к конкретному небольшому примеру, но не видите сути.

            А зачем вы приводите в пример свой идеи код, который написан не правильно с точки зрения дизайна?


            1. Avvero
              25.06.2019 21:47

              Мало того, вы в этом еще и java вините


              1. faoxy Автор
                25.06.2019 22:27

                И я не в чем не обвиняю Java. Просто в разных языках разные инструменты достижения одного результата.


            1. faoxy Автор
              25.06.2019 21:53

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

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

              Суть от этого не меняется, но пример усложняется.


              1. Avvero
                26.06.2019 06:49

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

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


                1. faoxy Автор
                  26.06.2019 08:55

                  Не вижу смысла вести диалог в таком виде. Я пишу большое сообщение, вы отвечаете на первое предложение.


                  1. Avvero
                    26.06.2019 12:19

                    Я отвечаю на комментарий в целом, но соблюдая контекст треда


                    1. faoxy Автор
                      26.06.2019 13:00

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


                      1. Avvero
                        26.06.2019 13:02

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


                        1. faoxy Автор
                          26.06.2019 13:18

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


                          1. Avvero
                            26.06.2019 14:30

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


                            1. faoxy Автор
                              26.06.2019 14:32

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


                              1. Avvero
                                26.06.2019 14:35

                                Не понимаю, как «это» следует из моего утверждения. Но вы же понимаете, что не база вам optional возвращает, а вы в него ответ где-то завернете, так же?


                                1. faoxy Автор
                                  26.06.2019 14:37

                                  Если я использую Spring Data, то получаю Optional прямо от спринга.


                                  1. Avvero
                                    26.06.2019 14:38

                                    И?


                                    1. faoxy Автор
                                      26.06.2019 14:49

                                      То, что со временем мы можем захотеть изменить Optional на какой-нибудь Try и не сможем. Вот об этом и был пример.


                                      1. Avvero
                                        26.06.2019 14:53

                                        Не понимаю вас. Вот вы получаете Optional из spring data, а я нет — я всегда объекты или null.
                                        И вы со временем хотите поменять что? реализацию spring data?


                                        1. faoxy Автор
                                          26.06.2019 14:55

                                          Я советую более внимательно ознакомиться со статьей. Не вижу смысла продолжать.


                                          1. Avvero
                                            26.06.2019 15:00

                                            Согласен. И в комментариях к статье я выразил всё своё беспокойство касательно того, что в угоду доказательства «бессильности» java вы приводите в пример плохо написанный код.


                                            1. faoxy Автор
                                              26.06.2019 15:08

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


                                              1. Avvero
                                                26.06.2019 15:16

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

                                                В последнее время я часто слышу о том, что Java стала устаревшим языком, на котором сложно строить большие поддерживаемые приложения


                                                1. faoxy Автор
                                                  26.06.2019 16:37

                                                  Вы просто не поняли пример :)


                                                  1. Avvero
                                                    27.06.2019 03:26

                                                    Вы не хотите меня услышать — если ваши примеры написать на java грамотно, то в них не будет тех выдуманных проблем, что вы пытаетесь решить.
                                                    Я допускаю, что вы плохие примеры привели, хотя после исправления они не стали лучше :/


    1. faoxy Автор
      25.06.2019 21:30
      +1

      Удалять/полностью менять свои сообщения после ответа не очень хорошо :)


      1. Avvero
        25.06.2019 21:32

        Простите, я не на тот комментарий ответил, поэтому тут стер, а выше вставил.


  1. maxzh83
    25.06.2019 22:12
    +1

    иногда думаешь: “как бы хорошо это решилось вот этой штукой из другого языка”

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


    1. faoxy Автор
      25.06.2019 22:46

      Никто же не говорит, что на скале все круче. Мне, например, дико не нравится sbt. Кроме того, меня самого устрашают сервисы, которые делаеют долго и упорно на скале, но которые могли бы появиться по щелчку пальцев на спринге. Тут все зависит от задачи.


  1. Beholder
    25.06.2019 22:17
    +1

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

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


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


    1. faoxy Автор
      25.06.2019 22:40

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


      1. Beholder
        25.06.2019 23:28
        +1

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


      1. Avvero
        26.06.2019 06:12

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

        В последнее время я часто слышу о том, что Java стала устаревшим языком, на котором сложно строить большие поддерживаемые приложения

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

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


        1. Beholder
          26.06.2019 13:29

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


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

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


          1. Avvero
            26.06.2019 13:45

            Посыпаю голову пеплом, был не прав, спасибо что объяснили.
            Я так понимаю, что в приведенном примере вы расширяете this класс. А какое это дает преимущество в сравнении с простым методом?


  1. Avvero
    26.06.2019 07:25

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

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


    1. faoxy Автор
      26.06.2019 09:47

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


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


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


      1. AstarothAst
        26.06.2019 11:36

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

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


        1. faoxy Автор
          26.06.2019 11:46

          Тогда уж Elixir :)


      1. Avvero
        26.06.2019 11:58

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


        1. faoxy Автор
          26.06.2019 12:17

          Классы только код тормозят и ещё по памяти дорогие.
          Писать плохой код всегда дорого.


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


          1. Avvero
            26.06.2019 12:27

            Ваше утверждение о том, что классы тормозят код, совершенно абсурдно. Вы либо не понимаете зачем ООП нужен, либо покупаете красные машины, потому что «da red goez fasta”


            1. maxzh83
              26.06.2019 12:36

              что классы тормозят код совершенно абсурдно

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

              Так говорят люди, у которых ООП головного мозга.


              1. Avvero
                26.06.2019 12:44

                А кто ультимативно говорит, что разницы нет? Можете процитировать?


                1. maxzh83
                  26.06.2019 12:56

                  Ваше утверждение о том, что классы тормозят код, совершенно абсурдно


                  1. Avvero
                    26.06.2019 14:07

                    Фраза говорит совершенно о другом, не противоположном, другом


              1. Avvero
                26.06.2019 12:50

                Создайте переменную типа int и класс с одним полем типа int. И посмотрите сколько занимает в памяти оба варианта. Теперь представьте, что у вас коллекция на 100 млн таких объектов.

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


                1. maxzh83
                  26.06.2019 12:59

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

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


                  1. Avvero
                    26.06.2019 14:10

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


                    1. maxzh83
                      26.06.2019 14:38

                      Но не нужно пытаться принимать желаемое за действительное — если программист пишет код таким образом, что «классы его тормозят»

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

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


                      1. Avvero
                        26.06.2019 14:46

                        ООП не виноват, виновата его реализация

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


                        1. maxzh83
                          26.06.2019 14:49

                          Под реализацией я имел в виду, например, JVM, а не код прикладного программиста. Но переобуваетесь вы ловко)


                          1. Avvero
                            26.06.2019 14:51

                            Не придумывайте, тогда я не с вами.


            1. faoxy Автор
              26.06.2019 13:09

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


              1. Avvero
                26.06.2019 13:42

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


        1. AstarothAst
          26.06.2019 12:46

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

          ООП может быть и без классов.


          1. Avvero
            26.06.2019 12:53

            А это например как?


            1. faoxy Автор
              26.06.2019 12:57

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


              1. Avvero
                26.06.2019 12:59

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


                1. faoxy Автор
                  26.06.2019 13:09

                  Модули в Python


                  1. Avvero
                    26.06.2019 13:30

                    Т.е с модулями возможно наследование/полиморфизм/инкапсуляция?


                    1. AstarothAst
                      26.06.2019 13:45

                      Прототипное наследование в js устроит? Классов там нет, сразу объекты.


                      1. Avvero
                        26.06.2019 13:57

                        Зачем передергивать? Вопросы был про Python.
                        И изначально я спрашивал, чтобы уточнить что имел ввиду автор в конкретном комментарии статьи о java, а не чтобы углубляться в разнообразия языков, где фраза «ООП может быть и без классов» местами имеет право на жизнь, когда как в java нет.


                        1. faoxy Автор
                          26.06.2019 16:41

                          Ну модули можно наследовать импортом, динамика типов даёт полиморфизм, ну и приватные методы и переменные тоже можно делать (на сколько это допустимо в языке). Я мог бы написать пример, но обучение вас питону не входит сейчас в мои планы. :)


                          1. Avvero
                            26.06.2019 17:09

                            Вряд ли я у вас захочу чему-то учиться.


                            1. faoxy Автор
                              26.06.2019 18:27

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


                        1. AstarothAst
                          26.06.2019 21:12

                          Я не передергиваю, а говорю про ООП, а не про питон.


            1. maxzh83
              26.06.2019 13:06

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


              1. faoxy Автор
                26.06.2019 13:19

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


              1. aPiks
                26.06.2019 13:35

                Классы в JS не появились. Появилось слово класс, но внутри это тот самый старый JS.


                1. maxzh83
                  26.06.2019 13:59

                  Да, но я не стал об этом писать, чтобы не усложнять и не вызывать споров)


              1. AstarothAst
                26.06.2019 13:46

                Под капотом там все тоже старое, доброе, выворачивающее мозг, прототипное наследование.


            1. AstarothAst
              26.06.2019 13:45

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


              1. Avvero
                26.06.2019 13:59

                Я теперь понял, что вы имели ввиду.


                1. Avvero
                  26.06.2019 14:03

                  AstarothAst, ну а вы не находите, что в статье про java фраза вида

                  ООП может быть и без классов.
                  выглядит как вброс, могущий породить замешательство и привести к не нужному обсуждению?


                  1. maxzh83
                    26.06.2019 14:50

                    что в статье про java фраза вида

                    Вы прекрасно поняли о чем речь и обсуждение выше про Питон это доказывает)


                    1. Avvero
                      26.06.2019 14:55

                      Ну вот оно и породило, теперь мы там за ООП в питоне трем.


                  1. AstarothAst
                    26.06.2019 21:16

                    Какой же это вброс, когда речь про ООП, которое не имеет привязки к конкретному языку программирования? Заговорили вы про ООП в статье по java, или подняли бы тот же вопрос в статье по питону — разницы никакой. Запутать никого не хотел, хорошо, что разобрались.


  1. vvbob
    26.06.2019 09:10

    Пишу на Котлин на новой работе примерно уже пару месяцев, до этого его вообще не знал (любопытствовал, но даже до хелловорда не доходил). Пока что главный плюс для меня в сравнении с Явой — более чистый, визуально, код. Тупо «меньше букв», читается намного приятнее. Сам по себе язык менее многословный, плюс необязательность объявления типов в тех местах где они могут быть выведены компилятором.


  1. Papashkin
    26.06.2019 12:50

    1. Странно, что автор сравнивает Котлиновские корутины и Джавовые фьючи.
      Корутины не входят в ядро Котлина. Это отдельная либа, которая имплементится в Гредл-файле. В противовес Корутинам автор мог бы использовать Rx в Джаве;
      Кроме того, согласно этой статье корутины не о многопоточности, а о конкурентности. Улавливаете разницу?
    2. Большая часть проблем решается использованием библиотек. Лямбды например уже есть в Джаве, так что уже их за преимущество/недостаток можно не считать. Про описание классов — можно использовать AutoValue-Parcel или AutoValue.


  1. v_m_smith
    26.06.2019 20:36

    Что же вы про еще один JVM язык забыли — про Clojure? :)
    Если не брать либу clojure.core.async с горутинами, то в стдлибе асинк выглядит как-то так

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