«Это лучшее, что есть в работе программиста и художника: трепет от того, что ты находишься в процессе создания чего-то восхитительного. Это как предвкушение свежеиспеченного хлеба, аромат которого наполняет комнату», —

д-р Джой Буоламвини, «Разоблачение искусственного интеллекта».

Код должен быть корректным; он должен работать на всех входных данных, обрабатывать исключения и крайние случаи — это первое, чему вас учат как программиста. Затем вы узнаете об эффективности; вы понимаете, что разные алгоритмы имеют разную сложность, и стараетесь думать о наилучшем способе эффективного решения задачи. Со временем, начав сотрудничать с другими людьми, вы изучаете различные соглашения по созданию кода и практики, используемые в отрасли. Но что является следующим шагом? Что превращает программиста в мастера? Что отличает простой код от синтаксического шедевра?

Что делает код красивым?

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

Читабельность

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

Роберт К. Мартин, «Чистый код: руководство по искусству создания гибкого программного обеспечения».

«Любой дурак может написать код, понятный компьютеру. Хорошие программисты пишут код, понятный людям», — 

Мартин Фаулер.

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

Вот пять практик, которые позволят сохранить чистоту кода:

1. Именование. Используйте описательные, информативные и лаконичные имена для классов, переменных и методов. Если возможно, делайте их короткими. Называйте все, что можно, например, магические числа. Удачное именование избавляет от избыточных комментариев. Помните: имена являются важной частью кода, в отличие от живописи, где можно назвать свою работу «Галацидалацидезоксирибонуклеиновая кислота» и все равно считаться одним из лучших художников-сюрреалистов.

// Не делайте так:
public static double bmi(final double w, final double h) {
return w / (h * h);
}

// Делайте так:
public static double calculateBMI(final double weight, final double height) {
return weight / (height * height);
}



// Не делайте так:
public static int size(final int[] arrayOfMoviesFromTheUserBeforeInsertingToDb) {
return arrayOfMoviesFromTheUserBeforeInsertingToDb.length * 4;
}

// Делайте так:
public static int getSizeInBytes(final int[] arr) {
return arr.length * Integer.BYTES;
}

2. Упрощение. Максимально упрощайте свой код. Разделяйте логику на классы и методы, четко определяя единственную ответственность для каждого компонента. Пишите короткие строки кода и следите за тем, чтобы логику было легко понять.

// Не делайте так:
final int[] arr = new int[10];
for (int i = 0; i < arr.length - 1; arr[i + 1] = arr[i]++ | 1 << i++) {}

// Делайте так:
final int[] arr = new int[10];
for (int i = 0; i < arr.length; i++) {
arr[i] = (int) Math.pow(2, i);
}

arr[arr.length -1]--;

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

// Делайте так:
public int gcd(final int a, final int b) {
if (b == 0) {
return a;
}

return gcd(b, a % b);
}

// Не делайте этого с первой попытки, если у вас нет на то веских причин:
private final Cache<Pair<Integer, Integer>, Integer> cache = Caffeine.newBuilder()
.maximumSize(100_000)
.build();

public int gcd(final int a, final int b) {
if (b == 0) {
return a;
}

final Pair<Integer, Integer> key = Pair.of(b, a % b);
Integer result = cache.getIfPresent(key);
if (result != null) {
return result;
}

result = gcd(b, a % b);
cache.put(key, result);
return result;
}

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

// Не делайте так:
public Long getLatestDateInSecondsSinceEpoch(final List<Map<String, LocalDateTime>> list) {
long maxValue = Long.MIN_VALUE;
if (list != null) {
for (final Map<String, LocalDateTime> map : list) {
if (map != null) {
for (final LocalDateTime date : map.values()) {
if (date != null) {
maxValue = Math.max(maxValue, date.atZone(ZoneId.systemDefault()).toEpochSecond());
}
}
}
}
}
return maxValue;
}

// Делайте так:
public Long getLatestDateInSecondsSinceEpoch(final List<Map<String, LocalDateTime>> list) {
long maxValue = Long.MIN_VALUE;
if (list == null) {
return maxValue;
}

for (final Map<String, LocalDateTime> map : list) {
if (map == null) {
continue;
}

for (final LocalDateTime date : map.values()) {
if (date == null) {
continue;
}

maxValue = Math.max(maxValue, date.atZone(ZoneId.systemDefault()).toEpochSecond());
}
}
return maxValue;
}

