Краткий код — не всегда чистый, и вот почему

1
1062
views

Перевод статьи «Concise Code Isn’t Always Clean Code – and Here’s Why».

Мы, разработчики, всегда только «за» написание рабочего, читаемого, эффективного, краткого кода. А еще — такого, чтобы его можно было использовать многократно.

Многие из нас, думая о чистом коде, считают, что чем меньше кода, тем лучше. Это ловушка. Дело в том, что хотя в целом это правда, но далеко не всегда.

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

Итак, что такое чистый код?

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

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

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

Пример того, что я считаю чистым кодом

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

Мое решение:

func findOutlier(_ array: [Int]) -> Int {
  //since we're guaranteed to have 3 values, grab the first 3
  let parityArr = [
      array[0],
      array[1],
      array[2]
  ]
  //track any odd or even numbers found in parityArr || O(1) - (technically O(n) but we know the input won't grow)
  var odd = 0
  var even = 0
  for num in parityArr {
     //number is even
     if num % 2 == 0 {
         even += 1
     //number is odd
     } else {
         odd += 1
     } 
  }
  //track and test whether there were more odd or even numbers in the array
  var isEven = false
  if even > odd {
      isEven = true
  }  
  //return the first match that's an outlier based on the array containing more even or more odd numbers || O(n) - we don't know the input size 
  if isEven {
      return array.first(where: ({ $0 % 2 != 0 }))!
  } else {
      return array.first(where: ({ $0 % 2 == 0 }))!
  }
}

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

Когда стоит быть кратким, а когда — расписывать

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

Как чрезмерная краткость может повлиять на читаемость?

Что касается меня, я мог бы написать строку

return array.first(where: ({ $0 % 2 != 0 }))!

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

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

return array.first(where: { … в «расписанном» виде выглядит следующим образом:

for num in array {
    if num %2 !=0 {
        return num
    }
}

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

Я мог бы также сделать этот блок:

for num in array {
    if num %2 !=0 {
        return num
    }
}

выглядящим примерно так:

var isEven = even > odd

Блок return, упомянутый выше, можно записать в одну строчку при помощи тернарного оператора. Но может оказаться, что довольно много разработчиков не знакомы с этим тернарным оператором. Также я считаю, что блок if/else более читабелен в большинстве случав:

if isEven {
    return array.first(where: ({ $0 % 2 != 0 }))!
} else {
    return array.first(where: ({ $0 % 2 == 0 }))!
}
return isEven ? array.first(where: {$0 %2 != 0}) : array.first(where: {$0 %2 == 0})

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

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

Во многих из них использовались фильтры, во многих — тернарный оператор. И большинство из них были куда более краткими.

Пример чрезмерной краткости

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

func findOutlier(_ array: [Int]) -> Int {
    let odd = array.filter{$0 % 2 != 0}
    return odd.count > 1 ? array.filter{$0 % 2 == 0}[0] : odd[0]
}

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

Затем я стал разбивать return на if/else и понял, что мое решение, пожалуй, в большинстве случаев будет более эффективным. Мне только раз придется пройти весь массив, и то лишь в том случае, если искомое число будет последним.

Краткое решение все равно хорошее, но я бы не сказал, что отличное (или, как пишется на многих сайтах, best practice).

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

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

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

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

И о повторном использовании

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

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

Но как написание кода с учетом повторного использования затрагивает читаемость?

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

Пишите чистый код, но старайтесь не угодить в ловушки

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

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

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

Просто помните, что чистый код — понятие объемное. Оно не сводится только к краткому коду! Пишите свой код так, чтобы с ним могли работать другие разработчики: каждый, кому придется это делать, скажет вам спасибо.

1 КОММЕНТАРИЙ

  1. Так нужно усечь массив для определения четности до трёх первых элементов. Норкомэны.

ОСТАВЬТЕ ОТВЕТ

Please enter your comment!
Please enter your name here