Расширение существующих классов
Иногда бывает так, что необходимо расширить существующий класс без изменения его внутреннего содержимого. То есть уже после создания класса мы дополняем его другими классами. Рассмотрим небольшой пример. Пусть у нас есть класс, который представляет собой точку в двумерном пространстве. В разных местах нашего кода нам необходимо сериализовать его и в Json, и в XML.
public class DotDemo {
public static class Dot {
private final int x;
private final int y;
public Dot(int x, int y) {
this.x = x;
this.y = y;
}
public String accept(Visitor visitor) {
return visitor.visit(this);
}
public int getX() { return x; }
public int getY() { return y; }
}
public interface Visitor {
String visit(Dot dot);
}
public static class JsonVisitor implements Visitor {
@Override
public String visit(Dot dot) {
return String
.format("" +
"{" +
"\"x\"=%d, " +
"\"y\"=%d " +
"}",
dot.getX(), dot.getY());
}
}
public static class XMLVisitor implements Visitor {
@Override
public String visit(Dot dot) {
return "<dot>" + "\n" +
" <x>" + dot.getX() + "</x>" + "\n" +
" <y>" + dot.getY() + "</y>" + "\n" +
"</dot>";
}
}
public static void main(String[] args) {
Dot dot = new Dot(1, 2);
System.out.println("-------- JSON -----------");
System.out.println(dot.accept(new JsonVisitor()));
System.out.println("-------- XML ------------");
System.out.println(dot.accept(new XMLVisitor()));
}
}
Более подробно о паттерне и его использовании
Выглядит достаточно объемно, не так ли? Можно ли решить данную задачу более элегантно с помощью вспомогательных средств языка? Scala и Kotlin кивают положительно. Это достигается с помощью механизма method extension. Посмотрим, как это выглядит.
data class Dot (val x: Int, val y: Int)
// неявно получаем ссылку на объект
fun Dot.convertToJson(): String =
"{\"x\"=$x, \"y\"=$y}"
fun Dot.convertToXml(): String =
"""<dot>
<x>$x</x>
<y>$y</y>
</dot>"""
fun main() {
val dot = Dot(1, 2)
println("-------- JSON -----------")
println(dot.convertToJson())
println("-------- XML -----------")
println(dot.convertToXml())
}
object DotDemo extends App {
// val is default
case class Dot(x: Int, y: Int)
implicit class DotConverters(dot: Dot) {
def convertToJson(): String =
s"""{"x"=${dot.x}, "y"=${dot.y}}"""
def convertToXml(): String =
s"""<dot>
<x>${dot.x}</x>
<y>${dot.y}</y>
</dot>"""
}
val dot = Dot(1, 2)
println("-------- JSON -----------")
println(dot.convertToJson())
println("-------- XML -----------")
println(dot.convertToXml())
}
Смотрится намного лучше. Иногда этого очень не хватает при обильных маппингах и прочих преобразованиях.
Цепочка многопоточных вычислений
Сейчас все говорят про асинхронные вычисления и о запретах на блокировку в исполняющих потоках. Давайте представим такую задачу: у нас есть несколько источников чисел, где первый просто так отдает число, второй — возвращает ответ после вычисления по первому. В результате мы должны вернуть строку с двумя числами.
Попробуем сначала решить задачу на Java
private static CompletableFuture<Optional<String>> calcResultOfTwoServices (
Supplier<Optional<Integer>> getResultFromFirstService,
Function<Integer, Optional<Integer>> getResultFromSecondService
) {
return CompletableFuture
.supplyAsync(getResultFromFirstService)
.thenApplyAsync(firstResultOptional ->
firstResultOptional.flatMap(first ->
getResultFromSecondService.apply(first).map(second ->
first + " " + second
)
)
);
}
В этом примере наше число оборачивается в Optional для управления результатом. Кроме того, все действия выполняются внутри CompletableFuture для удобной работы с потоками. Основное действие разворачивается в методе thenApplyAsync. В этом методе мы в качестве аргумента получаем Optional. Далее вызывается flatMap для управления контекстом. Если полученный Optional вернулся как Optional.empty, то во второй сервис мы уже не пойдем.
Итого, что мы получили? С помощью CompletableFuture и возможностей Optional c flatMap и map нам удалось решить поставленную задачу. Хотя, на мой взгляд, решение выглядит не самым элегантным образом: прежде чем понять, в чем дело, необходимо вчитываться в код. А что было бы в случае с двумя и более источниками данных?
Мог ли нам как-то помочь решить проблему язык. И снова обратимся к Scala. Вот как это можно решить инструментами Scala.
def calcResultOfTwoServices(getResultFromFirstService: Unit => Option[Int],
getResultFromSecondService: Int => Option[Int]) =
Future {
getResultFromFirstService()
}.flatMap { firsResultOption =>
Future { firsResultOption.flatMap(first =>
getResultFromSecondService(first).map(second =>
s"$first $second"
)
)}
}
Выглядит знакомо. И это не случайно. Здесь используется библиотека scala.concurrent, которая является преимущественно оберткой над java.concurrent. Хорошо, а чем еще нам может помочь язык Scala? Дело в том, что цепочки вида flatMap, …, map можно представить в виде последовательности в for.
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.
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? Давайте попробуем сделать что-то подобное на корутинах.
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.
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.
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 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 в качестве аргумента функции означает забор переменной из окружения. Это чем-то напоминает неявное замыкание.
В совокупности у нас получается, что мы можем создать боевое и тестовое окружение достаточно лаконично без использования сторонних библиотек.
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. Бывают и другие случаи с большим количеством интеграций и агрегацией какой-то информации. Для таких задач, на мой взгляд, последний вариант смотрится очень хорошо. В общем, классно, если вы можете выбирать инструмент отталкиваясь от задачи.
Полезные ресурсы:
Комментарии (90)
Avvero
25.06.2019 21:19.
faoxy Автор
25.06.2019 21:27Ну хорошо, представим, что так. Но ведь где-то вы будете оборачивать вызов метода во фьючер? И если у вас асинхронный код, то наверняка вы заходите из каких-то методов возвращать фьючу, ну или хотя бы использовать?
Avvero
25.06.2019 21:36+1Вы выше привели пример метода, который вызывается в асинхронном контексте и знает про это, что уже вызывает вопрос — почему метод знает контекст вызова? А потом вы сами справедливо говорите
Но что, если вызов данного метода скрыт, а мы хотим провести тестирование в однопоточной среде? Или что, если мы хотим поменять реализацию класса, убрав/заменив CompletableFuture. К сожалению, в Java мы бессильны и должны поменять API метода.
Так тут не java виновата, а вы, что написали такой метод, который знает (зачем?) контекст вызова и неудобен при вызове в другом контексте.faoxy Автор
25.06.2019 21:42Во-первых, здесь я хотел показать, что так можно в принципе. При простом сложении такого точно не нужно. Вы слишком цепляетесь к конкретному небольшому примеру, но не видите сути.
Во-вторых, вы же согласны, что возращать контейнеры типов можно и это нормально? Тот же Optional.Avvero
25.06.2019 21:47Вы слишком цепляетесь к конкретному небольшому примеру, но не видите сути.
А зачем вы приводите в пример свой идеи код, который написан не правильно с точки зрения дизайна?faoxy Автор
25.06.2019 21:53Мне показалось очевидным странность сложения двух чисел в отдельном потоке. Одно выделение времени планировщика будет дороже. Представьте, что внутри более сложные операции, которые выполняются асинхронно. Ну, например, у нас в методе не сложение, а логика вызова 20 других сервисов, а ваше приложение под нагрузкой. Вряд ли вы захотите возвращать просто результат выполнения. Наверняка вы будете внутри проводить манипуляции с фьючерами и в итоге вернете фьючер для максимальной нагрузки ЦП без простоя на IO. Нет?
Ну или можете заменить CompletableFuture на Optional, если вы совсем придирчивы. Можем даже придумать требование: возвращение empty, если сложение привело к битовому переполнению.
Суть от этого не меняется, но пример усложняется.Avvero
26.06.2019 06:49Мне показалось очевидным странность сложения двух чисел в отдельном потоке.
Вы обращали внимание, как выглядит код, позволяющий использовать стримы в Java коде? Он позволяет вам обстрагировать бизнес логику «сложения двух чисел» от интеграционного кода, который метод с этой логикой вызывает.
И я вообще не вижу причин эти два кода мешать в один. Иначе это приведет к программистскому бессилию.
Но что, если вызов данного метода скрыт, а мы хотим провести тестирование в однопоточной среде? Или что, если мы хотим поменять реализацию класса, убрав/заменив CompletableFuture. К сожалению, в Java мы бессильны
faoxy Автор
26.06.2019 08:55Не вижу смысла вести диалог в таком виде. Я пишу большое сообщение, вы отвечаете на первое предложение.
Avvero
26.06.2019 12:19Я отвечаю на комментарий в целом, но соблюдая контекст треда
faoxy Автор
26.06.2019 13:00Ну так, а что вы ответите про Optional или асинхронные вычисления??? :)
Avvero
26.06.2019 13:02А я же вам ответил, вы не стали читать или не поняли?
faoxy Автор
26.06.2019 13:18Не понял вашу мысль (
Avvero
26.06.2019 14:30Я имел ввиду, чтобы дизайн кода должен быть таким, чтобы код бизнес логики был отделен от контекста ее использования и про него не знал.
Например существует метод Math.max(), это хороший метод. Могу ли я его использовать в «асинхронных» вычислениях? Могу. Нужно ли методу в контракте иметь CompletableFuture или Optional? Нет, не нужно.faoxy Автор
26.06.2019 14:32То есть, если я получаю optional с результатом запроса из базы, то мой код плох и надо было в случае отсутствия информации возвращать null?
Avvero
26.06.2019 14:35Не понимаю, как «это» следует из моего утверждения. Но вы же понимаете, что не база вам optional возвращает, а вы в него ответ где-то завернете, так же?
faoxy Автор
26.06.2019 14:37Если я использую Spring Data, то получаю Optional прямо от спринга.
Avvero
26.06.2019 14:38И?
faoxy Автор
26.06.2019 14:49То, что со временем мы можем захотеть изменить Optional на какой-нибудь Try и не сможем. Вот об этом и был пример.
Avvero
26.06.2019 14:53Не понимаю вас. Вот вы получаете Optional из spring data, а я нет — я всегда объекты или null.
И вы со временем хотите поменять что? реализацию spring data?faoxy Автор
26.06.2019 14:55Я советую более внимательно ознакомиться со статьей. Не вижу смысла продолжать.
Avvero
26.06.2019 15:00Согласен. И в комментариях к статье я выразил всё своё беспокойство касательно того, что в угоду доказательства «бессильности» java вы приводите в пример плохо написанный код.
faoxy Автор
26.06.2019 15:08Откройте почти любую документацию и вы увидите там игрушечные примеры. Так же и здесь. Просто вы не разобрались с материалом. А доказывать, что в Java нет kind'ов мне надоело. :)
Avvero
26.06.2019 15:16Это не документация, это сравнение возможностей языка через призму примеров из плохо написанного кода. Профанация.
Вы это тут пишите, народ потом читает и начинает перешептываться. И в итоге мы имеем
В последнее время я часто слышу о том, что Java стала устаревшим языком, на котором сложно строить большие поддерживаемые приложения
faoxy Автор
26.06.2019 16:37Вы просто не поняли пример :)
Avvero
27.06.2019 03:26Вы не хотите меня услышать — если ваши примеры написать на java грамотно, то в них не будет тех выдуманных проблем, что вы пытаетесь решить.
Я допускаю, что вы плохие примеры привели, хотя после исправления они не стали лучше :/
maxzh83
25.06.2019 22:12+1иногда думаешь: “как бы хорошо это решилось вот этой штукой из другого языка”
А я часто, читая чужой код на Java, думаю: «как же хорошо, что это не Scala и тут я не буду ломать голову из какого implicit что-то прилетело и откуда взялся метод, которого не должно быть». И я не против Scala, если что. Это очень крутой язык. Просто он со своей философией и для другого. И вот поэтому я против того, чтобы все языки в конце концов превратились в один. Если на Котлине/Scala все круче, так может просто не использовать Java?faoxy Автор
25.06.2019 22:46Никто же не говорит, что на скале все круче. Мне, например, дико не нравится sbt. Кроме того, меня самого устрашают сервисы, которые делаеют долго и упорно на скале, но которые могли бы появиться по щелчку пальцев на спринге. Тут все зависит от задачи.
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)) } }
Здесь тоже задаём разный контекст, но только явно, и не ломаем голову откуда он взялся.
Пример с визитором неудачный, так как для визиторов важен динамический диспатч, а методы-расширения дают статический.
faoxy Автор
25.06.2019 22:40Отличный пример! Добавлю его в статью, если вы не против?
Avvero
26.06.2019 06:12Вы правда считаете это пример отличным? Получается, что это нормально объявить интерфейс и ниже по коду расширить его. Какая цель такого дизайна? Заставить разработчика страдать от знания того, что чтобы понять, что интерфейс предоставляет, не достаточно посмотреть на его код. Нужно еще прочитать кучу кода вокруг, выискивая где хитрый автор расширил этот интерфейс еще.
Вы начали статью с
В последнее время я часто слышу о том, что Java стала устаревшим языком, на котором сложно строить большие поддерживаемые приложения
И правы лишь в одном — если писать код так, как описано в ваших примерах и как в примере уважаемого Beholder, то приложение далеко не поплывет.
В этом и была моя претензия к вашим примерам — вы приводите плохой код на java и говорите, что это ваша боль. Я вижу, что причина вашей боли не в java.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.Avvero
26.06.2019 13:45Посыпаю голову пеплом, был не прав, спасибо что объяснили.
Я так понимаю, что в приведенном примере вы расширяете this класс. А какое это дает преимущество в сравнении с простым методом?
Avvero
26.06.2019 07:25В последнее время я часто слышу о том, что Java стала устаревшим языком, на котором сложно строить большие поддерживаемые приложения. В целом, я не согласен с этой точкой зрения. На мой взгляд, язык все еще подходит для написания быстрых и хорошо организованных приложений.
Так все-таки, java, по вашему мнению, еще подходит для написания больших поддерживаемых приложений или нет?faoxy Автор
26.06.2019 09:47Конечно! Можно писать такие приложения и на С. Посмотрите на JVM. :)
У меня есть знакомые, которые после работы на C/Python вообще не понимают зачем нужно ООП. Ведь можно и на основе модулей и без классов строить поддерживаемые приложения. Классы только код тормозят и ещё по памяти дорогие.
Вопрос не в можно/нельзя, а на сколько просто решается та или иная задача. Например, я бы не взял с собой Java при написании приложения на акторах. Ведь пользоваться Scala с Akka гораздо более приятно.
AstarothAst
26.06.2019 11:36Ведь пользоваться Scala с Akka гораздо более приятно.
К чему эти полумеры? Берите Эрланг.
Avvero
26.06.2019 11:58У меня есть знакомые, которые после работы на C/Python вообще не понимают зачем нужно ООП
Моя дочь тоже не знает, зачем нужен ООП, ей 4 года и она любит пони. Программист должен знать зачем ООП нужен. Иначе он лишает себя возможности использовать его тогда, когда нужно.
Ведь можно и на основе модулей и без классов строить поддерживаемые приложения.
Что значит без классов? Может вы имеете ввиду без ООП? Без ООП можно, но стоимость поддержки и модификации будет дороже.
Классы только код тормозят и ещё по памяти дорогие.
Писать плохой код всегда дорого. Что вы делаете, чтобы ваш код из-за классов тормозил?faoxy Автор
26.06.2019 12:17Классы только код тормозят и ещё по памяти дорогие.
Писать плохой код всегда дорого.
Эмммм… В С вообще нет классов и для многих задач это является оптимальным решением. Не очень понял как это связано с плохим кодом. Вообще, складывается впечатление, что вы называете плохим кодом все в чем не разобрались. Помимо ООП есть ещё много хороших практик, которые могут упростить поддержку.Avvero
26.06.2019 12:27Ваше утверждение о том, что классы тормозят код, совершенно абсурдно. Вы либо не понимаете зачем ООП нужен, либо покупаете красные машины, потому что «da red goez fasta”
maxzh83
26.06.2019 12:36что классы тормозят код совершенно абсурдно
Почему абсурдно? Создайте переменную типа int и класс с одним полем типа int. И посмотрите сколько занимает в памяти оба варианта. Теперь представьте, что у вас коллекция на 100 млн таких объектов. Ну и еще на работу со структурой класса потребуется доп. процессорное время. Да, очень часто этим пренебрегают ради удобства, которое дает ООП и классы, но ультимативно говорить о том, что разницы нет — это странно.
Вы либо не понимаете зачем ООП нужен либо покупаете красные машины, потому что «da red goez fasta”
Так говорят люди, у которых ООП головного мозга.Avvero
26.06.2019 12:50Создайте переменную типа int и класс с одним полем типа int. И посмотрите сколько занимает в памяти оба варианта. Теперь представьте, что у вас коллекция на 100 млн таких объектов.
А вы же понимаете, что если программист, например, выбирает вторую реализацию, то это ему необходимо или у него по умолчанию ООП головного мозга? Т.е вы хотите сказать, что выбор реализации зависит исключительно от желания микрооптимизаций в ущерб всему остальному?maxzh83
26.06.2019 12:59Т.е вы хотите сказать, что выбор реализации зависит исключительно от желания микрооптимизаций в ущерб всему остальному?
Я хочу сказать, что для разных задач существуют разные подходы. Помимо ООП есть и ФП и процедурное программирование. Желание везде и всюду видеть объекты и пытаться их применить — это и есть ООП головного мозга.Avvero
26.06.2019 14:10С этим утверждением я более чем согласен.
Но не нужно пытаться принимать желаемое за действительное — если программист пишет код таким образом, что «классы его тормозят», виноват ли ООП? Я считаю, что нет.maxzh83
26.06.2019 14:38Но не нужно пытаться принимать желаемое за действительное — если программист пишет код таким образом, что «классы его тормозят»
Какое еще желаемое? Я не писал про тормоза. Я писал про оверхед.
виноват ли ООП
ООП не виноват, виновата его реализация. Чужеродные для железа абстракции производительности не прибавляют. Еще виновата голова, которая сует абстракции куда не нужно, только и всего.Avvero
26.06.2019 14:46ООП не виноват, виновата его реализация
Мы же с вами на одной волне!
если программист пишет код таким образом, что «классы его тормозят», виноват ли ООП? Я считаю, что нет.
faoxy Автор
26.06.2019 13:09Я возможно вас удивлю, но существует не только Энтерпрайз разработка. Например, в разработке под МК постоянные аллокации памяти часто нежелательны. Поэтому там редко делается выбор в сторону C++.
Avvero
26.06.2019 13:42Вы тогда пожалуйста конкретизируйте на счет какого случая вы пишите. А то пишите абстрактно, я абстрактно отвечаю, а оказывается вы про МК все это время имели ввиду.
AstarothAst
26.06.2019 12:46Что значит без классов? Может вы имеете ввиду без ООП?
ООП может быть и без классов.Avvero
26.06.2019 12:53А это например как?
faoxy Автор
26.06.2019 12:57ООП не про классы, а про объекты :)
Avvero
26.06.2019 12:59Приведите плз пример объекта, который не является экземпляром класса
faoxy Автор
26.06.2019 13:09Модули в Python
Avvero
26.06.2019 13:30Т.е с модулями возможно наследование/полиморфизм/инкапсуляция?
AstarothAst
26.06.2019 13:45Прототипное наследование в js устроит? Классов там нет, сразу объекты.
Avvero
26.06.2019 13:57Зачем передергивать? Вопросы был про Python.
И изначально я спрашивал, чтобы уточнить что имел ввиду автор в конкретном комментарии статьи о java, а не чтобы углубляться в разнообразия языков, где фраза «ООП может быть и без классов» местами имеет право на жизнь, когда как в java нет.faoxy Автор
26.06.2019 16:41Ну модули можно наследовать импортом, динамика типов даёт полиморфизм, ну и приватные методы и переменные тоже можно делать (на сколько это допустимо в языке). Я мог бы написать пример, но обучение вас питону не входит сейчас в мои планы. :)
maxzh83
26.06.2019 13:06Это, например, в JavaScript, где классы появились совсем недавно, до этого отлично без них жили.
AstarothAst
26.06.2019 13:46Под капотом там все тоже старое, доброе, выворачивающее мозг, прототипное наследование.
AstarothAst
26.06.2019 13:45ООП, это в первую очередь «объектно», а для наличия объекта не обязательно нужен класс. Взять java script в том виде, как он появился — там нет классов, там сплошные объекты. Которые можно наследовать, инкапсулировать и так далее. Прототипное наследование в классах не нуждается. Или взять Эрланг — в нем нет классов от слова «совсем», при этом он считается вполне себе ООП-языком. Надеюсь направление мысли понятно.
Avvero
26.06.2019 13:59Я теперь понял, что вы имели ввиду.
Avvero
26.06.2019 14:03AstarothAst, ну а вы не находите, что в статье про java фраза вида
ООП может быть и без классов.
выглядит как вброс, могущий породить замешательство и привести к не нужному обсуждению?AstarothAst
26.06.2019 21:16Какой же это вброс, когда речь про ООП, которое не имеет привязки к конкретному языку программирования? Заговорили вы про ООП в статье по java, или подняли бы тот же вопрос в статье по питону — разницы никакой. Запутать никого не хотел, хорошо, что разобрались.
vvbob
26.06.2019 09:10Пишу на Котлин на новой работе примерно уже пару месяцев, до этого его вообще не знал (любопытствовал, но даже до хелловорда не доходил). Пока что главный плюс для меня в сравнении с Явой — более чистый, визуально, код. Тупо «меньше букв», читается намного приятнее. Сам по себе язык менее многословный, плюс необязательность объявления типов в тех местах где они могут быть выведены компилятором.
Papashkin
26.06.2019 12:50- Странно, что автор сравнивает Котлиновские корутины и Джавовые фьючи.
Корутины не входят в ядро Котлина. Это отдельная либа, которая имплементится в Гредл-файле. В противовес Корутинам автор мог бы использовать Rx в Джаве;
Кроме того, согласно этой статье корутины не о многопоточности, а о конкурентности. Улавливаете разницу? - Большая часть проблем решается использованием библиотек. Лямбды например уже есть в Джаве, так что уже их за преимущество/недостаток можно не считать. Про описание классов — можно использовать
AutoValue-Parcel
илиAutoValue
.
- Странно, что автор сравнивает Котлиновские корутины и Джавовые фьючи.
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)
Avvero
Возможно Вы выбрали не удачный пример чтобы донести мысль, потому что появление такой необходимости и предложенное Вами решение говорит о плохом дизайне кода. Как минимум, сериализация не должна быть частью поведения класса «точка», как максимум «The Open Closed Principle» диктует другой подход к реализации ожидаемого поведения.
Avvero
Ну вот далее:
Это «мы» бессильный, если строим код таким образом, что он не позволяет отделить бизнес логику от интеграционной.
Простите, но мне кажется, что вы описываете ситуацию, когда плохо написанный код на одном языке более удобен/гибок, чем плохо написанный на другом.faoxy Автор
Простите, а вы всегда отделяете контейнеры типов (List, Set, Optional, Completable Future и т.д.) от их содержимого? :)
Avvero
Я вопрос не понял, но если говорить примерами, то я бы вместо
написал
и это метод бы вызывал при необходимости.
faoxy Автор
Спасибо. Дополнил пример.
Avvero
Дополнили, но мой поинт бы в другом
faoxy Автор
Давайте обсудим :)