5. DRY (не повторяйтесь). Старайтесь избегать дублирования кода. Если у вас есть одинаковый (или похожий) код в разных местах, подумайте о том, чтобы извлечь его в общее место и использовать для обоих случаев. Когда кода меньше, его легче отслеживать. Знаю, что некоторые (наиболее интеллектуально развитые) из вас скажут, что великие художники, такие как Энди Уорхол, М. К. Эше и Рене Магритт, создавали дублирующие версии картин. Но поверьте: программисты не любят дубликатов.

// Не делайте так:
private static Character getFirstLexicographicallyOrderedLetter(final String str) {
if (str == null || str.isEmpty()) {
return null;
}

final String lowerCaseStr = str.toLowerCase();
Character first = null;
for (int i = 0; i < lowerCaseStr.length(); i++) {
final char c = lowerCaseStr.charAt(i);
if (Character.isLetter(c) && (first == null || c < first)) {
first = c;
}
}
return first;
}

private static Character getLastLexicographicallyOrderedLetter(final String str) {
if (str == null || str.isEmpty()) {
return null;
}

final String lowerCaseStr = str.toLowerCase();
Character last = null;
for (int i = 0; i < lowerCaseStr.length(); i++) {
final char c = lowerCaseStr.charAt(i);
if (Character.isLetter(c) && (last == null || c > last)) {
last = c;
}
}
return last;
}

private static Character getFirstLexicographicallyOrderedVowelOrLetter(final String str) {
if (str == null || str.isEmpty()) {
return null;
}

final ToIntFunction<Character> score = "zyxwvtsrqpnmlkjhgfdcbuoiea"::indexOf;
final String lowerCaseStr = str.toLowerCase();
Character first = null;
for (int i = 0; i < lowerCaseStr.length(); i++) {
final char c = lowerCaseStr.charAt(i);
if (Character.isLetter(c) && (first == null || score.applyAsInt(c) > score.applyAsInt(first))) {
first = c;
}
}
return first;
}

// Делайте так:
private static Character getFirstLexicographicallyOrderedLetter(final String str) {
return getLetterByComparator(str, Comparator.reverseOrder());
}

private static Character getLastLexicographicallyOrderedLetter(final String str) {
return getLetterByComparator(str, Comparator.naturalOrder());
}

private static Character getFirstLexicographicallyOrderedVowelOrLetter(final String str) {
final ToIntFunction<Character> charToVowelScore = "uoiea"::indexOf;
return getLetterByComparator(str, Comparator.comparingInt(charToVowelScore)
.thenComparing(Comparator.reverseOrder()));
}

private static Character getLetterByComparator(final String str, final Comparator<Character> comparator) {
if (str == null || str.isEmpty()) {
return null;
}

final String lowerCaseStr = str.toLowerCase();
Character result = null;
for (int i = 0; i < lowerCaseStr.length(); i++) {
final char c = lowerCaseStr.charAt(i);
if (Character.isLetter(c) && (result == null || comparator.compare(c, result) > 0)) {
result = c;
}
}
return result;
}

«Код читают чаще, чем пишут», —

Гвидо ван Россум.

«Код как юмор. Когда приходится его объяснять, это плохо», —

Кори Хаус.

Синтаксический сахар и новые возможности

«Код, который вы пишете, делает вас программистом. Код, который вы удаляете, делает вас хорошим программистом. Код, который вам не нужно писать, делает вас великим программистом», —

Марио Фаско.

«Функция хорошего программного обеспечения заключается в том, чтобы сложное казалось простым», — 

Грэди Буч.

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

Приятно, увидев строку кода, подумать: «Я и не знал, что так можно сделать» или «Какой отличный выбор синтаксиса», а не «Почему автор кода изобретает колесо? Для этого есть стандартная реализация, начиная с Java 8». Прогресс — часть процесса. Свиные мочевые пузыри превращаются в тюбики с краской, перья — в печатные машинки, аналоговые камеры — в цифровые. То же происходит и в программировании, где можно собрать самую свежую devel-версию с помощью brew install.

// Не делайте так:
private void addToCount(final List<String> list) {
for (int i = 0; i < list.size(); ++i) {
final String str = list.get(i);
final Integer count = counter.get(str);
if (count == null) {
counter.put(str, 1);
} else {
counter.put(str, count + 1);
}
}
}

