Гроккинговая культура Java - почему все так тяжело? Для чего это оптимизировать? [закрыто]


288

Я много программировал на Python. Теперь по соображениям работы я пишу код на Java. Проекты, которые я делаю, довольно маленькие, и, возможно, Python будет работать лучше, но есть веские неинженерные причины для использования Java (я не могу вдаваться в подробности).

Синтаксис Java не проблема; это просто другой язык. Но кроме синтаксиса, у Java есть культура, набор методов разработки и практики, которые считаются «правильными». И сейчас я совершенно не в состоянии "проглотить" эту культуру. Поэтому я был бы очень признателен за объяснения или указатели в правильном направлении.

Минимальный полный пример доступен в вопросе переполнения стека, который я начал: https://stackoverflow.com/questions/43619566/returning-a-result-with-several-values-the-java-way/43620339

У меня есть задача - разобрать (из одной строки) и обработать набор из трех значений. В Python это однострочная (кортеж), в Pascal или C 5-строчная запись / структура.

Согласно ответам, эквивалент структуры доступен в синтаксисе Java, а тройка доступна в широко используемой библиотеке Apache - однако «правильный» способ сделать это - создать отдельный класс для значения, дополненный добытчики и сеттеры. Кто-то был очень любезен, чтобы привести полный пример. Это было 47 строк кода (ну, некоторые из этих строк были пустыми).

Я понимаю, что огромное сообщество разработчиков, скорее всего, не "неправильно". Так что это проблема с моим пониманием.

Практики Python оптимизируют для удобочитаемости (что в этой философии приводит к удобству обслуживания), а затем и скорости разработки. С практики оптимизируют для использования ресурсов. Что оптимизируют практики Java? Мое лучшее предположение - масштабируемость (все должно быть в состоянии, готовом для проекта с миллионами LOC), но это очень слабое предположение.


1
не ответит на технические аспекты вопроса, но все еще в контексте: softwareengineering.stackexchange.com/a/63145/13899
Newtopian

74
Возможно, вам понравится сочинение Стива Йегге « Казнь в царстве существительных»
полковник Паник


3
Вот то, что я считаю правильным ответом. Вы не впадаете в Java, потому что думаете о программировании как системный администратор, а не как программист. Для вас программное обеспечение - это то, что вы используете для достижения конкретной задачи наиболее подходящим способом. Я пишу Java-код в течение 20 лет, и для выполнения некоторых проектов, над которыми я работал, понадобилось 20,2 года. Java не является заменой для Python и наоборот. Они оба делают свою работу, но их работа совершенно другая.
Ричард

1
Причина, по которой Java является стандартом де-факто, заключается в том, что он просто правильный. Впервые он был создан правильно серьезными людьми, у которых были бороды до того, как бороды стали модными. Если вы привыкли запускать скрипты оболочки для манипулирования файлами, Java кажется недопустимо раздутым. Но если вы хотите создать кластер с двумя резервированными серверами, способный обслуживать 50 миллионов человек в день, подкрепленный кластерным кэшированием redis, 3 биллинговыми системами и кластером базы данных Oracle размером 20 миллионов фунтов стерлингов. Вы не собираетесь использовать python, даже если обращаетесь к база данных в Python на 25 строк меньше кода.
Ричард

Ответы:


237

Язык Java

Я считаю, что во всех этих ответах отсутствует смысл, пытаясь приписать намерения тому, как работает Java. Многословие Java не связано с его объектно-ориентированным подходом, поскольку Python и многие другие языки также имеют более жесткий синтаксис. Многословие Java также не связано с поддержкой модификаторов доступа. Это просто то, как Java была разработана и развивалась.

Изначально Java был создан как слегка улучшенный C с ОО. Таким образом, Java имеет синтаксис эпохи 70-х годов. Кроме того, Java очень консервативно относится к добавлению функций, чтобы сохранить обратную совместимость и позволить ей выдержать испытание временем. Если бы в 2005 году Java добавила модные функции, такие как XML-литералы, когда XML был в моде, язык был бы раздут с призрачными функциями, о которых никто не заботится, и которые ограничивали бы его развитие 10 лет спустя. Поэтому в Java просто не хватает современного синтаксиса для краткого выражения концепций.

Тем не менее, нет ничего фундаментального, препятствующего Java принять этот синтаксис. Например, в Java 8 добавлены лямбда-выражения и ссылки на методы, что значительно уменьшает многословие во многих ситуациях. Java также может добавить поддержку для компактных объявлений типов данных, таких как классы case Scala. Но Java просто не сделала этого. Обратите внимание, что пользовательские типы значений находятся на горизонте, и эта функция может ввести новый синтаксис для их объявления. Я полагаю, мы увидим.


Культура Java

История развития корпоративной Java в значительной степени привела нас к культуре, которую мы видим сегодня. В конце 90-х - начале 00-х годов Java стал чрезвычайно популярным языком для серверных бизнес-приложений. В то время эти приложения были в значительной степени написаны специально и включали множество сложных задач, таких как HTTP API, базы данных и обработка XML-каналов.

В 00-х годах стало ясно, что многие из этих приложений имеют много общего, и популярными стали среды для управления этими проблемами, такие как Hibernate ORM, анализатор XML Xerces, JSP и API сервлетов, а также EJB. Однако, несмотря на то, что эти платформы уменьшили усилия для работы в конкретной области, которую они настроили для автоматизации, они требовали конфигурации и координации. В то время, по какой-либо причине, было популярно писать фреймворки для удовлетворения самого сложного варианта использования, и поэтому эти библиотеки были сложны в настройке и интеграции. И со временем они становились все более сложными, поскольку они накапливали особенности. Корпоративное развитие Java постепенно становилось все более и более связано с подключением сторонних библиотек, а не с написанием алгоритмов.

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

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


15
Что забавно, так это то, что другие языки, похоже, воспринимают некоторые «java-измы». Возможности PHP OO в значительной степени основаны на Java, и в настоящее время JavaScript часто критикуют за огромное количество фреймворков и то, как трудно собрать все зависимости и запустить новое приложение.
Marcus

14
@ marcus, может быть, потому, что некоторые люди наконец-то научились «не изобретать велосипед»? Зависимости - это цена не изобретать колесо в конце концов
Walfrat

9
«Terse» часто переводится как «загадочные», «умные» , односоставные .
Тулаинс Кордова

7
Вдумчивый, хорошо написанный ответ. Исторический контекст. Относительно неисследованный. Красиво сделано.
Cheezmeister

