Как стать автором
Обновить

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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

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

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

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

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

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


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

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

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

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

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


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


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

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

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

Тогда уж Elixir :)

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


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Модули в Python

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

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

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

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

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

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

(def getResultFromFirstService (promise))
(defn getResultFromSecondService [v] (println v (+ 100 v)))
(future (getResultFromSecondService @getResultFromFirstService))
(deliver getResultFromFirstService 42)
С примерами по многопоточности не то что-то.
Попробуем сначала решить задачу на 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
                                )
                        )
                );
    }


Зачем использовать thenApplyAsync, если мы не меняем Executor? В вашем коде вся логика будет выполняться в ForkJoinPool.commonPool(). Можно тогда и простым thenApply обойтись. К тому же, не будет теряться время на переключение контекста между окончанием работы первого сервиса и началом работы второго. Хотя, возможно, в реализации CompletableFuture есть оптимизации на этот счёт.
А как же Kotlin? Давайте попробуем сделать что-то подобное на корутинах.
val result = async {
        withContext(Dispatchers.Default) { getResultFromFirstService() }?.let { first ->
            withContext(Dispatchers.Default) { getResultFromSecondService(first) }?.let { second ->
                "$first $second"
            }
        }
    }


Тут вы, как и в примере на Java, хотите с помощью функции (не ключевое слово, кстати, как и async) withContext() насильно заставить второй сервис выполняться в отличном от исходного диспетчере, а значит и, потенциально, в другом потоке. Зачем? Ну отправит она эту задачу на выполнение в другой поток (возможно), всё равно же он прерывается до окончания выполнения.
В итоге можно прийти к более простому примеру:
    val result = GlobalScope.async {
        getResultFromFirstService()?.let { first ->
            getResultFromSecondService(first)?.let { second ->
                "$first $second"
            }
        }
    }

Конечно, оговорюсь, что из вашего примера непонятно, на каком СoroutineScope вызывается async, но тогда этот пример тем более неудачный, так как вы про это нигде не говорили.

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

Более того, в java уже можно потрогать project loom, и с ним код сильно упростится и распрямится.

Спасибо за своевременный комментарий!)))


Конечно, вы правы в том, что использование thenApplyAsync без специфичного Executor кажется избыточным. Однако, стоит понимать, что демонстрация асинхронных возможностей Java с помощью CompletableFuture в данном случае была направлена на показ возможностей языка, а не на идеальную оптимизацию производительности. Не каждый пример должен быть прямым указанием к действию, иногда это просто демонстрация возможностей.

Что касается вашего комментария о Kotlin и использовании withContext — да, конечно, можно всё сделать проще и понятнее, если использовать GlobalScope.async. Тем не менее, использование различных контекстов исполнения служит для показа гибкости корутин. Мне показалось это очевидным, тк заголовок называется Цепочка многопоточных вычислений, что предполагает понимания причин происходящего.

  1. Расширение существующих классов -> @ExtensionMethod в Lombok'е и @Extensions в Manifold'е

  2. Остальное - ждём от VAVR'а и новых версий Java...

Зарегистрируйтесь на Хабре, чтобы оставить комментарий