Clean Code

Введение 

Большинство начинающих программистов сталкивается со многими дилеммами в процессе написания кода, например задумываются о том, какой код будет востребован в индустрии. У каждой компании свои бенчмарки, лучшие практики для написания кода и основополагающие принципы. Однако есть один критерий кода, который единодушно поддерживается всеми: читаемость. Читаемый код и проживет дольше, и особых проблем с обслуживанием и пониманием не доставит. Более того, он позволяет будущим поколениям разработчиков легко вносить в него изменения. Проблема читаемости кода актуальна и для начинающих разработчиков 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

Предыдущая статьяУчимся писать строки документации в Python
Следующая статьяЧто значит this в JavaSсript?