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



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

Введение 


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

668   0  

Comments

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