Чувак, где моя черепаха? +24


Как написать программу, чтобы победить на конкурсе плохого кода? Этот вопрос я задал сам себе, когда прочёл о необычном челлендже на форуме reddit. Да, вы правильно поняли. Это статья не о чистом коде и правильных тестах. Но здесь не будет и речи о плохом, заурядно плохом коде, том коде, который мы очень часто видим в наших проектах. Я расскажу об экстремальном, невообразимом, гениально плохом коде, коде, который использует те возможности джавы, о которых вы, скорее всего, и не догадывались, и те приёмы, которые вы никогда не встретите в обычных проектах. Сможете ли вы использовать эти приёмы на практике? Думаю, нет. Если вы прагматичный человек, то сэкономите своё время и остановитесь. Не читайте эту статью. Однако если вы хотите немного отвлечься от повседневной рутины, увидеть и узнать что-то новое о нашем любимом языке Java, - милости просим!

Bad Code Coding Challenge

Два года назад, пролистывая темы на сайте реддит, я случайно наткнулся на форум о плохом коде. Сам форум не показался мне интересным - мы и так каждый день видим образцы плохого кода. Какой смысл ими делиться? Но моё внимание привлёк необычный конкурс. Он назывался “Bad Code Coding Challenge”. Каждые несколько недель создатель этого конкурса выкладывал условие задачи, и нужно было написать работающее решение на любом языке программирования, максимально избегая использования любых хороших практик. Оригинальный пост с подробными правилами и условиями вы можете посмотреть по ссылке, здесь же я приведу краткий перевод на русский язык задачи, которую я взялся решать.

Условия задачи

Нужно написать программу, которая принимает список инструкций для робота-черепахи и выводит ее координаты и направление после выполнения всех команд. Черепаха существует в бесконечной сетке координат. В начале выполнения программы она всегда находится в точке 0,0 и смотрит на север. Черепаха может смотреть на север, восток, юг или запад. Черепаха принимает две инструкции, каждая с аргументом. Команда forward двигает черепашку вперед в ее текущем направлении на указанное количество шагов. Команда rotate поворачивает черепаху по часовой стрелке на заданный угол. Поворот может осуществляться только на угол, кратный 90.

Первая попытка

Задание очень простое, вполне для школьников, начинающих изучать программирование. Как написать хороший код? Создать несколько классов. Для команд, для черепашки. Отделить пользовательский интерфейс от бизнес логики… Ну а плохой код? Никаких классов, все в одном методе main! Всё должно быть свалено в одну кучу и максимально компакто. Мне пришла в голову мысль, что идеально плохая программа, вообще, должна быть написана в одну строчку, точнее в один стейтмент языка. Что ещё можно добавить к рецепту? Магических чисел и непонятных операций. Немыслимые преобразования типов… Да, классов у нас не будет, но есть простые типы. Можно придумать что-то необычное, то, чего никто и никогда не видел.

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

public class Turtle {
    public static void main(String[] args) {
        System.out.println(java.text.MessageFormat.format(
            "x = {1}, y = {0}, direction = {2, choice, 0#north|1#east|2#south|3#west}", 
            new java.util.Scanner(System.in).useDelimiter("\\R").tokens()
                .mapToInt(s -> Integer.parseInt(s.substring(7).trim()) << s.charAt(0) / 'r' * ' ' / 2)
                .mapToObj(data -> new Integer[] {(int)(short) data, (data >> 16) / 90})
                .reduce(new Integer[]{0, 0, 0},
                    (turtle, action) -> {
                        turtle[2] += action[1];
                        turtle[2] %= 4;
                        turtle[turtle[2] % 2] += turtle[2] < 2 ? action[0] : -action[0];
                        return turtle;
                    })));
    }
}

Так как у нас только один класс, нам не нужны сборщики и предварительная компиляция кода. Мы можем запустить программу, набрав в командной строке java Turtle.java. Программа выполняет инструкции из стандартного потока ввода, если вы набираете команды из консоли, то для завершения ввода нужно нажать Ctrl+D.

$java Turtle.java                       
forward 10                              
rotate 90  

x = 0, y = 10, direction = east 

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

Как и обещал, в коде есть необычное преобразование типов: (int)(short). При попытке что-то поменять, например, убрать одно из преобразований, программа перестаёт работать. Напишите мне в комметариях, как часто вы встречаете подобное в проектах на Java.

В поисках совершенства

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

Главная цель, которую я себе ставил, не была достигнута. Мы можем насчитать целых пять точек с запятой! У меня не получилось втиснуть алгоритм в один стейтмент…

