playbook

KINOPLAN

SCALA STYLE GUIDE

Здесь содержатся рекомендации по стилю написания кода Scala в Kinoplan.

Цели этого руководства:

Если команда решает что-то писать не так в своем проекте, то она документирует это в CONTRIBUTING.md проекта.

Некоторые правила могут приносить вред в меньшинстве случаев (ухудшать читаемость кода, например), но ради единообразия им все равно нужно следовать. Однако все из них обсуждаемые.

Оглавление

  1. Синтаксический стиль
  2. Основы языка
  3. Обработка исключений
  4. Параллелизм
  5. Play Framework
  6. Akka
  7. Производительность
  8. Тестирование
  9. Документирование
  10. Git
  11. Разное

Синтаксический стиль

Соглашение об именовании

Соглашение об именовании переменных

Длина строки

Пробелы

Правило 30

Если элемент состоит из более чем 30 подэлементов, весьма вероятно, что существует серьезная проблема архитектуры. Поэтому, старайтесь придерживаться следующих рекомендаций:

Отступы

Вертикальные пробелы (пустые строки)

Круглые скобки

Фигурные скобки

Литералы

Импорты

Сортировка внутри класса

Сортировка внутри класса должна идти по порядку по следующему шаблону:

Pattern Matching

Инфиксная нотация

Избегайте инфиксной нотации для методов, которые не являются символическими методами.

// хорошо
releases.map(...)
status.contains("active")
   
// плохо
releases map (...)
status contains "active"

// но перегруженные операторы должны быть вызваны в инфиксном стиле
a mod b
a += 1

Анонимный метод

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

// хорошо
releases.map { release =>
  ...
}

// хорошо
releases.map(release => ...)

// плохо
releases.map(release => {
  ...
})

// плохо
releases.map { release => {
  ...
}}

// плохо
releases.map({ release => ... })

Основы языка

Case class

Перечисления

Apply методы

Модификатор override

Кортежи

Работа с датами

Вызов по имени

Множественные списки параметров

Используйте множественные списки параметров, если это сделает ваш код более элегантным. Порой бывает удобно делать последний явный список с одной лямбдой (например, для написания if/else в функциональном стиле).

def applyIf(predicate: A => Boolean)(action: A => A): A = {
  if (predicate(value)) action(value) else value
}

val updatedSale = sale.applyIf(_.status == Created) {
  _.copy(status = Approved)
}

Передача именованных функций

Перегрузка операторов

Используйте имена символических методов, если они не затрудняют понимание намерений методов. В противном случае, не используйте их.

// хорошо, говорящее именование
JsObject.empty ?+ ("comment" -> commentO)

// плохо
JsObject.empty.addIfDefined("comment" -> commentO)

// плохо, трудно понять
stream1 >>= stream2

// хорошо, очевидно что происходит
stream1.join(stream2)

Вывод типа

Оператор return

Избегайте использования оператора return. Компилятор Scala преобразует его в try/catch, что может привести к неожиданному поведению.

Функциональное программирование

Неявные преобразования (implicits)

Используйте неявные преобразования, только если:

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

Если вы хотите использовать неявное преобразование, всегда спрашивайте себя, есть ли способ достичь того же самого без их помощи.

Обработка исключений

Throw

Try vs try

Scala предоставляет монадическую обработку ошибок (через Try, Success и Failure), что облегчает цепочки действий. Используйте их, если вам ясна семантика ожидаемых ошибок и исключений. В противном случае предпочтительнее использовать try/catch.

Старайтесь не использовать Try в качестве возвращаемого типа для метода или переменной.

// не рекомендуется
def get(releaseId: Int): Try[Release] = ...

// хорошо
def get(releaseId: Int): Either[String, Release] = ...

Option

Монады

Одной из интересных особенностей Scala являются монады. Почти все (например, коллекции, Option, Future, Try) является монадой, и операции на них могут быть сцеплены вместе. Это невероятно мощная концепция, но сцепление следует использовать экономно. В частности:

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

Параллелизм

concurrent.Map

Явная синхронизация и параллельные коллекции

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

  1. scala.collection.concurrent.Map: Используйте, когда все состояния захвачены на карте, и ожидается высокая степень конкуренции.

     private[this] val currentMap = new scala.collection.concurrent.Map[String, String]
    
  2. java.util.Collections.synchronizedMap: Используйте, когда все состояния захвачены на карте, и конкуренция не ожидается, но вы все равно хотите сделать код безопасным. В случае отсутствия конкуренции JVM JIT-компилятор может удалить служебные данные синхронизации путем смещенной блокировки.

     private[this] val currentMap = java.util.Collections.synchronizedMap(new java.util.HashMap[String, String])
    
  3. Явная синхронизация путем синхронизации всех критических разделов: может использоваться для защиты нескольких переменных. Подобно предыдущему пункту, JVM JIT-компилятор может удалить служебные данные синхронизации путем смещенной блокировки.

     class Manager {
       private[this] var count = 0
       private[this] val map = new java.util.HashMap[String, String]
       def update(key: String, value: String): Unit = synchronized {
         map.put(key, value)
         count += 1
       }
       def getCount: Int = synchronized { count }
     }
    