10
@ TulainsCórdova Я согласен, что мы должны «избегать ... хитрых уловок, таких как чума» (Дональд Кнут), но это не означает написание forцикла, когда a mapбудет более подходящим (и читаемым). Есть счастливая среда.
Брайан МакКатчон

73

Я считаю, что у меня есть ответ на один из вопросов, которые вы подняли, которые не были подняты другими.

Java оптимизирует обнаружение ошибок программиста во время компиляции, никогда не делая никаких предположений

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

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

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

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

В Java есть два способа сделать это:

  1. Используйте в Triplet<A, B, C>качестве возвращаемого типа (который на самом деле должен быть java.util, и я не могу объяснить , почему это не так . Тем более JDK8 ввести Function, BiFunction, Consumer, BiConsumerи т.д. ... Это только кажется , что Pairи по Tripletкрайней мере , будет иметь смысл. Но я отвлекся )

  2. Для этого создайте свой собственный тип значения, в котором каждое поле будет правильно названо и введено.

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

Некоторые языки обеспечивают статическую проверку типов и вывод типов одновременно, но это оставляет дверь открытой для тонкого класса проблем несоответствия типов. Где разработчик намеревался вернуть значение определенного типа, но фактически возвращает другое, и компилятор STILL принимает код, потому что случается так, что по совпадению и функция, и вызывающая сторона используют только методы, которые могут применяться как к намеченному и фактические типы.

Рассмотрим что-то подобное в Typescript (или типе потока), где вместо явной типизации используется вывод типа.

function parseDurationInMillis(csvLine){
    // here the developer intends to return a Number, 
    // but actually it's a String
    return csv.firstField();
}

// Compiler infers that parseDurationInMillis is String, so it does
// string concatenation and infers that plusTwoSeconds is String
// Developer actually intended Number
var plusTwoSeconds = 2000 + parseDurationInMillis(csvLine);

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


Обратите внимание, что в соответствии с надлежащими объектно-ориентированными принципами и доменным моделированием, случай разбора продолжительности в связанном вопросе также может быть возвращен как java.time.Durationобъект, который был бы намного более явным, чем оба вышеупомянутых случая.


38
Утверждение, что Java оптимизирует код на правильность, может быть верным аргументом, но мне кажется (программирование на многих языках, в последнее время немного Java), что язык либо дает сбой, либо крайне неэффективен при этом. Конечно, Java устарела, и много чего еще не существовало, но есть современные альтернативы, которые обеспечивают возможно лучшую проверку правильности, например, Haskell (и, смею сказать, «Rust or Go»). Вся громоздкость Java не нужна для этой цели. - И к тому же , это не совсем объяснить культуру, но Соломонов показал , что культура является BS в любом случае.
Томсминг

8
Этот пример не демонстрирует, что вывод типов является проблемой только потому, что решение JavaScript разрешить неявные преобразования в динамическом языке глупо. Любой вменяемый динамический язык или статически рекламируемый язык с выводом типа не допустит этого. В качестве примера для обоих видов: python выдаст исключение времени выполнения, Haskell не скомпилируется.
Voo

39
@tomsmeding Стоит отметить, что этот аргумент особенно глуп, потому что Haskell существует на 5 лет дольше, чем Java. У Java может быть система типов, но она даже не прошла современную проверку правильности, когда была выпущена.
Алексис Кинг,

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

19
@KonradRudolph Да, этот ответ совершенно бессмысленный, хотя я полагаю, что он эмоционально привлекателен. Я не уверен, почему у него так много голосов. Haskell (или, что еще важнее, Idris) оптимизирует гораздо больше для корректности, чем Java, и у него есть кортежи с облегченным синтаксисом. Более того, определение типа данных является однострочным, и вы получаете все, что получит версия Java. Этот ответ оправдывает плохой языковой дизайн и ненужную многословность, но он звучит хорошо, если вы любите Java и не знакомы с другими языками.
Алексис Кинг,

50

Java и Python - два языка, которые я использую больше всего, но я иду с другой стороны. То есть я был глубоко в мире Java, прежде чем начал использовать Python, поэтому я мог бы помочь. Я думаю, что ответ на более широкий вопрос «почему все так тяжело» сводится к двум вещам:

  • Затраты на разработку между двумя подобны воздуху в длинном воздушном шаре животного воздушного шара. Вы можете сжать одну часть воздушного шара, а другую часть раздувать. Python имеет тенденцию сжимать раннюю часть. Ява сжимает более позднюю часть.
  • У Java все еще нет некоторых функций, которые могли бы убрать часть этого веса. Java 8 внесла огромный вклад в это, но культура еще не полностью переварила изменения. Java может использовать еще несколько вещей, таких как yield.

Java «оптимизирует» программное обеспечение с высокой стоимостью, которое будет поддерживаться в течение многих лет большими группами людей. У меня был опыт написания материала на Python и год спустя, и я был озадачен моим собственным кодом. В Java я могу посмотреть на крошечные фрагменты кода других людей и сразу же узнать, что он делает. В Python вы не можете этого сделать. Дело не в том, что один лучше, как вы, кажется, понимаете, просто у них разные затраты.

В конкретном случае, который вы упоминаете, нет кортежей. Простое решение - создать класс с общедоступными ценностями. Когда появилась Java, люди делали это довольно регулярно. Первая проблема заключается в том, что это головная боль при обслуживании. Если вам нужно добавить некоторую логику или безопасность потоков или вы хотите использовать полиморфизм, вам, по крайней мере, нужно будет коснуться каждого класса, который взаимодействует с этим объектом 'tuple-esque'. В Python есть решения для этого, такие как __getattr__и т.д., так что это не так страшно.

Есть некоторые вредные привычки (ИМО) вокруг этого, хотя. В этом случае, если вам нужен кортеж, я спрашиваю, почему вы сделали его изменчивым объектом. Вам нужны только геттеры (на заметку, я ненавижу соглашение get / set, но оно таково). Я думаю, что пустой класс (изменяемый или нет) может быть полезен в частном или частном контексте пакета в Java , То есть, ограничивая ссылки в проекте на класс, вы можете позже выполнить рефакторинг по мере необходимости, не изменяя открытый интерфейс класса. Вот пример того, как вы можете создать простой неизменный объект:

public class Blah 
{
  public static Blah blah(long number, boolean isInSeconds, boolean lessThanOneMillis)
  {
    return new Blah(number, isInSeconds, lessThanOneMillis);
  }

  private final long number;
  private final boolean isInSeconds;
  private final boolean lessThanOneMillis;