Вторая оплошность - это имена переменных. Видимо подсознательно я хотел, чтобы код был понятен, и дал такие имена, как turtle и action. Не факт, что они проясняют смысл алгоритма, но их не должно быть совсем. Я решил, что в программе не должно быть имён переменных! Даже из одной буквы! Пусть будет лишь намёк, лишь символ…

Я решил повторить попытку. Взяв за основу первый код, я начал его менять. Я добавлял и убавлял, я крутил, вытягивал и снова сжимал код, пытаясь втиснуть его в одно утверждение. Почему-то, мне вспомнилось одно из четверостиший Саши Чёрного:

Дочурка под кроватью ставит кошке клизму,
В наплыве счастья полуоткрывши рот,
И кошка, мрачному предавшись пессимизму,
Трагичным голосом взволнованно орет.

Я попытался сконцентрироваться, взял бумагу и нарисовал точку с запятой. Рой странных операций, магических чисел и функций кружил у меня в голове, но я бессилен был соеденить их вместе. Строчки Саши Чёрного не выходили из головы:

Я похож на родильницу,
Я готов скрежетать…
Проклинаю чернильницу
И чернильницы мать!

Тут одна идея, ярко брызнув искрами, осветила всё вокруг. “Ананасы в шампанском, ананасы в шампанском! Удивительно вкусно, искристо́ и остро́!”, - восторженно заявляет ворвавшийся Игорь, полностью вытеснив Сашу. Я поспешно стучу по клавишам, стараясь зафиксировать пришедшую идею. “Стрёкот аэропланов! Бе́ги автомобилей! Ветропро́свист экспрессов! Крылолёт буеров!”, безумная смесь классов, функций, тернарых операций и магических чисел сыплется непрерывным потоком, соединяясь в бесконечно длинное выражение. Я ставлю точку с запятой. Одну-единственную точку с запятой. Закрывающие фигурные скобки. “Я, гений Игорь Северянин, своей победой упоен…”, - произносит поэт и исчезает.

Я сохраняю код класса в файл turtle.sh и выставляю ему бит выполнения. Набираю в консоли ./turtle.sh - программа работает.

#!/usr/bin/java --source 17

public class Turtle {
  public static void main(String[] args) {
    java.util.stream.IntStream.concat(java.util.stream.IntStream.of(0), 
      new java.util.Scanner(System.in).useDelimiter("\\R").tokens().mapToInt(
          $ -> Integer.parseInt($.substring(7).trim()) * (1 - $.charAt(0) / "porkling"
          .charAt(java.util.concurrent.ThreadLocalRandom.current().nextInt(8)) * 2)))
          .reduce((₽, ₴) -> ₴ < 0 ? (₽ << 3 >>> 3) + (((₽ >>> 29) - ₴ / 90) % 4 << 29)
                  : ₽ + (₽ << 1 < 0 ? -1 : 1) * (₴ << (₽ << 2 < 0 ? 0 : 16)))
          .ifPresent(¤ -> System.out.println(java.text.MessageFormat.format(
                  "x = {0}, y = {1}, direction = {2, choice, 0#north|1#east|2#south|3#west}",
                  ¤ << 16 >> 16, ¤ << 3 >>> 19, ¤ >> 29)));
  }
}

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

Показываю код коллеге. Просит объяснить, как это работает. Что ж, попробую. Там, где доллар и поросёнок ("porkling"), команды преобразуются в биты и аккуратно выстраиваются в число. На ThreadLocalRandom.current::nextInt можно не обращать внимания. Это всего лишь иллюзия случайности, что не мешает черепашке в точности выполнять команды. И при каждой команде, битики попадают в метод reduce. Там они перемещаются налево и направо, умножаются и делятся, взаимно сокращая друг друга. При следующем повороте или движении черепашки новая партия битиков поступает в метод reduce, и процесс идёт по кругу. Только когда последние биты сократят друг друга, мы сможем понять, где находится черепашка и куда она смотрит - на запад или на восток.

Легко понять и объяснить как работает чистый, хороший код. Значительно труднее, иногда даже невозможно понять и объяснить логику грязного, плохого кода. Я ещё раз посмотрел на программу. Код выглядел не просто грязным и плохим, он выглядел ужасным, каким-то зловещим. Нет, не нужно было участвовать в этом конкурсе. НЕТ ПЛОХОМУ КОДУ!

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

Исходный код программы, тесты и описание даже не надейтесь найти в моём репозитории по адресу: https://github.com/rabestro




Комментарии (17):

  1. Aspecter
    /#24610602 / +6

    Ни одна черепаха не пострадала в процессе.

  2. Daddy_Cool
    /#24610660 / +8

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

    • SomeAnonimCoder
      /#24610802

      Интересный вариант: написать что-то вроде fizz buzz enterprise edition для этой задачи. То есть такой плохой код, который появляется при reducto ad absurdum правил хорошего кода

  3. saboteur_kiev
    /#24610936

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

    Однострочные башизмы известны в мире админов, как вещи, которые решают здесь и сейчас. Но если это скрипт для многократного применения, его лучше расписать чуть шире, и переделать в posix-shell, например.

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

    Ну то есть объективная цель челленджа. А у вас цель невнятная..

    • garbagecollected
      /#24610958

      Перескажу вашу мысль в двух словах.

      Оценка челленджа основана не на фактах, а на мнениях.

  4. maxzh83
    /#24611066 / +15

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

  5. FenixF
    /#24611732

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

    Грубо говоря если кому то придет в голову отрефакторить этот код, правильно ли я понимаю, что бОльшую часть ваших трудов он убьет просто отрефакторив нейминг переменных?

  6. trolleyBuz
    /#24611756 / +3

    Прикольный код, но разве это плохой код? Плохой код должен выглядеть хорошо, с лёгкостью проходить code review, содержать подробные комментарии неверно описывающие работу кода, обильное, но бесполезное журналирование. Программа должна работать медленно и жрать все ресурсы, содержать "оптимизации" пускающие исполнение для тестирования и для прода по разным ветвям (навскидку - можно завязаться на число ядер), а ещё нужны гонки и дедлоки, но так, что бы это стабильно работало при тестировании и нестабильно в проде, например можно использовать неявный сброс кэшей цпу при запиcи в лог - включаем debug - всё работает, выключаем - тоже работает, но уже случайно. Для борьбы с гонками хорошо бы использовать sleep-ы, но так мы не пройдём ПР, поэтому задержки нужны неявные, вызовы во внешние сервисы... Время рабочее, внешние сервисы отвечают быстро - у нас всё хорошо, вечер, прайм-тайм, растёт RT - добавляется элемент игры.
    Память, она дожна течь, но не хип, это банально, пускай течёт директ, но не всегда, тоже что бы от луны в меркурии зависел и так что бы OOMKiller приходил внезапно. В код надо добавить уязвимостей, здесь без Unsafe-а не обойтись. Исключения надо ловить все, но только в основном потоке и не ловить в параллельных. У нас же память должна течь? - значит Throwable ловить не надо или ловить, но глушить, не надо никого смущать OOM-ами. И конечно же, необходимо обеспечить выход из программы как минимум в 3-х местах через System.exit();

  7. SaharnyMishka
    /#24611768 / +9

    Видимо вы не сталкивались с действительно плохим кодом)

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

    • StrangerInTheKy
      /#24612550

      и космический корабль запустит, причем работающий не только с теории относительности, но и с квантовой механикой

      Если этот код будет корректно работать и с ОТО, и с КМ одновременно, это будет гениальный код, сразу на нобелевку. Любой физик-теоретик за такой код Родину продаст. Два раза.

      P. S. Для совсем тупых поясняю: про родину - это шутка.

      • mayorovp
        /#24612570 / +1

        Не одновременно, а в зависимости от настроек в конфиге.

    • paluke
      /#24612674

      Надо задействовать несколько фреймворков, обязательно использовать БД… Но видимо написать 10000+ строк кода ради челленджа никто не готов.

  8. StjarnornasFred
    /#24612154 / +2

    Критерии качества кода - это (не в порядке важности):

    • функциональность - способность исполнять поставленные задачи наилучшим образом, в том числе с учётом разных обстоятельств типа ошибок юзера;

    • читаемость - понятность кода для человека, в том числе оформление и именование переменных;

    • оптимизированность - эффективность программы по времени и по памяти;

    • собственный размер - чем меньше, тем лучше.

    Соответственно, ужасный код должен быть минимально работоспособным (выполнять поставленную задачу в режиме итальянской забастовки), нечитаемым, умножать дважды два через кривые Фурье полным перебором лонгинта и при этом занимать кучу места на жёстком диске. Если он ещё и самопорождается в ходе работы, причём невозможно понять, какой его фрагмент за это отвечает - идеально! В Google без испытательного, к разработке обновления Gmail приступать с завтрашнего дня!

  9. Tarakanator
    /#24612226 / +1

    return "0, 0, север, начало координат привязано к черепахе."

  10. ris58h
    /#24612978

    Как написать хороший код? Создать несколько классов.

    В этой задаче как раз наоборот.