О дивный читаемый код


Введение 

Большинство начинающих программистов сталкивается со многими дилеммами в процессе написания кода, например задумываются о том, какой код будет востребован в индустрии. У каждой компании свои бенчмарки, лучшие практики для написания кода и основополагающие принципы. Однако есть один критерий кода, который единодушно поддерживается всеми: читаемость. Читаемый код и проживет дольше, и особых проблем с обслуживанием и пониманием не доставит. Более того, он позволяет будущим поколениям разработчиков легко вносить в него изменения. Проблема читаемости кода актуальна и для начинающих разработчиков Scala. В данной статье мне бы хотелось обратить внимание на ряд часто встречающихся ошибок. 

Для всех наших примеров воспользуемся классом Movie

object Genre extends Enumeration { type Genre = Value val HORROR, ACTION, COMEDY, THRILLER, ROMANCE= Value } class Movie( movieName: String, movieActors: List[String], movieRating: Double, movieGenre: Genre ) { val name: String = movieName val actors: List[String] = movieActors val rating: Double = movieRating val genre: Genre = movieGenre }

Обходимся без очень сложных лямбда-функций 

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

var movies: List[Movie] =_ movies.map(movie => if (movie.rating< 4) "Bad" else if movie.rating< 7) "Average" else "Good")

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

movies.map(classifyMovie) def classifyMovie(movie: Movie): String = { val rating = movie.rating if (rating < 4) "Bad" else if (rating < 7) "Average" else "Good" }

К тому же в таком виде он гораздо лаконичнее. 

Лучше val, чем var 

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

val movie = new Movie( "Mystic River", List("Sean Penn", "Kevin Bacon"), 8, Genre.THRILLER ) movie.actors = movie.actors :+ "Laura Linney"

Эта операция не разрешена компилятором Scala. Чтобы добавить два неизменяемых списка, необходимо создать один новый. 

val updatedActors = movie.actors :+ "Laura Linney"

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

Сопоставление с образцом вместо оператора if/else 

Одной из лучших практик для написании кода в Scala является использование сопоставления с образцом вместо традиционных операторов switch или громоздкого if/else. Создадим рекламные объявления на основе различных жанров кино. 

def classifyGenre(genre: Genre): String = { if (genre == HORROR) "You should be scared!" else if (genre == ACTION) "Let's have a fight!" else if (genre == COMEDY) "You are so funny!" else if (genre == THRILLER) "Why so much suspense" else if (genre == ROMANCE) "A love story" else "I don't have a clue" }

А теперь попробуем использовать сопоставление с образцом для этой же цели. 

def classifyGenre(genre: Genre): String = { movie.genre match { case HORROR => "You should be scared!" case ACTION => "Let's have a fight!" case COMEDY => "You are so funny!" case THRILLER => "Why so much suspense" case ROMANCE => "A love story" case _ => "I haven't a clue" } }

Для подобных случаев такой фрагмент кода гораздо понятнее. Он может использоваться вместо сложной логики if/else. Сопоставление с образцом также применяется с классами case для извлечения значений или других типов. 

Option, Some и None вместо null 

В функции, которая может возвращать значения null, следует использовать тип Option. Предлагаю вам игру в шарады, в которой сначала нужно отгадать количество слов в названии фильма. При использовании простого оператора if/else потребуется каждый раз выполнять явную проверку на null.  

def guessTheWords(movieName: String): Int = { if (movieName != null) { movieName.split(" ").size } else 0 }

А вот другой способ написания той же функции. Давайте определим movieName как Option[String] вместо String в нашем исходном классе Movie. 

def guessTheWords(movieName: Option[String]): Int = { movieName match { case Some(x) => x.split(" ").size case None => 0 } }

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

Обработка перечислений (enum) при отсутствии значения 

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

def valueOf(genre: String): Genre = { val lookup = Genre.values.find(_.toString == genre) lookup match { case Some(g) => g case _ => null } }

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

def valueOf(genre: String): Genre = { val genres = Genre.values genres .find(_.toString.toLowerCase == genre.toLowerCase) .getOrElse( throw new NoSuchElementException( s"Supported values are ${genres} but found ${genre}" ) ) }

Подобной функциональности также можно добиться, применив функцию withName.Если случай использования требует раннего обнаружения ошибок в процессе компиляции, то можно рассмотреть вариант с Sealed Traits (запечатанными трейтами), нои у него есть свои ограничения. 

Преобразование с помощью foreach или map

Предположим, что существует метод, который переводит имена актеров фильма с английского на испанский язык (или любой другой). Анонимный метод называется translate.Простой цикл foreach вызовет эту функцию для всех элементов в коллекции movieActors и сохранит содержимое в ListBuffer, так как List не изменяем и не может быть преобразован. 

def transformFunction(actors: List[String]): List[String] = { var translatedActors = new ListBuffer[String]() actors.foreach(translatedActors += translate(_)) translatedActors.toList }

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

def transformFunction(actors: List[String]): List[String] = { actors.map(translate) }

Второй способ гораздо лаконичнее и не требует изменения коллекции, существующей вне лямбда-выражения. Map возвращает другой список того же размера, преобразуя каждый его элемент. Таким образом, с точки зрения производительности map определенно лучше, чем foreach. 

Класс case вместо кортежа 

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

def movieRatings(movies: List[Movie]): Unit = { movies .map(movie => (movie.name, movie.rating)) .filter(ratingTuple => ratingTuple._2 > 5) .foreach(movie => print(movie._1, x._2)) }

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

case class Rating(name: String, rating: Double) def movieRatings(movies: List[Movie]): Unit = { movies .map(movie => Rating(movie.name, movie.rating)) .filter(_.rating > 5) .foreach(movie => print(movie.name, movie.rating)) }

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

Интерполяция или конкатенация строк 

Возвращаясь к нашей игре в шарады, предположим, что кто-то отгадал первую половину названия фильма, такого как “Властелин колец”. Полное название может быть создано с помощью конкатенации строк, в результате чего возникнет новая строка, использующая оператор +. То, как компилятор обрабатывает ошибки в случае конкатенации строк, понять не так-то просто. 

def guessMovie(firstName: String, lastName: String): String = { firstName + " " + lastName }

Объединяя что-либо со String, следует рассмотреть вариант использования интерполяции строк. Он более удобный, безопасный, последовательный и читаемый. 

def guessMovie(firstName: String, lastName: String): String = { s"$firstName $lastName" }

Производительность конкатенации строк и интерполятора (s, f и raw) может варьироваться в зависимости от длины строки. 

Заключение 

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

Ссылки  https://github.com/lloydmeta/enumeratum/blob/master/enumeratum-core/src/main/scala/enumeratum/Enum.scala https://stackoverflow.com/questions/33593525/scala-safe-way-of-converting-string-to-enumeration-value https://stackoverflow.com/questions/28319064/java-8-best-way-to-transform-a-list-map-or-foreach https://nrinaudo.github.io/scala-best-practices/tricky_behaviours/string_concatenation.html

Перевод статьи Niharika Gupta: Readable code is better code


Поделиться статьей:


Вернуться к статьям

Комментарии

    Ничего не найдено.