  public Blah(long number, boolean isInSeconds, boolean lessThanOneMillis)
  {
    this.number = number;
    this.isInSeconds = isInSeconds;
    this.lessThanOneMillis = lessThanOneMillis;
  }

  public long getNumber()
  {
    return number;
  }

  public boolean isInSeconds()
  {
    return isInSeconds;
  }

  public boolean isLessThanOneMillis()
  {
    return lessThanOneMillis;
  }
}

Это своего рода шаблон, который я использую. Если вы не используете IDE, вы должны начать. Он сгенерирует геттеры (и сеттеры, если они вам нужны), так что это не так больно.

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

public class Blah 
{
  public static Blah fromSeconds(long number)
  {
    return new Blah(number * 1000_000);
  }

  public static Blah fromMills(long number)
  {
    return new Blah(number * 1000);
  }

  public static Blah fromNanos(long number)
  {
    return new Blah(number);
  }

  private final long nanos;

  private Blah(long nanos)
  {
    this.nanos = nanos;
  }

  public long getNanos()
  {
    return nanos;
  }

  public long getMillis()
  {
    return getNanos() / 1000; // or round, whatever your logic is
  }

  public long getSeconds()
  {
    return getMillis() / 1000; // or round, whatever your logic is
  }

  /* I don't really know what this is about but I hope you get the idea */
  public boolean isLessThanOneMillis()
  {
    return getMillis() < 1;
  }
}

Комментарии не для расширенного обсуждения; этот разговор был перенесен в чат .
maple_shaft

2
Могу ли я предложить вам также взглянуть на Scala в отношении «более современных» функций Java ...
MikeW

1
@MikeW Я начал с Scala еще в ранних выпусках и некоторое время активно участвовал в форумах по Scala. Я думаю, что это большое достижение в языковом дизайне, но я пришел к выводу, что это не совсем то, что я искал, и что я был в этом сообществе. Я должен посмотреть на это снова, потому что это вероятно изменилось значительно с того времени.
JimmyJames

Этот ответ не касается аспекта приблизительного значения, поднятого ОП.
ErikE

@ErikE Слова «приблизительное значение» не появляются в тексте вопроса. Если вы можете указать мне на конкретную часть вопроса, который я не затрагивал, я могу попытаться это сделать.
JimmyJames

41

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

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

Но как только вы начинаете думать о том, что ApproximateDurationтип делает для вас, вы понимаете, что он не ограничен настолько узко, что «просто, казалось бы, ненужный класс, чтобы вернуть три значения». Концепция, представленная этим классом, на самом деле является одной из ваших основных бизнес-концепций в области доменных имен - необходимость уметь представлять приблизительное время и сравнивать его. Это должно быть частью вездесущего языка ядра вашего приложения, с хорошей поддержкой объектов и доменов, чтобы оно могло быть протестировано, модульно, повторно использовано и полезно.

Является ли ваш код, который суммирует приблизительную длительность вместе (или длительности с допустимым пределом погрешности, как бы вы это ни указывали), полностью процедурным или есть какая-то объектность для него? Я бы предположил, что хороший дизайн, заключающийся в суммировании приблизительных длительностей, будет диктовать выполнение этого вне любого потребляющего кода, в пределах класса, который сам может быть протестирован. Я думаю, что использование такого типа доменного объекта будет иметь положительный волновой эффект в вашем коде, который поможет вам отойти от построчных процедурных шагов для выполнения одной высокоуровневой задачи (хотя и с множеством обязанностей), к классам с одной ответственностью которые свободны от конфликтов разных интересов.

Например, предположим, что вы узнаете больше о том, какая точность или масштаб на самом деле требуются для правильного суммирования и сравнения продолжительности, и вы обнаружите, что вам нужен промежуточный флаг для обозначения «ошибки приблизительно 32 миллисекунд» (близко к квадрату корень из 1000, поэтому на полпути логарифмически между 1 и 1000). Если вы привязали себя к коду, который использует примитивы для представления этого, вам придется найти каждое место в коде, где у вас есть, is_in_seconds,is_under_1msи изменить его наis_in_seconds,is_about_32_ms,is_under_1ms, Все должно было бы измениться повсюду! Создание класса, в обязанности которого входит запись допустимой погрешности, чтобы его можно было использовать в другом месте, освобождает ваших потребителей от знания деталей о том, какие значения имеют значения погрешности или что-либо о том, как они сочетаются, и позволяет им просто указывать допустимый предел погрешности. сейчас. (То есть ни один потребительский код с допустимым пределом погрешности не будет принудительно изменен при добавлении в класс нового запаса погрешности, поскольку все старые допустимые пределы погрешности все еще действительны).

Заключительное заявление

Жалобы на тяжесть Java, похоже, уходят, когда вы приближаетесь к принципам SOLID и GRASP и более продвинутой разработке программного обеспечения.

добавление

Я добавлю совершенно беспристрастно и несправедливо, что автоматические свойства C # и возможность назначать свойства «только для получения» в конструкторах помогают еще больше убрать немного грязный код, который потребуется «пути Java» (с явными частными полями поддержки и функциями получения / установки) :

// Warning: C# code!
public sealed class ApproximateDuration {
   public ApproximateDuration(int lowMilliseconds, int highMilliseconds) {
      LowMilliseconds = lowMilliseconds;
      HighMilliseconds = highMilliseconds;
   }
   public int LowMilliseconds { get; }
   public int HighMilliseconds { get; }
}

Вот реализация Java выше:

public final class ApproximateDuration {
  private final int lowMilliseconds;
  private final int highMilliseconds;

  public ApproximateDuration(int lowMilliseconds, int highMilliseconds) {
    this.lowMilliseconds = lowMilliseconds;
    this.highMilliseconds = highMilliseconds;
  }

  public int getLowMilliseconds() {
    return lowMilliseconds;
  }

  public int getHighMilliseconds() {
    return highMilliseconds;
  }
}

Теперь это чертовски чисто. Обратите внимание на очень важное и преднамеренное использование неизменности - это кажется решающим для этого особого вида ценностного класса.

В этом отношении этот класс также является достойным кандидатом на structроль типа значения. Некоторое тестирование покажет, имеет ли переключение на структуру выигрыш в производительности во время выполнения (может).


9
Я думаю, что это мой любимый ответ. Я обнаружил, что когда я продвигаю такой дрянной класс, как этот взрослый, во что-то взрослое, он становится домом для всех связанных с этим обязанностей и дзин! все становится чище! И я обычно узнаю что-то интересное о проблемном пространстве одновременно. Очевидно, что есть навык, позволяющий избежать чрезмерного проектирования ... но Гослинг не был глупым, когда он пропустил синтаксис кортежей, так же, как с GOTO. Ваше заключительное заявление отлично. Спасибо!
SusanW

2
Если вам нравится этот ответ, читайте о доменно-управляемом дизайне. На эту тему можно многое рассказать о представлении концепций предметной области в коде.
Неонтапир

@neontapir Да, спасибо! Я знал, что есть имя для этого! :-) Хотя я должен сказать, что я предпочитаю это, когда концепция домена появляется органически из вдохновляющей части рефакторинга (как это!) ... это немного похоже на то, когда вы решаете проблему гравитации 19-го века, открывая Нептун .. .
SusanW

@SusanW Я согласен. Рефакторинг для удаления примитивной одержимости может стать отличным способом раскрытия концепций предметной области!
Неонтапир

Я добавил Java-версию примера, как обсуждалось. Я не разбираюсь в магии синтаксического сахара в C #, поэтому, если чего-то не хватает, дайте мне знать.
JimmyJames

24

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

Python - это мультипарадигмальный язык, который оптимизирует для ясности и простоты кода (легко читать и писать).

Java (традиционно) - это ОО-язык, основанный на классах с одной парадигмой, который оптимизирует для ясности и согласованности - даже за счет более подробного кода.

Кортеж Python - это структура данных с фиксированным числом полей. Та же функциональность может быть достигнута обычным классом с явно объявленными полями. В Python естественно предоставлять кортежи в качестве альтернативы классам, потому что это позволяет значительно упростить код, особенно благодаря встроенной поддержке синтаксиса для кортежей.

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

Java предпочитает единую концепцию (классы), последовательно применяемую с минимумом синтаксического сахара особого случая, в то время как Python предоставляет несколько инструментов и множество синтаксических сахаров, чтобы вы могли выбрать наиболее удобный для любой конкретной цели.


+1, но как это сочетается со статически типизированными языками с классами, такими как Scala, которые, тем не менее, обнаружили необходимость введения кортежей? Я думаю, что в некоторых случаях кортежи являются более точным решением, даже для языков с классами.
Андрес Ф.

@AndresF .: Что вы подразумеваете под сеткой? Scala является языком, отличным от Java, и имеет разные принципы дизайна и идиомы.
JacquesB

Я имел в виду это в ответ на ваш 5-й абзац, который начинается с «но это не совсем соответствует культуре Java [...]». Действительно, я согласен, что многословие является частью культуры Java, но нехватка кортежей не может быть, потому что они «уже используют явно объявленные классы» - в конце концов, Scala также имеет явные классы (и вводит более краткие классы падежей ), но это также позволяет кортежи! В конце концов, я думаю, что, скорее всего, Java не представила кортежи не только потому, что классы могут достичь того же (с большим количеством беспорядка), но также и потому, что его синтаксис должен быть знаком программистам C, а у C не было кортежей.
Андрес Ф.

@AndresF .: Scala не испытывает отвращения к множеству способов ведения дел, он сочетает в себе функции как функциональной парадигмы, так и классической ОО. В этом смысле он ближе к философии Python.
JacquesB

@AndresF .: Да, Java начинала с синтаксиса, близкого к C / C ++, но значительно упростила. C # начинался как почти точная копия Java, но за эти годы добавил множество функций, включая кортежи. Так что это действительно разница в философии языкового дизайна. (Более поздние версии Java кажутся менее догматичными. Функция высшего порядка изначально была отклонена, потому что вы могли делать то же самое, какие классы, но теперь они были введены. Поэтому, возможно, мы видим изменение в философии.)
JacquesB

16

Не ищите практики; как правило, это плохая идея, как сказано в передовой практике ПЛОХО, шаблоны ХОРОШИЕ? , Я знаю, что вы не спрашиваете о лучших практиках, но я все же думаю, что вы найдете там некоторые важные элементы.

Поиск решения вашей проблемы лучше, чем практика, и ваша проблема не в том, чтобы быстро вернуть три значения в Java:

  • Есть массивы
  • Вы можете вернуть массив в виде списка в одну строку: Arrays.asList (...)
  • Если вы хотите сохранить сторону объекта с наименьшим количеством возможного шаблона (и без ломбок):

class MyTuple {
    public final long value_in_milliseconds;
    public final boolean is_in_seconds;
    public final boolean is_under_1ms;
    public MyTuple(long value_in_milliseconds,....){
        ...
    }
 }

Здесь у вас есть неизменяемый объект, содержащий только ваши данные, и общедоступный, поэтому нет необходимости в геттерах. Однако обратите внимание, что если вы используете некоторые инструменты сериализации или персистентный слой, например ORM, они обычно используют getter / setter (и могут принять параметр для использования полей вместо getter / setter). И именно поэтому эти практики используются очень часто. Поэтому, если вы хотите узнать о практиках, лучше понять, почему они здесь, чтобы лучше их использовать.

Наконец: я использую геттеры, потому что я использую много инструментов сериализации, но я их тоже не пишу; Я использую ломбок: я использую ярлыки, предоставленные моей IDE.


9
По-прежнему важно понимать общие идиомы, встречающиеся на разных языках, поскольку они становятся более доступным стандартом де-факто. Эти идиомы имеют тенденцию попадать под заголовок «лучшие практики» по любой причине.
Берин Лорич

3
Эй, разумное решение проблемы. Кажется вполне разумным просто написать класс (/ struct) с несколькими открытыми членами вместо полномасштабного, чрезмерно спроектированного ООП-решения с [gs] etters.
Томсминг

3
Это приводит к раздутию, потому что, в отличие от Python, поиск или более короткое и более элегантное решение не приветствуются. Но плюс в том, что раздувание будет более или менее одинаковым для любого разработчика Java. Поэтому существует более высокая степень удобства сопровождения, поскольку разработчики более взаимозаменяемы, и если два проекта объединяются или две команды непреднамеренно расходятся, разница в стиле меньше для борьбы.
Михаил Рамендик

2
@MikhailRamendik Это компромисс между YAGNI и ООП. Православная ООП говорит, что каждый предмет должен быть заменяемым; единственное, что имеет значение, это открытый интерфейс объекта. Это позволяет вам писать код, который меньше ориентирован на конкретные объекты и больше на интерфейсы; и поскольку поля не могут быть частями интерфейса, вы никогда не будете их выставлять. Это может значительно облегчить поддержку кода в долгосрочной перспективе. Кроме того, он позволяет предотвратить недопустимые состояния. В вашем исходном примере возможно иметь объект, который является «<ms» и «s», которые являются взаимоисключающими. Плохо.
Луаан

