Методы расширения в Java


    В таких языках программирования, как C#, Kotlin, Groovy, Scala есть возможность расширять класс путем добавления нового функционала, при этом не требуется наследование или изменение самого изначального класса. Это реализовано с помощью специальных выражений, называемых расширения. Java, в отличие от этих языков, не имеет такой возможности из коробки и даже не планирует в ближайших релизах. Благодаря Lombok это стало возможным. Методы расширения были реализованы в Lombok еще 8 лет назад (с поддержкой Eclipse), но для многих все упиралось в поддержку плагином в IDEA (код компилировался, но IDE его не распознавала как валидный). Lombok плагин теперь предустановлен в IDEA 2021.1 EAP, и теперь он поддерживает методы расширения lombok (спасибо Anna Kozlova, Tagir Valeev, NekoCaffeine и Michail Plushnikov).
    Рассмотрим пример классического статического импорта:


    import static org.apache.commons.lang3.StringUtils.capitalize;
    
    public class ExtensionMethods {
        public static void main(String[] args) {
            String str = "test";
            String capitalized = capitalize(str);
            // "Test"
            System.out.println(capitalized);
        }
    }

    при переходе на метод расширения код станет выглядеть так:


    import lombok.experimental.ExtensionMethod;
    import org.apache.commons.lang3.StringUtils;
    
    @ExtensionMethod(StringUtils.class)
    public class ExtensionMethods {
        public static void main(String[] args) {
            String str = "test";
            String capitalized = str.capitalize();
            // "Test"
            System.out.println(capitalized);
        }
    }

    Заворачивания аргументов в скобки заменяются на цепочки вызовов, т.е. код вида call3(call2(call1(arg))) превратится в


    arg.call1()
        .call2()
        .call3();

    Во многих ситуациях это может облегчить чтение кода, особенно когда цепочки длинные, здесь есть некая аналогия со Stream Api или преобразования значения java.util.Optional.
    Фактически это просто синтаксический сахар. Код при компиляции будет заменен на вызов статического метода. Первый аргумент статического метода и станет объектом "this".


    null-значения


    В отличие от обычных instance-методов, методы расширения могут работать и с null-значениями, т.е. подобный вызов вполне допустим:


    import org.apache.commons.lang3.StringUtils;
    
    @ExtensionMethod(StringUtils.class)
    public class MethodExtensions {
        public static void main(String[] args) throws Exception {
            String nullStr = null;
            // "isEmpty=true"
            System.out.println("isEmpty=" + nullStr.trimToEmpty().isEmpty());
        }
    }

    Еще примеры


    Можно добавить в проект на JDK 8 метод, который появится только в JDK 11:


    @UtilityClass
    public class CollectionExtensions {
        public static <T> T[] toArray(Collection<T> list, IntFunction<T[]> generator) {
            return list.stream().toArray(generator);
        }
    }
    
    @ExtensionMethod(CollectionExtensions.class)
    public class MethodExtensions {
        public static void main(String[] args) throws Exception {
            List<Integer> list = Arrays.asList(1, 2, 3);
            // toArray(IntFunction<T[]>) добавлен только в Java 11
            Integer[] array = list.toArray(Integer[]::new);
            // "[1, 2, 3]"
            System.out.println(Arrays.toString(array));
        }
    }

    Или добавить более лаконичный вызов Stream.collect(toList()):


    @UtilityClass
    public class StreamExtensions {
        public static <T> List<T> toList(Stream<T> stream) {
            return stream.collect(Collectors.toList());
        }
    }
    
    @ExtensionMethod(StreamExtensions.class)
    public class MethodExtensions {
        public static void main(String[] args) throws Exception {
            List<Integer> list = Arrays.asList(3, 1, 2);
            List<Integer> sorted = list.stream()
                    .sorted()
                    .toList();
    
            // "[1, 2, 3]"
            System.out.println(sorted);
        }
    }

    Настройка проекта


    • Установите последнюю версию IDEA EAP, важно: EAP версии не стабильны, зато бесплатны. Плагин доступен и в Ultimate, и в Community Edition.
    • Добавьте зависимость lombok: maven

    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>1.18.16</version>
        <scope>provided</scope>
    </dependency>

    либо для gradle:


    compileOnly 'org.projectlombok:lombok:1.18.16'
    annotationProcessor 'org.projectlombok:lombok:1.18.16'
    testCompileOnly 'org.projectlombok:lombok:1.18.16'
    testAnnotationProcessor 'org.projectlombok:lombok:1.18.16'

    • Убедитесь, что включена опция проекта Build, Execution, Deployment -> Compiler -> Annotations processor -> Enable annotation processing
    • Добавьте аннотацию @ExtensionMethod на класс (откуда будет вызов), перечисляя все утилитные классы, из которых необходимо импортировать вызовы.

    Only registered users can participate in poll. Log in, please.

    Что думаете про методы расширения?

    • 23.8%Уже использую в Kotlin, Groovy, Scala, etc.29
    • 25.4%Уже использую в Lombok / обязательно попробую31
    • 14.8%Использую Lombok, но идея расширений не нравится18
    • 35.2%Lombok — зло43
    • 0.8%Другое (напишите в комментарии)1
    Ads
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More

    Comments 42

      +4

      Разработчики плагина открыли двери, которые лучше было держать закрытыми.


      Да, Ломбок, это мог уже давно, но плагин это не поддерживал, а то чего не поддерживает Идея — не существует. Можно было бы запретить Lombok, но его сейчас используют считай везде. А запретить использование отдельной фичи значительно труднее, чем запретить библиотеку целиком.


      Так поприветствуем же целые библиотеки, посвящённые тому, чтобы де факто добавлять методы в final класс String! Подготовимся к встрече с разработчиками, которые уверены, что метод capitalize был в этом классе всегда, потому что они не сталкивались с джавой без Lombok!


      Есть только одна надежда. Может быть, такие вот библиотеки надо подключать в виде исходников, чтобы код собирался с ними? Если нет, то всё уже потеряно.


      Хотя, если даже нужно распространять исходники, то уверен, тулы для распространения зависимостей в виде исходников не заставят себя ждать. Мы ещё попишем на чём-то типа тайп скрипта для джавы, да!


      Что lany, ppopoff, gsaw из дискуссии в https://habr.com/en/post/538280, где теперь ваш бог?

        0

        Lombok предусмотрительно добавил функцию "delombok" для тех, кто разочаровался и передумал. Для большинства кейсов это возможно прямо из интерфейса IDEA (для @ExtensionMethod пока не реализовано).

          +1

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


          Поэтому код после деломбокизации выглядел почти так, как будто его писал человек. Но тут другая история. Во-первых, цепочки вызовов превратятся во вложенные вызовы, то есть изменится то, как выглядит код. Во-вторых, что более важно, статические методы, которые были написаны для расширения классов — они никуда не денутся. Ох это будет славная охота!

          0
          >Можно было бы запретить Lombok, но его сейчас используют считай везде.
          А ничего, что в голосовании пункт «Зло» пока лидирует?
            0
            А ничего, что в голосовании пункт «Зло» пока лидирует?

            Вот прямо сейчас 16 голосов тех, кто использует Ломбок и 13 человек, которые считают Ломбок злом. Не факт, что те, кто считает Ломбок злом не используют его. Я, например, использую только в путь )). Даже, наверное, могу сказать, что будущее джавы за чем-то ломбокоподобным.

              0
              >Не факт, что те, кто считает Ломбок злом не используют его.
              Ну, а почему они так считают, и продолжают жрать кактус? Я использовал — выпилил без особого сожаления. По примерно тем же причинам, что у вас в комменте расписаны.

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

              Кстати, опрос следовало бы сделать с многовариантным выбором. Потому что в скале это вполне может быть хорошо, а в виде ломбока — не очень. Одно другого не исключает.
                0

                Согласен насчет мультивыбора, к сожалению, после публикации этот режим включить уже невозможно.

                  0

                  Может добавить ещё одно голосование просто? Или это тоже нельзя?

                    0

                    Можно. Но не думаю, что это улучшит ситуацию.

            +3

            Хм. При чём тут мой бог? Ничего не понял.


            Я, кстати, тоже участвовал в поддержке ломбоковских extension-методов в IntelliJ IDEA. Кстати, баги всё равно возможны. Наверняка ещё остались инспекции, квик-фиксы, рефакторинги, которые не ожидают такой подставы, что метод — вовсе не метод.


            Интересно, что Stream.toList() появится в 16-й Java, причём будет там отличаться от Collectors.toList() (в отличие от последнего, возвращает неизменяемую коллекцию). Удачи потом баги фиксать.

              +1
              Хм. При чём тут мой бог? Ничего не понял.

              Да всё просто )).


              Вы там успокаивали окружающих, что в Джаву синтаксический сахар точно не проползёт.


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


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


              А с появлением в плагине фичи, описанной в статье, Ломбок делает ещё один шаг к превращению Джавы во что-то, чем она раньше не была. То есть хотя синтаксический сахар не пролезет в стандарт, он всё-таки пролез в практику применения языка. Де факто то, про что вы говорили, что этого не может быть никогда — случилось )).


              Вот это я имел в виду, когда использовал фигуру речи “где теперь ваш бог”.


              Кроме того, я ведь обращался не только к вам. Другие люди рядом в комментариях говорили, что Ломбок он только сокращает геттеры и всё такое, а тут БАХ и вот.


              Я, кстати, тоже участвовал в поддержке ломбоковских extension-методов в IntelliJ IDEA.

              Тем самым добавляя в Джаву синтаксический сахар, который вам не нравится ))

                0
                и ещё его можно использовать с восьмой Джавой

                Почему это важно?


                А с появлением в плагине фичи, описанной в статье, Ломбок делает ещё один шаг к превращению Джавы во что-то, чем она раньше не была.

                Тем самым добавляя в Джаву синтаксический сахар, который вам не нравится

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

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

                  Да. От своих слов не отказываюсь.


                  Тем самым добавляя в Джаву синтаксический сахар, который вам не нравится ))

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

                    0

                    Да надеюсь с поддержкой jetbrains багов в нем станет меньше. Я вот словил недавно баг связанный с несколькими препроцессорами аннотаций и рекордами. Если использовать mapstruct, lombok и records, то при ребилде через компилятор идеи он то генерирует пустой конструктор, то mapstract не находит геттеры для полей. Причём если делать ребилд через компилятор мавена, а не идеи то все работает хорошо. Баг очень рандомный кстати.

                  +1

                  Тагир, добавил упоминание участия в тексте.

                    0

                    Спасибо!

                +1
                Реквестирую формальное запрещение на ExtensionMethod на этапе сборки в Gradle/Maven :)
                +4

                Мне вот интересно — а откуда такой хейт методов расширения? Помню, когда в шарп добавляли var тоже был миллион криков о том, как теперь не получится писать Person person = new Person() и код становится запутанным и непонятным. Потом то же говорили про лямбды, потом про таплы. Теперь вот методы расширения подъехали.


                А ведь это всего лишь сахарок. Чтобы писать foo.Bar() вместо MySuperDuperHelper.Bar(foo). Очевидно, что никакого наследования от Foo не требуется чтобы просто написать функцию которая принимает его как аргумент. Так почему вдруг весь этот бойлер с именем функции оказывается так желаем? Не помню ни одного случая когда я хотел бы вместо того чтобы вызвать хелпер-расширение писать весь путь к хелперу целиком. Собственно, и работа с нуллами понятна, потому что передать нулл в статическую функцию вроде не


                Возможно, я что-то про java-мир не знаю и вместо достаточно компактного x1.abs() + x2.abs() / x3.myCustomSqrt() людям приятнее писать Math.abs(x1) + Math.abs(x2) / MySuperMathHelper.myCustomSqrt(x3), но по-моему читаемость такого варианта только страдает. Или сравните вот такой код:


                myList
                    .stream()
                    .filter(s -> s.startsWith("c"))
                    .map(String::toUpperCase)
                    .mySuperDuperCombinator(s -> s[0])
                    .sorted()
                    .forEach(System.out::println);

                и


                MySuperHelper.mySuperDuperCombinator(
                    myList
                        .stream()
                        .filter(s -> s.startsWith("c"))
                        .map(String::toUpperCase), 
                    s -> s[0]
                    )
                    .sorted()
                    .forEach(System.out::println);

                Возможно, первый вариант выглядит немного непривычно, но он читается куда проще, поверьте человеку, который часто и с тем и с другим работал. Точно так же, как пришли лямбды и var/val, так же и это будет скоро восприниматься как "а почему раньше не сделали".


                Решительно не понимаю всех настроений про "шефвсёпропало", открытые врата в ад, выпущенного из бутилки джины и призывы бойкотировать и запрещать.

                  +1
                  Мне вот интересно — а откуда такой хейт методов расширения?

                  Не уверен, как относится к этим метода прогрессивная общественность, но своё мнение изложить могу.


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


                  И делают и каждый раз приходится объяснять, что не надо так делать НИКОГДА. Я, когда объяснял где-то в стопятидесятый раз, пришёл к выводу, что статические методы нужно запретить совсем. Даже какие-то типа String intToStr(int num). Потому что проблем с объяснением, почему именно тут нельзя делать статический метод больше, чем пользы от этих методов.


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


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

                  Идея добавить методов к уже существующим классам мне скорее симпатична, чем отвратительна. Можно будет построить свою уютную джаву с прикольными цепочками методов и нескучным API платформенных классов.


                  Но я всегда предпочитал 5 классов по 2 метода одному классу с десятью методами. Расширения делают скорее вот этот второй подход. Хотя методы разнесены по разным файлам, может это не так страшно. Я подробно не думал на эту тему, однако с вашими рассуждениями про удобство скорее согласен.


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

                    +5
                    >статические методы нужно запретить совсем
                    Вообще, это не совсем очевидно. Возможно, вы так считаете, потому что у вас «Спринговые синглтоны и всё такое.»?
                      0
                      Вообще, это не совсем очевидно

                      Я бы даже сказал, что совсем не очевидно.


                      Возможно, вы так считаете, потому что у вас «Спринговые синглтоны и всё такое.»?

                      Да, я по-моему так и написал ))

                        +1
                        Не, ну вы написали что у вас есть А и Б. Синглтоны и т.п. и статические методы — но связи между двумя этими вещами напрямую не провели. Она напрашивается — но вот прям черным по белому не написана.
                          0
                          Не, ну вы написали что у вас есть А и Б. Синглтоны и т.п. и статические методы — но связи между двумя этими вещами напрямую не провели.

                          Не провёл. На всякий случай раскрою свою мысль. Если объект класса один и соответственно экземпляров методов тоже по одному, то разработчик часто решает, что раз так, то тогда правильно сделать метод статическим. Так ещё понятнее, что второго экземпляра делать не планируется. Код, по мнению многих людей, становится чище (я с этим категорически не согласен). И ещё включается неверное применяемый в данном контексте принцип KISS. Раз можно следать статический метод — зачем делать метод, которому нужен экземпляр объекта?

                      +4
                      И делают и каждый раз приходится объяснять, что не надо так делать НИКОГДА. Я, когда объяснял где-то в стопятидесятый раз, пришёл к выводу, что статические методы нужно запретить совсем. Даже какие-то типа String intToStr(int num).

                      Даже не знаю что на это сказать. Делать какой-нибудь IntegerToStringConverter синглтон для подобной функции это очень сильно.


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

                      И что для вас это меняет? Вот вам сильно принципиально, capitalize() это метод который объявлен на строке самими Богами джава-стд или это приехало из библиотеки StringUtils с миллионом звезд на гитхабе?


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

                      В каком-нибудь расте инстансные методы это сахар для статических методов, которые принимают this первым аргументом. То есть какой-нибудь foo.toString() вы могли бы записать просто как Object.toString(foo). На самом деле разделение на статические методы и инстансные я скорее рассматриваю как ошибку языков программирования, как например похожей ошибкой было разделение на "функции" и "процедуры" в паскале. Вся разница только в сахаре, чтобы не писать имя типа каждый раз когда хочется вызвать метод.


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




                      Я неспроста привел пример со стрим апи. В сишарпе весь стрим апи реализован как пачка хелпер-функций в классе Enumerable. У самого интерфейса стрима есть только ядро: функция получения следующего элемента стрима и проверка не закончился ли стрим. Всё остальное сделано как чистые функции поверх этого. Именно поэтому в шарпе есть достаточно много библиотек, которые расширяют стрим АПИ в том или ином виде и вот этих mySuperDuperCombinator там на любой вкус и цвет. Из тех, что я лично писал, например, это метод сhunks(this Stream s, uint chunkSize), который разбивает стрим на чанки нужного размера (последний может быть чуть меньше) чуть оптимальнее, чем например предлагается тут.


                      Оформить это как метод расширения без необходимости писать упомянутый по той же ссылке Iterables или прокидывания синглтонов как мне кажется является куда более хорошим решением, чем делать наоборот. Сами понимаете, вариантов использования АПИ всегда больше, чем можно предположить, и лучше иметь возможность допилить функционал, которые разработчики языка не смогли/не захотели, без прокидывания 20 дополнительных IntegerToStringConverterSingleton/StreamChunkDivider/ConsoleOutputer/...

                        –3
                        Делать какой-нибудь IntegerToStringConverter синглтон для подобной функции это очень сильно

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


                        И вот, тут появляется такая фича, для которой нужно написать этих статических методов.

                        И что для вас это меняет?

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


                        Вот вам сильно принципиально, capitalize() это метод который объявлен на строке самими Богами джава-стд или это приехало из библиотеки StringUtils

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


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

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

                          +3
                          Вот представьте себе, насколько сильно я утомлён статическими методами, что готов пойти даже на ЭТО, чтобы полностью устранить эту проблему? Ну и ещё вы возможно, не каждый день работаете со спрингом, там подклчение такого синглтона делается в одну строку кода. От применения статического метода не отличается считай ничем.

                          Я работаю не со спрингом, но с другим диаем где подключение точно также делается в одну строку services.AddSingleton<MySuperDuperService>(). Но делать какую-нибудь функцию возведения в квадрат методом на синглотне мне в голову почему-то все равно не приходило. Не хочу получить FizzBuzzEnterpriseEdition на ровном месте.


                          Статических методов будет больше. 95% статических методов нужно заменить на методы в синглтонах или просто в классах.

                          Кому нужно? Зачем нужно?


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

                          Никто ничего не расширяет, это всё тот же статический метод, просто с небольшим сахарком чтобы не занимать ценное место написанием длинного наименования класса который несет 0 смысла: неважно, SuperStringHelper добавляет функцию или StringSuperHelper, смысл функции capitalize одинаков и не зависит о того, как именно называется класс, который его объявляет.


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

                          Когда вы последний раз мокали функцию capitalize? И как этот мок должен выглядеть? Это типа "в военное время число Пи может быть равно четырём и даже пяти"?

                            0

                            У меня такое ощущение, что я недостаточно чётко выражаю своим мысли, поэтому я сейчас напишу вольный комментарий, а не ответ на ваши вопросы. Если вам после этого всё ещё будет интересно прочитать именно ответы на ваши вопросы, я с удовольствием ответу по всем пунктам.


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


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


                            Рефакторить такой код тоже сложно, потому что понять что в тесте очень непросто. Он получается запутанным.


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


                            С появлением методов расширения увидеть такой статический метод в ревью будет ещё сложнее и объяснять придётся ещё дольше.


                            P.S. Даже с capitalize всё не так просто, как кажется. Потому что возможно бизнес логика требует капитализировать слова в определённых случаях. И одна из методик тестирования такого кода — передавать мок и проверять, что код вызвает этот метод.

                              +2

                              Отвечу вольным комментарием на вольный комментарий.


                              Необходимость мокать или нет метод определяется скорее не тем, сложную логику он выполняет, а является он чистый он или нет. Поэтому, например, функция capitalize вполне может быть статическим методом — она просто делает некоторую работу. А вот например функция capitalize из вашего второго примера постскриптума интереснее: если список исключений постоянен, то для нас это ничего не меняет — функция всё так же работает определенным образом. Замокать в тесте так, чтобы она себя вела по-другому — только себе же придумывать проблемы, когда в реальном коде она так делать не будет. Другой вопрос, если список исключений получается аргументом. Тут у нас опять две опции: либо оно приходит отдельным аргументом типа "foo".capitalize("exception1", "exception2"), либо оно откуда-то берется. Откуда-то это значит из конструктора, значит это уже не может быть статическим методом, и это вполне ваш сервис получается, в бине или где там ещё.


                              Просто подумайте логически, стали бы вы писать мок на сложение в таком духе:


                              myMock.setup(x -> x.add(It.Is.any<int>())
                                .returns(new [] {2,2}, 4)
                                .returns(new [] {3,5}, 52)

                              Нет, потому что сложение — оно сложение, и даже если под add имеется в виду "сложи только если два числа четные, а иначе умножь их на 13" то оно все равно всегда ведет себя таким образом, и мокать это поведение совершенно не нужно.


                              Другое дело, когда у нас функция типа "отправь емейл" или даже просто "верни случайное число". Она никак не может быть статической, потому что функция не является чистой, а значит мы можем захотеть что-то поменять, например, задать seed. Вот пример из красной книги Scala (который я на шарп перевел, но легко гуглится оригинал). Метод Cafe.BuyCoffee который реально покупает кофе — может быть только инстансным методом и требовать мокирования, а вот (Coffee, Charge) BuyCoffee(CreditCard card) — уже нет.


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


                              P.S. Возвращаясь к вашему постскриптуму: такое название функции возможно вводит в заблуждение и стоило бы либо не делать его экстешном (чтобы не путать людей, которые вряд ли подумают что там есть какой-то список исключений), либо назвать capitalizeButSometimesNot

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

                                Ваша позиция мне понятна, но я с вами категорически не согласен.


                                В своём примере я говорю про функцию, которая капитализирует не все слова, использующую функцию capitalize, которая капитализирует все.


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


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

                                  +1
                                  В своём примере я говорю про функцию, которая капитализирует не все слова, использующую функцию capitalize, которая капитализирует все.

                                  окей


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

                                  И? Вот вы сделаете мок (псевдокод) capitalize.mock("foo", "Foo").mock("bar", "Bar").. Чем он лучше просто вызова capitalize? Мы знаем, что capitalize работает корректно (мы на это тесты написали). В итоге этими моками мы только вносим вероятность ошибки (где-то в моке опечатаемся и не то получим).


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

                                  Я пытаюсь понять ценность этого. То есть "надо замокать" — окей, я понял вашу позицию. Но я не понимаю зачем её мокать? Причем мок будет заключаться в том, чтобы просто руками выписать то, что функция и так бы вернула. Она не отправляет емейлов, не меняет никакого состояния, она просто берет и возвращает какие-то значения.


                                  Мок в данном случае выглядит как ещё одна точка отказа без особой пользы. Откуда и вопрос: а чего мы собственно выигрываем от такого мока? От мока какого-нибудь HttpService мы выигрываем в том, что нам не нужно настраивать лишнее окружение, которое нам неинтересно в данном тесте. А тут?

                                    0
                                    Я пытаюсь понять ценность этого. То есть “надо замокать” — окей, я понял вашу позицию. Но я не понимаю зачем её мокать?

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


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


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


                                    Возможно вы скажете, что такие разработчики нам не нужны, но кто тогда будет работать? )))


                                    И? Вот вы сделаете мок (псевдокод) capitalize.mock(“foo”, “Foo”).mock(“bar”, “Bar”)… Чем он лучше просто вызова capitalize?

                                    Это непростой вопрос, поскольку метод capitalize очень очень простой. Если бы он был библиотечным, я бы даже не подумал его мокать.


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


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


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


                                    Дальше выяснится, что в capitalize могут передать и целый текст и хорошо бы учесть это в коде. И некоторые слова в тексте надо будет капсить, поэтому рядом с методом будет список таких слов.


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


                                    Следующим шагом решат что слова для капса надо добавлять через админку. А значит в методе понадобится доступ к базе данных. Думаете, его перенесут в сервис? Нет, там просто вызовут Context.getBean и спокойно продолжат использовать его в статическом контексте. Потому что для этого не придётся делать правки по всему коду, а при переносе в сервис придётся.


                                    Может быть вы думаете, что всё это страшилки, которые выдумали взрослые? Я тоже думал, пока мне лично не пришлось объяснять людям, что так делать нельзя. И вы знаете, не убедил! Делали через “ну так уж и быть, раз тебе так печёт, то ладно”.


                                    Перефразируя известное изречение скажу, что каждый статический метод в большом проекте неконтролируемо растёт, пока не поучает возможность отсылать электронную почту

                                      +1
                                      Также, когда мы захотим эту функцию порефакторить, упадёт не только её тест, но и тесты кода, который её вызывает. Это не проблема, если разработчик постоянно запускает тесты, но по моим наблюдениям, разработчик долго правит код, а потом тесты падают уже на CI. И поди потом разбери, почему упали тесты.

                                      Мне кажется, это не аргумент. Если падает тест А и тест Б, причем известно что функция А вызывает Б, то чинится Б а потом смотрится, помогло ли это для А или нет.


                                      Во-вторых чистые функции очень редко меняются, чаще дописываются новые. Например рядом с Capitalize пишется CapitalizeAllWords и что-то в таком духе.


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

                                      А вот в этот момент как раз стоит сказать "такие разработчики нам не нужны". Именно это переломный момент, когда функция перестает быть чистой, а значит теряет право быть статическим методом и должна переехать в сервис. Если разработчик продолжает лепить костыли, то стоит ему дать в руки дядю Боба/Фаулера и пусть почитает про термин "рефакторинг".


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


                                      Перефразируя известное изречение скажу, что каждый статический метод в большом проекте неконтролируемо растёт, пока не поучает возможность отсылать электронную почту

                                      Могу только искренне посочувствовать.

                                        +2
                                        Мне кажется, это не аргумент.

                                        Это распространённое мнение. Оно подкрепляется тем, что если не делать моков, то получается тестирование интеграции методов между собой, что скорее хорошо, чем плохо.


                                        Если падает тест А и тест Б, причем известно что функция А вызывает Б, то чинится Б а потом смотрится, помогло ли это для А или нет.

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


                                        А вот в этот момент как раз стоит сказать "такие разработчики нам не нужны".

                                        Этот момент ещё нужно вовремя отсечь, что не так просто, как кажется. Легче запрещать чекстайлом всё и с запасом.


                                        Скажите, вы в основном работаете в команде? И ещё интересно какого размера команда, если так.


                                        В моей практике такя фигня начинается в любой команде, где больше пяти человек, если в проекте ведётся активная разработка.

                                          +1
                                          Это распространённое мнение. Оно подкрепляется тем, что если не делать моков, то получается тестирование интеграции методов между собой, что скорее хорошо, чем плохо.

                                          Но если так делать получается переусложнено на ровном месте. Серьезно, физзбазз вы же видели, как можно было так раписать 10 строк элементарного кода — не представляю. Ребята — гении.


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

                                          А у вас не настроено тестирование на пуллреквесты? Если тесты не проходят то пуллреквест залить нельзя. Интеграционные офк на каждый чих не погоняешь, но чистые функи на то и чистые что юниты их только влёт проверяют.


                                          Этот момент ещё нужно вовремя отсечь, что не так просто, как кажется. Легче запрещать чекстайлом всё и с запасом.

                                          Не знаю, у нас подобные вещи на ревью просто не проходят. А при желании можно написать анализатор которй будет жесткую ошибку кидать при обращении к плохим функциям, например тому же getBean(). То есть просто обращение из статической функции — ошибка, не компилитс никак. Не знаю, есть ли в джаве такие возможности, если есть, то рекомендую. Зачем нужен кодстайл если можно заставить компилятор энфорсить внутренние правила.


                                          Скажите, вы в основном работаете в команде? И ещё интересно какого размера команда, если так.

                                          В моей практике такя фигня начинается в любой команде, где больше пяти человек, если в проекте ведётся активная разработка.

                                          Последние 2 года я лид небольшой команды из 6 разработчиков, и фигни слава богу особо не происходит. Мы весьма рьяно смотрим за тем, чтобы какая-то ерунда не попала в прод, чтобы техдолги закрывались и нам будущим не было стыдно за решения, принятые ранее.

                      0
                      Это не хейт расширения самого по себе, а неприятие того, как происходит само добавление. Можно посмотреть на другие языки — там либо способы расширения классов входят в синтаксис языка, либо сам язык позволяет писать синтаксический сахар.
                        +2

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

                          +2

                          После переноса Lombok плагина под крышу самого JetBrains произошла его "легализация". По крайней мере, надежды на то, что к его поддержке будут относиться серьезно, только выросли.

                      0
                      Есть ещё такое, но плагин к IDEA платный
                        0

                        Да. Там, кстати, наоборот — сам класс, который предоставляет extension-методы, должен быть соответствующе помечен, а в Lombok — класс, который их импортирует. И, как вы написали, проблема manifold (при всем его богатстве функционала) — в том, что плагин платный для IDEA Ultimate, причем ценник хороший.

                        +1
                        Ломбок — комбайн, да такой комбайн, что хреново становится, когда понимаешь, что нужно гвозди забивать микроскопом. Если ломается комбайн, ломается вся система, никакого сравнения с дженериками. Для долгосрочной поддержки однозначно зло!
                          +1

                          Плагин для Lombok ломается в основном на обновлении IDE, а сам Lombok достаточно стабилен, по крайней мере последняя версия (не могу заверить за фичи, помеченные как experimental). Раньше нужно было подождать, пока плагин зафиксят (делали оперативно), сейчас это будет делать сама JetBrains. Если я неправильно вас понял, прошу разъяснить, что именно имеется в виду.

                        Only users with full accounts can post comments. Log in, please.