Вступление

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

Не используйте очень большую лямбду

Использование Lambda весьма полезно, поскольку его можно использовать без присвоения переменной. Однако сложные лямбда-функции могут затруднить чтение кода и отладку. Давайте рассмотрим пример, в котором мы хотим классифицировать фильм на хороший, плохой или средний.

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

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

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. Это также намного более читабельно.

Обработка перечислений, когда значение отсутствует

Есть много способов справиться с ситуацией, когда пользователь вводит недопустимое значение перечисления. Один из способов - вернуть 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 vs Map для преобразования

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

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

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

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

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

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

Допустим, мы хотим рекомендовать названия фильмов на основе оценок покупателей. Пример: Список ((Мистическая река, 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
  }

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

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

Https://medium.com/@dkomanov/scala-string-interpolation-performance-21dc85e83afd