Для вариантов 1 и 2 не позволяйте представлениям или итераторам коллекций выходить за пределы защищенной области. Это может произойти в неочевидными способами, например, при возврате Map.keySet или Map.values. Если представления или значения необходимы для передачи, создайте копию данных.

val map = java.util.Collections.synchronizedMap(new java.util.HashMap[String, String])

// Это не сработает!
def values: Iterable[String] = map.values

// Вместо этого скопируйте элементы
def values: Iterable[String] = map.synchronized { Seq(map.values: _*) }

Аннотация volatile

Пакет java.util.concurrent.atomic предоставляет примитивы для свободного доступа к примитивным типам, таким как AtomicBoolean, AtomicInteger и AtomicReference.

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

Предпочитайте Atomic переменные при явной синхронизации, когда:

// хорошо: четко и эффективно выражается только однократное выполнение параллельного кода
val initialized = new AtomicBoolean(false)
...
if (!initialized.getAndSet(true)) {
  ...
}

// плохо: менее понятно, что охраняется синхронизацией, может излишне синхронизировать
val initialized = false
...
var wasInitialized = false
synchronized {
  wasInitialized = initialized
  initialized = true
}
if (!wasInitialized) {
  ...
}

Закрытое поле

Стоит обратить внимание, что private поля по-прежнему доступны для других экземпляров одного и того же класса, поэтому защита его с помощью this.synchronized (или просто synchronized) технически недостаточна. Вместо этого сделайте поле private[this].

// не безопасно
class Example {
  private var count: Int = 0
  def inc(): Unit = synchronized { count += 1 }
}

// безопасно
class Example {
  private[this] var count: Int = 0
  def inc(): Unit = synchronized { count += 1 }
}

Изоляция

В целом, логика параллелизма и синхронизации должна быть изолирована настолько, насколько это возможно. Это фактически означает:

Play Framework

Структура проекта

Новые проекты

Контроллеры

Модели

Akka

Структура актора

Рассмотрим структуру работы с акторами в Play Framework на примере синхронизации оповещений:

  1. В директорию actors добавьте файл NotificationSyncActor.scala:

     package actors
    	
     import akka.actor.Actor
     import com.google.inject.Inject
     import filters.Log
     import services.notification.NotificationSyncService
    	
     import scala.concurrent.duration._
     import scala.concurrent.ExecutionContext
    	
     object NotificationSyncActor {
       case object PackageMissingNotificationSync
       case object PackageDeletingNotificationSync
       ...
    	  
       val SYNC_PERIOD = 1.hour
     }
    	
     class NotificationSyncActor @Inject()(
       notificationSyncService: NotificationSyncService
     )(implicit ex: ExecutionContext) extends Actor {
       import NotificationSyncActor._
    	
       val logger = Log.app
    	
       logger.info("[Start notification sync service module]")
    	  
       context.system.scheduler.schedule(30.seconds, SYNC_PERIOD, self, PackageMissingNotificationSync)
       context.system.scheduler.schedule(35.seconds, SYNC_PERIOD, self, PackageDeletingNotificationSync)
       ...
    	  
       override def receive: Receive = {
         case PackageMissingNotificationSync => notificationSyncService.updatePackageMissingNotifications
         case PackageDeletingNotificationSync => notificationSyncService.updatePackageDeletedNotifications
         ...
       }
     }
    
  2. В директорию services добавьте файл NotificationSyncService.scala где будут располагаться методы обновления оповещений.
  3. В директорию modules добавьте файл NotificationModule.scala:

     package modules
    
     import actors.NotificationSyncActor
     import com.google.inject.AbstractModule
     import play.api.libs.concurrent.AkkaGuiceSupport
    	
     class NotificationModule extends AbstractModule with AkkaGuiceSupport {
       override def configure() = {
         bindActor[NotificationSyncActor]("notification-sync-actor")
       }
     }
    
  4. Затем, в conf/application.conf добавьте:

     ...
    	
     play.modules.enabled += "modules.NotificationModule"
    	
     ...
    