2
@PeterMortensen Это библиотека Java, которая делает много совершенно не связанных вещей. Это очень хорошо, хотя. Вот особенности
Майкл

11

Об идиомах Java в целом:

Существуют различные причины, по которым в Java есть классы для всего. Насколько мне известно, главная причина:

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


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

Вы можете уменьшить шаблон с помощью таких инструментов, как Lombok:

@Value
class MyTuple {
    long value_in_milliseconds;
    boolean is_in_seconds;
    boolean is_under_1ms;
}

5
Также проект компании Google Автозначение: github.com/google/auto/blob/master/value/userguide/index.md
Иван

Это также, почему Java-программы могут быть относительно легко поддерживаться!
Торбьерн Равн Андерсен

7

Есть много вещей, которые можно сказать о культуре Java, но я думаю, что в случае, если вы столкнулись с этим прямо сейчас, есть несколько важных аспектов:

  1. Библиотечный код пишется один раз, но используется гораздо чаще. Хотя приятно минимизировать накладные расходы на написание библиотеки, в долгосрочной перспективе, вероятно, более целесообразно писать так, чтобы минимизировать накладные расходы на использование библиотеки.
  2. Это означает, что самодокументируемые типы великолепны: имена методов помогают понять, что происходит и что вы получаете от объекта.
  3. Статическая типизация является очень полезным инструментом для устранения определенных классов ошибок. Это, конечно, не все исправляет (людям нравится шутить по поводу того, что Haskell, как только вы заставите систему типов принимать ваш код, это, вероятно, правильно), но очень легко сделать невозможными некоторые неправильные вещи.
  4. Написание кода библиотеки связано с указанием контрактов. Определение интерфейсов для ваших аргументов и типов результатов делает границы ваших контрактов более четко определенными. Если что-то принимает или создает кортеж, нельзя сказать, является ли это тип кортежа, который вы должны фактически получить или создать, и существует очень мало ограничений на такой универсальный тип (имеет ли оно даже правильное количество элементов? они того типа, что вы ожидали?).

"Структурировать" классы с полями

Как уже упоминалось в других ответах, вы можете просто использовать класс с открытыми полями. Если вы сделаете их окончательными, вы получите неизменный класс и инициализируете их с помощью конструктора:

   class ParseResult0 {
      public final long millis;
      public final boolean isSeconds;
      public final boolean isLessThanOneMilli;

      public ParseResult0(long millis, boolean isSeconds, boolean isLessThanOneMilli) {
         this.millis = millis;
         this.isSeconds = isSeconds;
         this.isLessThanOneMilli = isLessThanOneMilli;
      }
   }

Конечно, это означает, что вы привязаны к определенному классу, и все, что когда-либо нужно для получения или использования результата анализа, должно использовать этот класс. Для некоторых приложений это нормально. Для других это может вызвать некоторую боль. Большая часть Java-кода посвящена определению контрактов, что обычно приводит вас к интерфейсам.

Еще одна ловушка заключается в том, что при подходе на основе классов вы открываете поля, и все эти поля должны иметь значения. Например, isSeconds и millis всегда должны иметь какое-то значение, даже если isLessThanOneMilli имеет значение true. Какой должна быть интерпретация значения поля миллис, если isLessThanOneMilli имеет значение true?

«Структуры» как интерфейсы

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

   interface ParseResult {
      long getMillis();

      boolean isSeconds();

      boolean isLessThanOneMilli();

      static ParseResult from(long millis, boolean isSeconds, boolean isLessThanOneMill) {
         return new ParseResult() {
            @Override
            public boolean isSeconds() {
               return isSeconds;
            }

            @Override
            public boolean isLessThanOneMilli() {
               return isLessThanOneMill;
            }

            @Override
            public long getMillis() {
               return millis;
            }
         };
      }
   }

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

С такой структурой, как этот результат анализа, контракт вашего синтаксического анализатора очень четко определен. В Python один кортеж на самом деле не отличается от другого кортежа. В Java доступна статическая типизация, поэтому мы уже исключаем определенные классы ошибок. Например, если вы возвращаете кортеж в Python и хотите вернуть кортеж (millis, isSeconds, isLessThanOneMilli), вы можете случайно сделать:

return (true, 500, false)

когда вы имели в виду:

return (500, true, false)

С таким интерфейсом Java вы не можете скомпилировать:

return ParseResult.from(true, 500, false);

вообще. Ты должен сделать:

return ParseResult.from(500, true, false);

Это преимущество статически типизированных языков в целом.

Этот подход также дает вам возможность ограничить, какие ценности вы можете получить. Например, при вызове getMillis () вы можете проверить, является ли isLessThanOneMilli () истиной, и, если это так, сгенерировать исключение IllegalStateException (например), поскольку в этом случае нет значащего значения millis.

Трудно сделать неправильную вещь

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

На практике вы действительно можете использовать TimeUnit и длительность, чтобы у вас был такой результат:

   interface Duration {
      TimeUnit getTimeUnit();

      long getDuration();

      static Duration from(TimeUnit unit, long duration) {
         return new Duration() {
            @Override
            public TimeUnit getTimeUnit() {
               return unit;
            }

            @Override
            public long getDuration() {
               return duration;
            }
         };
      }
   }

   interface ParseResult2 {

      boolean isLessThanOneMilli();

      Duration getDuration();

      static ParseResult2 from(TimeUnit unit, long duration) {
         Duration d = Duration.from(unit, duration);
         return new ParseResult2() {
            @Override
            public boolean isLessThanOneMilli() {
               return false;
            }

            @Override
            public Duration getDuration() {
               return d;
            }
         };
      }

      static ParseResult2 lessThanOneMilli() {
         return new ParseResult2() {
            @Override
            public boolean isLessThanOneMilli() {
               return true;
            }

            @Override
            public Duration getDuration() {
               throw new IllegalStateException();
            }
         };
      }
   }

Это будет намного больше кода, но вам нужно написать его только один раз, и (при условии, что вы правильно документировали вещи), люди, которые в конечном итоге используют ваш код, не должны догадываться, что означает результат, и не может случайно сделать такие вещи, как result[0]когда они имеют в виду result[1]. Вы по-прежнему можете довольно кратко создавать экземпляры, и извлекать из них данные не так уж и сложно:

  ParseResult2 x = ParseResult2.from(TimeUnit.MILLISECONDS, 32);
  ParseResult2 y = ParseResult2.lessThanOneMilli();

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

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

Вы пишете эти структуры только один раз, но создаете их много раз, так что вы все еще хотите это краткое создание (которое вы получаете). Статическая типизация гарантирует, что данные, которые вы получаете от них, соответствуют вашим ожиданиям.

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


В конце я использовал свой собственный enum вместо TimeUnit, потому что я просто добавил UNDER1MS вместе с фактическими единицами времени. Так что теперь у меня просто два поля, а не три. (Теоретически я мог бы неправильно использовать TimeUnit.NANOSECONDS, но это было бы очень запутанным.)
Михаил Рамендик

Я не создавал получатель, но теперь я вижу, как получатель позволил бы мне выдать исключение, если бы, с originalTimeUnit=DurationTimeUnit.UNDER1MSпомощью вызывающего, попытался прочитать значение в миллисекундах.
Михаил Рамендик

Я знаю, что вы упомянули об этом, но помните, что ваш пример с кортежами Python действительно о динамических по сравнению со статически типизированными кортежами; это не говорит о самих кортежах. Вы можете иметь статически типированные кортежи, которые не будут компилироваться, как я уверен, вы знаете :)
Andres F.

1
@Veedrac Я не уверен, что ты имеешь в виду. Моя точка зрения состояла в том, что нормально писать код библиотеки более подробно (даже если это занимает больше времени), поскольку на использование кода будет затрачено больше времени, чем на его написание .
Джошуа Тейлор

1
@ Veedrac Да, это правда. Но я утверждаю , что есть это различие между кодом , который будет осмысленно использовать повторно, и код , который не будет. Например, в пакете Java некоторые классы являются общедоступными и используются из других мест. Эти API должны быть хорошо продуманы. Внутри есть классы, которым нужно только общаться друг с другом. Эти внутренние связи - это места, где быстрые и грязные структуры (например, объект [], который должен иметь три элемента) могут быть в порядке. Для публичного API обычно предпочтительнее что-то более осмысленное и явное.
Джошуа Тейлор

7

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

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

Это тоже совсем не долго. Единственное, что вам нужно написать, - это определения полей. Сеттеры, геттеры, hashCode и равно могут быть сгенерированы. Таким образом, ваш реальный вопрос должен заключаться в том, почему геттеры и сеттеры не генерируются автоматически, но это проблема синтаксиса (синтаксическая проблема сахара, некоторые скажут), а не проблема культуры.

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


2
«Никто» на самом деле неверен - есть множество инструментов, которые оптимизируют для меньшего количества слов, регулярные выражения являются крайним примером. Но вы, возможно, имели в виду «никого в обычном мире Java», и в этом чтении ответ имеет большой смысл, спасибо. Моя собственная забота о многословии - не время, потраченное на написание кода, а время, потраченное на его чтение; IDE не сэкономит время на чтение; но, похоже, идея в том, что в 99% случаев читается только определение интерфейса, поэтому ЭТО должно быть кратким?
Михаил Рамендик

5

Есть три разных фактора, которые способствуют тому, что вы наблюдаете.

Кортежи против именованных полей

Пожалуй, самый тривиальный - в других языках вы использовали кортеж. Спорить о том, является ли кортежи хорошей идеей, на самом деле не главное, но в Java вы использовали более тяжелую структуру, поэтому это немного несправедливое сравнение: вы могли бы использовать массив объектов и некоторое приведение типов.

Синтаксис языка

Может быть проще объявить класс? Я не говорю о том, чтобы сделать поля открытыми или использовать карту, а о классах дел в Scala, которые предоставляют все преимущества описанной вами установки, но гораздо более кратки:

case class Foo(duration: Int, unit: String, tooShort: Boolean)

Мы могли бы иметь это - но есть цена: синтаксис становится более сложным. Конечно, это может стоить того в некоторых случаях, или даже в большинстве случаев, или даже в большинстве случаев в течение следующих 5 лет - но об этом нужно судить. Кстати, это одна из приятных вещей с языками, которые вы можете изменить самостоятельно (например, lisp) - и обратите внимание, как это становится возможным благодаря простоте синтаксиса. Даже если вы на самом деле не изменяете язык, простой синтаксис позволяет использовать более мощные инструменты; например, я часто пропускаю некоторые опции рефакторинга, доступные для Java, но не для Scala.

Философия языка

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

Конечно, можно написать код, который противоречит философии языка, и, как вы заметили, результаты часто бывают ужасными. Наличие класса с несколькими полями в Java сродни использованию var в Scala, вырождению предикатов пролога для функций, выполнению unsafePerformIO в haskell и т. Д. Java-классы не предназначены для облегчения - они не предназначены для передачи данных , Когда что-то кажется трудным, часто полезно отступить назад и посмотреть, есть ли другой путь. В вашем примере:

Почему длительность отделена от единиц? Существует множество временных библиотек, которые позволяют вам объявить продолжительность - что-то вроде Duration (5, секунд) (синтаксис будет различаться), что позволит вам делать все, что вы хотите, гораздо более надежным способом. Может быть, вы хотите преобразовать его - зачем проверять, если результат [1] (или [2]?) Равен «часу» и умножается на 3600? И третий аргумент - какова его цель? Я предполагаю, что в какой-то момент вам придется распечатать «менее 1 мс» или фактическое время - это логика, которая естественно связана с данными времени. Т.е. у вас должен быть такой класс:

class TimeResult {
    public TimeResult(duration, unit, tooShort)
    public String getResult() {
        if tooShort:
           return "too short"
        else:
           return format(duration)
}

}

или что вы действительно хотите сделать с данными, следовательно, инкапсулируя логику.

Конечно, может быть случай, когда этот способ не сработает - я не говорю, что это магический алгоритм для преобразования результатов кортежей в идиоматический код Java! И могут быть случаи, когда это очень уродливо и плохо, и, возможно, вам следовало бы использовать другой язык - вот почему их так много!

Но мой взгляд на то, почему классы являются «тяжелыми структурами» в Java, заключается в том, что вы не должны использовать их в качестве контейнеров данных, а в качестве отдельных ячеек логики.


Что я хочу сделать с длительностями, так это на самом деле сделать их сумму, а затем сравнить ее с другой. Нет выходных данных. При сравнении необходимо принимать во внимание округление, поэтому мне нужно знать исходные единицы времени на момент сравнения.
Михаил Рамендик

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

Более важной частью этого конкретного вопроса была более общая сторона вещей, и из всех ответов картина, кажется, собирается вместе.
Михаил Рамендик

@MikhailRamendik, возможно, какой-то аккумулятор подойдет, но вы знаете проблему лучше. На самом деле речь идет не о решении этой конкретной проблемы, извините, если я немного отвлекся - мой главный смысл заключается в том, что язык препятствует использованию «простых» данных. иногда это помогает вам переосмыслить свой подход - в других случаях int - это просто int
Thanos Tintinidis

@MikhailRamendik Хорошая особенность метода состоит в том, что когда у вас есть класс, вы можете добавить к нему поведение. И / или сделайте его неизменным, как вы (в большинстве случаев это хорошая идея; вы всегда можете вернуть новый объект в виде суммы). В конце концов, ваш «лишний» класс может инкапсулировать все необходимое вам поведение, и тогда стоимость для получателей станет незначительной. Более того, они могут вам или не нужны. Подумайте об использовании lombok или autovalue .
Maaartinus

5

Насколько я понимаю, основные причины

  1. Интерфейсы являются основным способом Java для абстрагирования классов.
  2. Java может возвращать только одно значение из метода - объект или массив или собственное значение (int / long / double / float / boolean).
  3. Интерфейсы не могут содержать поля, только методы. Если вы хотите получить доступ к полю, вы должны пройти через метод - следовательно, геттеры и сеттеры.
  4. Если метод возвращает интерфейс, у вас должен быть реализующий класс для фактического возврата.

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


Чтобы добавить к этому: если вам нужно это только в небольшом контексте (скажем, частный метод в классе), хорошо использовать чистую запись - идеально неизменную. Но это действительно действительно никогда не должно проникать в публичный контракт класса (или, что еще хуже, в библиотеку!). Вы можете отказаться от записей просто для того, чтобы гарантировать, что это никогда не произойдет с будущими изменениями кода, конечно; это часто более простое решение.
Луаан

И я согласен с мнением «Кортежи плохо работают на Java». Если вы используете дженерики, это очень быстро закончится.
Турбьёрн Равн Андерсен

@ ThorbjørnRavnAndersen Они довольно хорошо работают в Scala, статически типизированном языке с обобщениями и кортежами. Так может это вина Явы?
Андрес Ф.

@AndresF. Обратите внимание на раздел «Если вы используете дженерики». Способ, которым Java делает это, не поддается сложным конструкциям, в отличие, например, от Haskell. Я не знаю Scala достаточно хорошо, чтобы проводить сравнение, но правда ли, что Scala допускает множественные возвращаемые значения? Это может очень помочь.
Торбьерн Равн Андерсен

@ ThorbjørnRavnAndersen Функции Scala имеют одно возвращаемое значение, которое может иметь тип кортежа (например def f(...): (Int, Int), это функция, fкоторая возвращает значение, которое оказывается кортежем целых чисел). Я не уверен, что дженерики в Java - проблема с этим; обратите внимание, что Haskell также выполняет стирание типа, например. Я не думаю, что есть техническая причина, по которой у Java нет кортежей.
Андрес Ф.

3

Я согласен с ответом JacquesB, что

Java (традиционно) язык ОО, основанный на классах с одной парадигмой, который оптимизирует для ясности и последовательности

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

Чего вы добиваетесь, когда у вас есть четкость и согласованность, выполненные Java-способом? Я считаю, что он эволюционировал как язык, который претендует на обеспечение предсказуемого, последовательного, единообразного способа решения любой проблемы программного обеспечения.

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

Или, как сказал один мудрец очень давно ,

Лучший способ оценить язык - взглянуть на код, написанный его сторонниками. «Radix enim omnium malorum est cupiditas» - и Java является явным примером денежно-ориентированного программирования (MOP). Как сказал мне главный сторонник Java в SGI: «Алекс, ты должен идти туда, где деньги». Но я не особо хочу туда, где деньги - там обычно не пахнет хорошо.


3

(Этот ответ не является объяснением для Java в частности, но вместо этого обращается к общему вопросу «Что может оптимизировать [тяжелая] практика?»)

Рассмотрим эти два принципа:

  1. Хорошо, когда ваша программа делает правильные вещи. Мы должны упростить написание программ, которые делают правильные вещи.
  2. Это плохо, когда ваша программа делает не то, что нужно. Мы должны усложнять написание программ, которые делают неправильные вещи.

Попытка оптимизировать одну из этих целей может иногда мешать другой (то есть усложнение неправильных действий может также усложнять правильные действия или наоборот).

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

Например, если ошибка или перерыв в работе вашей программы на несколько часов может привести к гибели людей (медицинские системы, аэронавтика) или даже просто к потере денег (например, миллионы долларов, скажем, в рекламных системах Google), вы сделаете разные компромиссы (не только на вашем языке, но также и в других аспектах инженерной культуры), чем для одноразового сценария: он, скорее всего, склоняется к «тяжелой» стороне.

Другие примеры, которые делают вашу систему более «тяжелой»:

  • Когда у вас много команд, над которыми работали многие команды в течение многих лет, одна большая проблема заключается в том, что кто-то может неправильно использовать чужой API. Функция, вызываемая с аргументами в неправильном порядке, или вызываемая без обеспечения каких-либо предварительных условий / ограничений, которые она ожидает, может быть катастрофической.
  • В качестве особого случая, скажем, ваша команда поддерживает определенный API или библиотеку и хотела бы изменить или изменить его. Чем более «ограничены» ваши пользователи в том, как они могут использовать ваш код, тем легче его изменить. (Обратите внимание, что здесь было бы предпочтительно иметь реальные гарантии, что никто не сможет использовать его необычным образом.)
  • Если разработка делится между несколькими людьми или группами, может показаться целесообразным, чтобы один человек или группа «указали» интерфейс, а другие действительно его реализовали. Чтобы это работало, вы должны быть в состоянии получить определенную степень уверенности, когда реализация будет выполнена, что реализация фактически соответствует спецификации.

Это всего лишь несколько примеров, чтобы дать вам представление о случаях, когда создание «тяжелых» вещей (и затруднение для быстрого написания некоторого кода) может быть действительно преднамеренным. (Можно даже утверждать, что если написание кода требует больших усилий, это может привести к более тщательному размышлению перед написанием кода! Конечно, эта строка аргумента быстро становится нелепой.)

Пример: внутренняя система Python от Google имеет тенденцию делать вещи «тяжелыми», так что вы не можете просто импортировать чужой код, вы должны объявить зависимость в файле BUILD , команде, код которой вы хотите импортировать, нужно объявить свою библиотеку как виден ваш код и т. д.


Примечание : все вышесказанное относится к случаю, когда вещи становятся «тяжелыми». Я абсолютно не утверждаю, что Java или Python (либо сами языки, либо их культура) являются оптимальным компромиссом для любого конкретного случая; это тебе думать. Две связанные ссылки на такие компромиссы:


1
Как насчет чтения кода? Там есть еще один компромисс. Код, утяжеленный странными заклинаниями и обильными ритуалами, труднее читать; с образцом и ненужным текстом (и иерархиями классов!) в вашей IDE становится все труднее увидеть, что на самом деле делает код. Я вижу аргумент, что код не обязательно должен быть простым для написания (не уверен, согласен ли я с ним, но у него есть некоторые достоинства), но он определенно должен быть легким для чтения . Очевидно , сложный код может и был построен с Java - это было бы глупо утверждать обратное - но это благодаря его многословия, или несмотря на это?
Андрес Ф.

@AndresF. Я отредактировал ответ, чтобы было яснее, что речь идет не о Java (о чем я не большой поклонник). Но да, компромиссы при чтении кода: с одной стороны вы хотите иметь возможность легко прочитать «что на самом деле делает код», как вы сказали. С другой стороны, вы хотите иметь возможность легко увидеть ответы на такие вопросы, как: как этот код связан с другим кодом? Что хуже всего может сделать этот код: какое влияние он может оказать на другое состояние, каковы его побочные эффекты? От каких внешних факторов может зависеть этот код? (Конечно, в идеале нам нужны быстрые ответы на оба набора вопросов.)
ShreevatsaR

2

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

Часть того, что влияет на рекомендацию, - это то, что считается читаемым и поддерживаемым в Python, а Java сильно отличается.

  • В Python кортеж является языковой функцией.
  • И в Java, и в C # кортеж является (или будет) библиотечной функцией.

Я упоминаю только C #, потому что в стандартной библиотеке есть набор классов Tuple <A, B, C, .. n>, и это прекрасный пример того, насколько громоздкими являются кортежи, если язык не поддерживает их напрямую. Почти в каждом случае ваш код становится более читабельным и обслуживаемым, если вы правильно выбрали классы для решения этой проблемы. В конкретном примере в вашем связанном вопросе переполнения стека другие значения будут легко выражены как вычисленные геттеры возвращаемого объекта.

Интересное решение, которое разработала платформа C #, которая обеспечивает «золотую середину», - это идея анонимных объектов (выпущенных в C # 3.0 ), которые хорошо справляются с этим зудом. К сожалению, у Java пока нет эквивалента.

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


4
В последней версии c # новые кортежи являются гражданами первого класса, а не библиотеками. Потребовалось слишком много версий, чтобы попасть туда.
Игорь Солоиденко

Последние 3 абзаца можно суммировать как «Язык X имеет функцию, которую вы ищете, чего нет в Java», и я не вижу, что они добавляют к ответу. Зачем упоминать C # в теме Python to Java? Нет, я просто не вижу смысла. Немного странно от парня с репутацией 20k +.
Оливье Грегуар

1
Как я уже сказал, «я упоминаю только C #, потому что Tuples - это библиотечная функция», аналогично тому, как это будет работать в Java, если в основной библиотеке есть Tuples. Это очень громоздко в использовании, и лучший ответ - всегда иметь отдельный класс. Тем не менее, анонимные объекты поцарапали зуд, подобный тому, для которого предназначены кортежи. Без языковой поддержки для чего-то лучшего, вам, по сути, придется работать с тем, что лучше всего поддерживать.
Берин Лорич

@IgorSoloydenko, может, есть надежда на Java 9? Это только одна из тех функций, которые являются логическим следующим шагом к лямбдам.
Берин Лорич

1
@IgorSoloydenko Я не уверен, что это действительно так (граждане первого класса) - они, кажется, являются расширениями синтаксиса для создания и развертывания экземпляров классов из библиотеки, скорее, с поддержкой первого класса в CLR, как это делают класс, структура, перечисление.
Пит

0

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

У меня было это обсуждение с другой стороны, об аргументах метода: рассмотрим простой метод, который вычисляет ИМТ:

CalculateBMI(weight,height)
{
  System.out.println("BMI: " + (( weight / height ) x 703));
}

В этом случае я бы поспорил против этого стиля, потому что вес и рост связаны между собой. Метод «сообщает», что это два отдельных значения, когда это не так. Когда бы вы рассчитали ИМТ с весом одного человека и ростом другого? Это не имеет смысла.

CalculateBMI(Person)
{
  System.out.println("BMI: " + (( Person.weight / Person.height ) x 703));
}

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

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


4
Хорошо, но предположим, что у вас есть (гипотетическая) задача показать график: как ИМТ зависит от веса для некоторого определенного фиксированного роста. Тогда вы не сможете сделать это без создания Person для каждой точки данных. Кроме того, вы не можете создать экземпляр класса Person без действительной даты рождения и номера паспорта. Что теперь? Я не downvote кстати, есть есть веские причины для пакетирования свойства вместе в одном классе.
артём

1
@artem - это следующий шаг, когда интерфейсы вступают в игру. Так что интерфейс с личным весом может быть чем-то вроде этого. Вы попали в пример, который я привел, чтобы показать, что то, что происходит вместе, должно быть вместе.
Питер Б

0

Честно говоря, культура заключается в том, что программисты на Java изначально имели тенденцию выходить из университетов, где преподавали объектно-ориентированные принципы и принципы устойчивого проектирования программного обеспечения.

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

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

Но, как вы говорите, это не без недостатков: сегодня, используя Java более 10 лет, я склонен использовать либо Node / Javascript, либо Go для новых проектов, потому что оба позволяют ускорить разработку, а с архитектурой в стиле микросервиса это часто достаточно. Судя по тому факту, что Google сначала активно использовал Java, но был создателем Go, я думаю, они могли делать то же самое. Но даже сейчас, когда я использую Go и Javascript, я все еще использую многие навыки проектирования, которые я приобрел за годы использования и понимания Java.


1
В частности, инструмент подбора персонала foobar, который использует Google, позволяет использовать два языка для решений: java и python. Мне интересно, что Go не вариант. Я попал на эту презентацию на Go, и в ней есть несколько интересных моментов о Java и Python (а также о других языках). Например, есть один важный момент: «Возможно, в результате, неопытные программисты запутали» легкость используйте «с интерпретацией и динамической типизацией». Стоит прочитать.
JimmyJames

@JimmyJames только что попал на слайд с надписью «в руках экспертов, они великолепны» - хорошее краткое изложение проблемы в вопросе
Том
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.