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