Как я пытался писать красивый код
Недавно прошёл конкурс красоты кода. Участие по направлению Android в этом конкурсе было интересным опытом, которым я поделюсь в статье.
Недавно прошёл конкурс красоты кода. Участие по направлению Android в этом конкурсе было интересным опытом, которым я поделюсь в статье.
Содержание:
- О конкурсе
- Задание по направлению Android
- Моё решение
- Решение ChatGPT
- Звонок другу
- Комментарий победителя в номинации «Изящный код» и его решение
О конкурсе
Компьютерный код может написать любой разработчик. Красивый код пишут лишь единицы. Чистый, изящный, лаконичный, читаемый и понятный код, который работает без багов — это настоящее произведение искусства в сфере разработки.
Для участников стояла задача написать красивый код по одному из пяти направлений (Python, Java, Data Science, Front-end, Android).
Каждое направление предусматривало 3 номинации:
- Краса кода — решение, признанное максимально эффективным по мнению жюри;
- Изящный код — самое лаконичное решение, соответствующее поставленной задаче;
- Звезда кода — самое неординарное решение по общей оценке жюри.
Призы конкурса: iPhone 14, умная колонка и приглашение на вечеринку в честь Дня программиста.
Задание по направлению Android
Имея вводные данные, написать функцию, получающую список категорий (List Category), список характеристик (List Feature), и преобразующую их в один List элементов, и возвращающую его.
Правила формирования результирующего списка:
- Первый элемент связан с категорией (Category). Хранит в себе всю информацию о категории.
- Далее идут все элементы, связанные с характеристикой (Feature) относящиеся к данной категории.
- После последней характеристики, относящийся к открытой категории, идет элемент, сигнализирующий о том, что категория закончилась. Хранит в себе только CategoryId.
Количество элементов не ограничено.
Вводные данные:
Моё решение
Итак, нам нужно вывести список категорий и характеристик. При этом элементы в списке должны находиться разные типы элементов: категория, характеристика, концевик категории (её ID).
Первое, что пришло на ум — List<Any>. Это плохое решение, ведь так в этот список можно положить вообще всё, что угодно.
Я посмотрел, что объединяет входные дата-классы. Воспользовавшись nullable-свойствами, я создал общий дата-класс. В названии его я буквально написал, что он содержит.
Объекты этого класса будут наполнять результирующий список.
Дальше ничего не интересного: я просто воспользовался вложенными циклами.
С помощью именованных аргументов конструктора класса, в список сначала добавляется категория, потом все соответствующие характеристики, затем ID категории. Такое довольно простое решение.
Небольшая оптимизация:
В данном случае после добавления характеристики (feature) в итоговый список (categoriesWithFeatures), она удаляется из списка характеристик (mutableFeatures), поэтому следующая итерация цикла пройдёт быстрее, так как в списке будет меньше элементов.
Однако в данном случае необходимо создавать mutableFeatures (изменяемый дупликат списка features), что может быть неэффективно по памяти, если начальный список достаточно большой.
Эта оптимизация подходила бы, если входная функция возвращала изменяемый список:
После участия в конкурсе я нашёл конструкции языка и методы, которые позволяют улучшить код. Например, вместо моего дата-класса больше подошёл бы Sealed Class с тремя потомками: Категорией, Характеристикой и ID категории.
Решение ChatGPT
Я попросил ChatGPT решить поставленную задачу, и вот что он написал:
Моё решение лучше этого, потому что оно, как минимум, работает. Ура, моё решение лучше решения ChatGPT, плюс самооценка!
Что же делает наш умный товарищ:
- Пытается добавить элемент к неизменяемому списку (currentCategory?.features?.add(feature));
- Пишет комментарии в решении, которое должно читаться без комментариев.
Хорошо, давайте закроем на это глаза. Закрыли, но решение всё ещё плохое: оно не соответствует поставленной задаче:
- Основная функция в итоге возвращает список объектов CategoryWithFeatures. Этот класс содержит данные категории и список характеристик. Всё это он запихал в один класс. Теперь нет отдельных элементов категории, характеристики и ID категории, которые должны быть в результирующем списке по условию задачи.
- Его цикл работает только, если во входных данных характеристики идут строго по порядку: сначала характеристики одной категории, потом другой. В противном случае в цикле создаются дубликаты категорий.
Вот, что мы в итоге получаем:
Я также показал своё решение chatGPT и попросил его улучшить. Вот его версия моего решения:
Мой дата-класс он оставил без изменений и поменял саму функцию. Теперь сначала с помощью функции groupBy создаётся Map<Int, List<Feature>>, где ключи — ID категории, значения — списки характеристик. Потом всё те же вложенные циклы, только теперь нужные характеристики не нужно искать, а просто брать из созданного Map.
В итоге:
- Результат работы такой же;
- Вложенные циклы остались;
- Перед вложенными циклами появились: ещё один цикл от функции groupBy и дополнительная переменная;
- Код стал короче, но, как по мне, его читаемость не изменилась.
Звонок другу
Не получив фидбека от организаторов конкурса, я попросил его у друга. Вместе с фидбеком я получил ещё один вариант решения:
Я попытался переписать код, но в итоге сложность по времени получилась такая же. Пока что не придумал другого варианта.
Действительно, во втором цикле из-за функции addAll (которая циклически добавляет все элементы списка featuresFromMap) у нас снова получаются вложенные циклы. При этом читаемость кода снизилась.
К слову, весь первый цикл делает почти то же самое, что и функция groupBy. Разница в том, что groupBy вернёт Map<Int, List<Feature>>, а цикл — MutableMap<Int, List<CategoryOrFeatureOrEndElement>>.
Мне не нравится моя мапа на самом деле. Я добавил список не фичей, а этого класса только из-за того, что не хотел дополнительно забивать память потом. Хотя мб я, кстати, неправильно посчитал, и такой вариант ничем не отличается. Короче, в промышленном коде так делать не стоит.
Комментарий победителя конкурса и его решение
Давид Жубрёв, победитель в номинации «Изящный код», разрешил опубликовать его решение:
Рассмотрим, что здесь происходит. Метод flatMap в соответствии с лямбда-функцией преобразует каждый элемент списка категорий в List, где первый элемент — объект категории. Список характеристик фильтруется по ID категории и преобразовывается в массив. С помощью оператора * каждый элемент массива вкладывается в упомянутый List. Последний элемент List — ID категории.
После этого flatMap избавляется от вложенных списков и возвращает одномерный список всех элементов.
Хоть в результате и получается List<Any>, решение очень лаконичное и понятное, что идеально соответствует номинации. По временной сложности всё остаётся так же, но выглядит лучше. Если добавить сюда другую структуру данных, например, тот же Sealed Class, то будет, возможно, лучшее решение поставленной задачи.
Победитель прокомментировал конкурс и своё участие в нём:
Я не возлагал больших надежд на победу, для меня это был, скорее, небольшой фан. Я был очень удивлен, когда мне написали о победе, но было крайне приятно)
Я очень люблю работать с коллекциями, поэтому решение быстро пришло в голову, минут 20-30 ушло на всё, включая миллион запусков кода, чтобы ну точно все работало)
В целом, было довольно интересно участвовать, да и конкурс весьма необычный оказался, на мой взгляд)
Спасибо за прочтение данной статьи! Буду рад узнать ваше мнение о конкурсе и представленных решениях :)