// Делайте так:
private void addToCount(final List<String> list) {
list.forEach(str -> counter.merge(str, 1, Integer::sum));
}

«Самая опасная фраза в языке — «Мы всегда так делали»», — 

Грейс Хоппер.

Сопровождаемость

«Единственная постоянная вещь в жизни — это перемены»,

Гераклит.

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

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

А как же эффективность?

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

Например, в большинстве случаев я бы посоветовал использовать i * 2 вместо i << 1. Сдвиг обычно намного быстрее умножения на уровне инструкций, но последнее может запутать других программистов (и вас в будущем). Кроме того, во многих языках компилятор может оптимизировать умножение на сдвиг.

Примеры красиво написанного кода

Как я уже говорил, красота — в глазах смотрящего. Трудно приводить реальные примеры в статье, поскольку они могут включать множество файлов и строк кода. Поэтому я решил показать вам несколько коротких примеров того, что показалось мне весьма увлекательным под названием «Fluent Interface». Fluent interface — объектно-ориентированный API, использующий цепочку методов и каскадирование для создания предметно-ориентированного языка. Проще говоря, различные компоненты логики (вызовы методов) организованы так, как если бы они были предложением, что делает их использование чрезвычайно читабельным и декларативным.

1. Hamcrest: фреймворк для сопоставления утверждений с множеством встроенных мэтчеров (инструментов, проверяющих утверждения на соответствие) и возможностью написания своих собственных.

assertThat("foo", equalToIgnoringCase("FoO"));
assertThat(Cat.class,typeCompatibleWith(Animal.class));
assertThat(person, hasProperty("city", equalTo("New York")));
assertThat(list, containsInAnyOrder("hello", "world"));
assertThat(array, hasItemInArray(42));
assertThat(map, hasEntry(key, value));
assertThat(1, greaterThan(0));
assertThat("test", equalToIgnoringWhiteSpace(" test"));
assertThat("congratulations",stringContainsInOrder(Arrays.asList("con","gratul","ations")));
assertThat("congratulations", startsWith("cong"));
assertThat(list, everyItem(greaterThan(42)));
assertThat("congratulations", anyOf(startsWith("cong"), containsString("ions")));

2. Awaitility: библиотека для тестирования асинхронных систем с настраиваемыми пользователем параметрами ожидания.

await()
.atMost(5, SECONDS)
.until(result::isNotEmpty);

with()
.pollInterval(1, SECONDS)
.await("insert row to DB")
.atMost(1, MINUTES)
.until(newRowWasAdded())

3. DataStax Java Driver: драйвер для программной генерации CQL-запросов.

Select select =
selectFrom("keyspace", "table")
.column("name")
.column("age")
.whereColumn("id").isEqualTo(bindMarker());
// эквивалентно SELECT name,age FROM keyspace.table WHERE id=?

deleteFrom("table")
.whereColumn("key").isEqualTo(bindMarker())
.ifColumn("age").isEqualTo(literal(28));
// DELETE FROM table WHERE key=? IF age = 28

insertInto("table")
.value("name", bindMarker())
.value("age", bindMarker())
.usingTtl(60)
// INSERT INTO table (name,age) VALUES (?,?) USING TTL 60

Заключение

Есть множество альтернативных способов решения одной и той же проблемы, и в каждом конкретном случае есть свои особенности, приводящие к разным путям решения. Ваша цель — выбрать не тот, который проще реализовать, а тот, который легче понять и поддерживать в дальнейшем. Возможно, ваш выбор будет похож на выбор спутника жизни — выбирайте на долгосрочную перспективу, поскольку вы (надеюсь) останетесь с ним/ней на какое-то время. Франсиско де Гойя, бесспорно, один из величайших испанских художников-романтиков, но я бы не стал вешать картину «Сатурн, пожирающий одного из своих сыновей» в детскую — всегда нужно подходить к искусству с учетом аудитории.

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

Чарльз Энтони Ричард Хоар.

Читайте также:

Читайте нас в Telegram, VK и Дзен


Перевод статьи Gal Ashuach: When Done Correctly — Code Can Be a Form Of Art

Предыдущая статьяClaude-in-Chrome постепенно становится лучшим отладчиком фронтенда, который я когда-либо использовал