Cтоит сделать важное уточнение, что решение о том, распологать ли основной код в самом акторе (внутри receive или ниже recieve), либо поместить код в отдельный класс решать вам.

Ask и Tell

Производительность

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

Первым делом ознакомьтесь с характеристиками производительности. Вероятно вы просто неправильно используете коллекции или методы.

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

Однако для чувствительного к производительности кода вот несколько советов.

Тесты производительности

Очень сложно написать хороший тест производительности, потому что компилятор Scala и JVM JIT-компилятор делают много магии для кода. Чаще всего, ваш microbenchmark не измеряет то, что вы хотите измерить.

Используйте jmh если вы пишете тесты производительности кода. Убедитесь, что вы прочитали все примеры тестов производительности так, что вы понимаете эффект устранения мертвого кода, постоянное складывание и развертывание цикла на microbenchmarks.

While

Используйте while циклы вместо for или функциональных преобразований (например, map, foreach). For циклы и функциональные преобразования с точки зрения производительности очень медленные.

val points = // Array[Int]


// более низкая производительность, по сравнению с примером ниже
val newPoints = points.zipWithIndex.map { case (point, i) =>
  if (i % 2 == 0) 0 else point
}

// более высокая производительность, по сравнению с примером выше
val newPoints = new Array[Int](points.length)
var i = 0
val length = newPoints.length
while (i < length) {
  newPoints(i) = if (i % 2 == 0) 0 else points(i)
  i += 1
}

Option и null

Для кода, чувствительного к производительности, предпочитайте null значение над Option, чтобы избежать вызовов виртуальных методов и упаковки. Четко обозначьте поля с нулевым значением с помощью Nullable.

@javax.annotation.Nullable
private[this] var nullableRelease: Release = _

Библиотека коллекций Scala

Для кода, чувствительного к производительности, предпочитайте библиотеку коллекции Java поверх Scala, поскольку библиотека коллекции Scala часто медленнее, чем Java.

private[this]

Для кода, чувствительного к производительности, предпочитайте private[this] вместо private, так как private[this] генерирует поле, а не создает метод доступа. JVM JIT-компилятор не всегда может использовать методы доступа к полю private, поэтому безопаснее использовать private[this], что гарантирует отсутствие вызова виртуального метода для доступа к полю.

Тестирование

Пример теста:

package utils

import org.scalatestplus.play.PlaySpec
import utils.premiere.Helpers.StringExtended

class HelpersSpec extends PlaySpec {
  import HelpersSpec._

  texts.zipWithIndex.foreach {
    case ((encodedText, text), index) => {
      s"be valid decode text $index" in {
        encodedText.decode mustBe text.toCheckDecode
      }
    }
  }

  texts.zipWithIndex.foreach {
    case ((encodedText, text), index) => {
      s"be valid encode text $index" in {
        text.encode mustBe encodedText
      }
    }
  }
}

object HelpersSpec {
  val texts = ...
}

Документирование

Комментарии

Apidoc

Для наших методов API в контроллерах мы используем синтаксис документирования Apidoc.

Придерживайтесь следующих рекомендаций:

Scaladoc

Git

Разное

Настройка кодстайла в проекте

Рекомендуется использовать форматеры, для автоматического применения кодстайла ко всему проекту, например scalafmt, конфигурацию которого можно найти в этом же репозитории и которая задает кодстайл принятый в компании (файл .scalafmt.conf).

Для поддержания кода в чистоте так же рекомендуется использовать scalafix, в частности помогает с организацией импортов в нужном порядке, рекомендуемую конфигурацию, так же можно найти в этом репозитории (файл .scalafix.conf)

Архитектура

Технический долг

Не изобретай колесо

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

// плохо, зачем плодить новый класс если есть готовая реализация ниже
class Sha256 {
  ...
}

// хорошо
deg toSha256Hex(value: String) = MessageDigest.getInstance(SHA256).digest(value).toHex

Исключения

Правило бойскаута

Правило бойскаута:

Оставь место стоянки чище, чем оно было до твоего прихода.

Из книги Роберта Мартина Чистый код:

Если мы все будем оставлять свой код чище, чем он был до нашего прихода, то код попросту не будет загнивать. Чистка не обязана быть глобальной. Присвойте более понятное имя переменной, разбейте слишком большую функцию, устраните одно незначительное повторение, упростите сложную цепочку условий. Представляете себе работу над проектом, код которого улучшается с течением времени? Но может ли профессионал позволить себе нечто иное? Разве постоянное совершенствование не является неотъемлемой частью профессионализма?

В соответствии с этими правилами, старайся следовать следущим рекомендациям: