Здесь содержатся рекомендации по стилю написания кода Scala в Kinoplan.
Цели этого руководства:
Если команда решает что-то писать не так в своем проекте, то она документирует это в CONTRIBUTING.md проекта.
Некоторые правила могут приносить вред в меньшинстве случаев (ухудшать читаемость кода, например), но ради единообразия им все равно нужно следовать. Однако все из них обсуждаемые.
Классы, трейты, объекты должны следовать стилю UpperCamelCase.
class CategoryCinemaController
case class CategoryCinema
trait CategoryCinemaJson
object CategoryCinema
Аббревиатуры именуются как отдельные слова - в UpperCamelCase (исключение - DAO
).
// хорошо
class RepertoireDAO
// хорошо
class KdmDAO
// плохо
class KDMDAO
Пакеты - строчными буквам ASCII и желательно (но не обязательно) одним словом (если возникает необходимость в нескольких словах - стоит подумать о вложенных пакетах).
package models.placement.extensions
Методы/функции должны быть названы в стиле camelCase.
def cinemaCategories
Константы должны следовать стилю UpperSnakeCase и быть помещены в объект companion.
object Region {
val DEFAULT_TIME_ZONE = 3
}
Перечисления должны быть в UpperCamelCase.
sealed abstract class PaymentMethod ...
object PaymentMethod ... {
case object Advance extends PaymentMethod
case object FullPayment extends PaymentMethod
...
}
Аннотации должны быть в UpperCamelCase.
@SuppressWarnings
Поля json
и поля в базе данных должны быть в SnakeCase.
{
"cinema_id": 1803,
"is_on_sale": true
}
Поля headers
должны быть через -
.
X-Device-Token
Сокращение слов допустимо только в самых общепринятых случаях, в остальных писать полностью. Например DAO
, но не cfg
и conn
, вместо этого config
и connection
.
В рамках одного файла старайтесь придерживаться одного стиля в использовании конструкций, например, не стоит писать Try recover (без возвращаемого значения) и try catch вместе.
Переменные должны быть названы в стиле camelCase и должны иметь “говорящие” имена.
val serverPort = 3000
val clientPort = 9000
В названии переменной не должно быть тавтологии, название переменной должно быть читабельным.
val releases = ... // хорошо
val releaseList = ... // плохо
val seance = ... // хорошо
val s = ... // плохо
val user = ... // хорошо
val userModel = ... // плохо
i
используется в качестве индекса цикла для небольшого тела цикла.Для значений типа Option[_]
в конце имени добавляется O
. Это также верно для атрибутов/методов модели, но неверно для атрибутов/методов всех остальных классов (сервисы, контроллеры и др.)
def list(releaseIdO: Option[Int]) = ...
case class UserProfile(
addressO: Option[Address]
) {
def streetO: Option[String] = addressO.map(_.street)
}
class UserService {
def authUser: Option[User] = ...
}
Для значений типа Future[_]
в конце имени добавляется F
.
val releaseF: Future[Release] = ...
Добавляйте новую строку в конец каждого файла.
class C {
def f: Int = 1
def g: String = "s"
}
Отступы для Play Json комбинаторов и им подобным.
// хорошо,
// так как существующие форматеры(scalafmt, IDEA xml)
// не могут обработать такой кейс
val onlyRuEnReads: Reads[Title] = (
(__ \ "ru").readWithDefault("") and
(__ \ "en").readWithDefault("")
) (Title.apply(_, _))
// хорошо
val onlyRuEnReads: Reads[Title] = (
(__ \ "ru").readWithDefault("") and
(__ \ "en").readWithDefault("")
) (Title.apply(_, _))
Если элемент состоит из более чем 30 подэлементов, весьма вероятно, что существует серьезная проблема архитектуры. Поэтому, старайтесь придерживаться следующих рекомендаций:
Один пробел до и после операторов.
def mul(a: Int, b: Int) = a * b // хорошо
def mul(a: Int, b: Int) = a*b // плохо
Один пробел после запятой.
List(1, 2, 3) // хорошо
List(1,2,3) // плохо
Один пробел до и после двоеточия для type bound
.
// хорошо
def doSomething[M[_] : Monoid : Writes](m: M[Int]) = ...
// плохо
def doSomething[M[_]: Monoid: Writes](m: M[Int]) = ...
Один пробел после двоеточия.
def mul(a: Int, b: Int): Int = a * b // хорошо
//не ставь пробелы перед двоеточиями, кроме type bound
def mul(a : Int, b : Int) : Int = a * b // плохо
//не пропускай пробелы после двоеточия
def mul(a:Int, b:Int):Int = a * b // плохо
Два пробела для отсупов.
if (smth) {
println("Smth!") // хорошо
}
if (smth) {
println("Smth!") // плохо
}
Для объявления метода используйте два пробела для отступа. Если параметры и возращаемый тип не помещаются в одну строку, то поместите каждый параметр в новую строку, при этом возвращаемый тип должен быть с новой строки.
// хорошо
def planning(start: DateTime, isManual: Boolean): Boolean = {
// плохо
def planning(start: DateTime, isManual: Boolean)
: Boolean = {
// плохо
def planning(start: DateTime,
isManual: Boolean): Boolean = {
// хорошо
def allocating(
placements: List[Placement],
seances: List[Seance],
twisterHelp: TwisterHelp,
userIds: Set[String]
): (TwisterHelp, List[PlacementApart]) = {
// плохо
def allocating(
placements: List[Placement],
seances: List[Seance],
twisterHelp: TwisterHelp,
userIds: Set[String])
:(TwisterHelp, List[PlacementApart]) = {
Для классов, заголовок которых не помещается в одну строку, используйте два пробела для отступа его параметров, затем поместите закрывающую скобку и расширение на следующую строку.
// хорошо
class SeanceController @Inject()(
authUtils: AuthUtils,
authFilters: AuthFilters,
seanceService: SeanceService,
releaseService: ReleaseService
) extends InjectedController with Smth {
// плохо
class SeanceController @Inject()(
authUtils: AuthUtils,
authFilters: AuthFilters,
seanceService: SeanceService,
releaseService: ReleaseService)
extends InjectedController
with Smth
{
Для вызова метода, класса если не помещается в одну строку, то используйте два пробела для отсупа его параметров и поместите каждый параметр в новую строку.
// хорошо
allocating(
placements,
suitableSeances,
twisterHelp,
userIds
)
// плохо
allocating(placements,
suitableSeances,
twisterHelp,
userIds)
// хорошо
val result = allocating(
placements,
suitableSeances,
twisterHelp,
userIds
)
// плохо
val result = allocating(placements,
suitableSeances,
twisterHelp,
userIds)
// хорошо
new Info(
placements,
suitableSeances,
twisterHelp,
userIds
)
// плохо
new Info(
placements,
suitableSeances,
twisterHelp,
userIds)
Не используйте выравнивание по вертикали. Это затрудняет изменение выровненного кода в будущем. Исключения могут соствлять не *.scala
файлы: routes, sbt файлы, конфиги и прочее.
// хорошо
val MIN_QUOTA = 0
val MAX_QUOTA = 100
val DEFAULT_QUOTA = 50
// плохо
val MIN_QUOTA = 0
val MAX_QUOTA = 100
val DEFAULT_QUOTA = 50
// хорошо
val seances = ...
val releases = ...
// тоже хорошо (логическая группа полей)
val releaseIds = ...
val releases = ...
// хорошо, так как похожи по смыслу и после равно идет однострочный код
val seances = seanceService.find(...)
val releases = releaseService.find(...)
val cinemas = cinemaService.find(...)
// плохо, так как после равно идет многострочный код
val seances = {
...
}
val releases = {
...
}
if
— Опускайте фигурные скобки, если у вас есть предложение else.
В противном случае заключите содержимое в фигурные скобки, даже если оно состоит только из одной строки.for
— Опускайте фигурные скобки, если у вас есть предложение yield.
В противном случае окружите содержимое фигурными скобками, даже если оно состоит только из одной строки.case
— Всегда опускайте фигурные скобки в предложениях case.
// хорошо
if (smth) {
println("Smth!")
}
// плохо
if (smth)
println("Smth!")
// хорошо
if (smth) first else second
// хорошо
if (smth) first
else second
//плохо
if (smth) first
else {
...
}
//хорошо
if (smth) {
...
} else {
...
}
//хорошо
for {
x <- board.rows
y <- board.files
} yield (x, y)
// хорошо
try {
smth()
} catch {
...
}
//хорошо
news match {
case "good" => println("Good news!")
case "bad" => println("Bad news!")
}
Не нужно ставить фигурные скобки, если тело метода целиком находится на той же строке.
def f: Int = calcSomeNumber
Или всё тело содержит не более трех вертикальных отступов
def f: Int =
calcSomeNumber
.map(...)
.head
Но допустимо, если отступов больше:
def f: Int = {
calcSomeNumber
.filter(...)
.map(...)
.head
}
Всегда используйте прописные литералы, вместо строчных. Строчные литералы труднее дифференцировать, особенно это заметно для типа Long
- l
и 1
.
val longNumber = 1L // хорошо
val floatNumber = 1F // хорошо
val longNumber = 1l // плохо
val floatNumber = 1f // плохо
Большинство import
в файле должны располагаться вверху, прямо под названием пакета. Единственное случай, когда вы можете это нарушить, это импорт некоторых или всех имен из объекта внутрь класса или метода (например, довольно удобно импортить request
внутри метода).
def find(id: ObjectId) = (
authUtils.authenticateAction() andThen
authFilters.accessAdvertising andThen
authUtils.campaignAction(id)
) { request =>
import request.{campaign, user}
placementService.find(campaign, user) match {
...
}
}
_
, если вы не импортируете более 4 сущностей. При таком импорте код становится менее устойчивым к внешним воздействиям, происходит лишнее загромождение пространоства имен, увеличивается риск появления конфликта имен. Более подробно можно почитать тут.import
используйте полный путь (например, scala.util.Random
) вместо относительного (например, util.Random
). Однако, для решения конфликта гораздо удобнее использовать относительный путь (например, mutable.Map
и Map
, ucs.Cinema
и kinoplan.Cinema
и т.д.).java.*
и javax.*
scala.*
akka.*
org.*
, com.*
и т.д.).Конфигурация стиля кода InteliJ позволяет автоматически группировать импорт по пространству имен. Используйте шаблон import layout в Intelij, для этого зайдите в настройки Settings->Editor->Code Style->Scala
, затем выберете вкладку Imports и приведите import layout к следующему виду:
java
javax
_______ blank line _______
scala
_______ blank line _______
akka
all other imports
Code->Optimize Imports
) перед созданием пулреквеста, для поддержания чистоты import
.Сортировка внутри класса должна идти по порядку по следующему шаблону:
class CampaignController {
// сначала импорты, если необходимо
import Helper._
...
// потом переменные
val PORT = 3000
...
// затем методы
// методы поиска
def all = ...
...
// методы создания
def create() = ...
...
// методы обновления и/или изменения
def update() = ...
def copy() = ...
...
// методы удаления
def delete = ...
...
}
Для метода, все тело которого является выражением, поместите match на ту же строку что и объявление метода.
def title(status: String) = status match {
...
}
Если при вызове функции существует только один case, то поместите его в ту же строку, что и вызов функции. Если же вызов case не помещается в ту же строку, то перенесите его на следующую строку.
// хорошо
statuses.zipWithIndex.map { case (status, index) =>
...
}
// хорошо
tuple.zipWithIndex.map {
case ((status, placement, campaign, calculation), index) =>
...
}
// плохо
tuple.zipWithIndex.map {
case ((status, placement, campaign, calculation), index) => {
...
}
}
Если при вызове функции существует несколько case, то поместите каждый case на новую строку.
list.map {
case a: User => ...
case b: Hall => ...
}
case
без фигурных скобок.Если case
соответствует типу объекта, то не разворачивайте его аргументы, так как это делает рефакторинг более трудным и код более подвержен ошибкам.
case class User(id: Int, title: String)
case class Cinema(id: Int, title: String)
// плохо потому, что
// 1. При добавление новых полей нужно их добавлять и в шаблоне
// 2. Легко словить несовпадение элементов
lists.map {
case list @ User(id, _) => ...
case list @ Cinema(_, title) => ...
}
// хорошо
lists.map {
case list: User => ...
case list: Cinema => ...
}
Избегайте инфиксной нотации для методов, которые не являются символическими методами.
// хорошо
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. Вместо этого используйте конструктор копирования. Наличие изменяемых классов через var
может быть подвержено ошибкам, например, хэш-карты могут поместить объект в неправильный сегмент, используя старый хэш-код или вы присваиваете значение из immutable
в mutable
переменную, а затем изменяете значение mutable
переменной, то значение immutable
переменной также меняется.
// хорошо
case class Release(id: Int, title: String, age: Int)
// плохо
case class Release(id: Int, title: String, var age: Int)
// Для изменения значений, использовать копирующий конструктор для создания нового экземпляра
val release = Release(1, "Человек, который изменил всё", 12)
val updatedRelease = release.copy(age = 16)
В очень исключительных случаях допускается наличие у класса параметра var
, но этот класс определенно должен быть вспомогательным и не являться моделью для коллекции из бд. И разумеется, вы должны обладать достаточными компетенциями, чтобы это использовать.
Используйте метод apply
для case class
в тех случаях, когда вы хотите создать модель через кастомный список параметров. Такой метод должен обязательно находится в объекте-компаньоне этого класса и иметь в качестве возвращаемого типа название класса.
case class Period(start: DateTime, end: DateTime)
object Period {
// хорошо
def apply(start: DateTime): Period = ...
// плохо
def apply(start: DateTime): String = ...
}
Во всех остальных случаях избегайте использования метода apply
.
Не добавляйте модификатор override
, если это действительно не требуется, как для переопределения конкретных методов, так и для реализации абстрактных методов, так как Scala компилятор в большинстве случаев не трубует явного указывания модификатора override
. Например:
// так не надо делать
override def writes(release: Release): JsValue
// однако так нужно
override def equals(apart: Any): Boolean = {
Используйте кортежи для извлечения нескольких переменных из одного выражения.
val (a, b) = (0, 1)
Однако, не используйте кортежи в конструкторе класса, особенно когды вы помечаете поле как @transient
, так как компилятор Scala создает дополнительное поле Tuple2
которое не будет @transient
для примера выше.
class Example {
// не сработает, так как компилятор сгенерит non-transient Tuple2
@transient private val (a, b) = someTuple2()
}
val (a, b) = (0, 1)
Используйте pattern matching для доступа к элементам кортежа вместо доступа по номерам.
// плохо
names.map(_._1)
// хорошо
names.map { case (firstName, _) => firstName }
Всегда используйте Joda DateTime
для работы с датами, даже если вам нужда только часть полной даты (вместо LocalDate
, LocalTime
, LocalDateTime
и др.).
Сейчас уже написано много кода, большая часть на DateTime
, а слишком большой выгоды от строгого разделения нет.
В случаях, когда парметр может не понадобиться при вызове метода, используйте вызов по имени.
def error(message: => String): Unit = ...
Однако не забывайте о побочном эффекте, который может нести в себе вызов по имени.
Используйте множественные списки параметров, если это сделает ваш код более элегантным. Порой бывает удобно делать последний явный список с одной лямбдой (например, для написания 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)
}
Если внутри функции используется лишь одно поле, то используется вызов, через _
и круглые скобки:
cinema.filter(_.id == cinemaId)
Если обратный вызов принимающей функции и передаваемая функция имеют одинаковую сигнатуру, то аргументы и подчеркивания должны быть опущены:
// плохо
Option(123).map(println(_))
// хорошо
Option(123).map(println)
Используйте имена символических методов, если они не затрудняют понимание намерений методов. В противном случае, не используйте их.
// хорошо, говорящее именование
JsObject.empty ?+ ("comment" -> commentO)
// плохо
JsObject.empty.addIfDefined("comment" -> commentO)
// плохо, трудно понять
stream1 >>= stream2
// хорошо, очевидно что происходит
stream1.join(stream2)
protected
) методы и атрибуты должны иметь явную типизацию.Избегайте использования оператора return. Компилятор Scala преобразует его в try/catch
, что может привести к неожиданному поведению.
Для методов, предназначенных для рекурсивной обработки, старайтесь по возможности применять аннотацию @tailrec
, если не происходят асихронные операции в рекусрии. Во-первых, тем самым компилятор Scala проверяет метод на рекурсивность(например, что не используются замыкания, функциональные преобразования и т.д.). Во-вторых, компилятор оптимизирует рекурсию.
Не используйте императивный стиль (циклы, var, mutable структуры данных и т. д.), если это не обусловлено соображениями производительности.
Используйте неявные преобразования, только если:
ClassTag
, TypeTag
).play-json
, BSON readers & writers
и т. п.)
и все поля модели отображаются в (де-)сериализуемую структуру как есть.
trait TitleJson {
// уместно имплицитное преобразование
implicit val writes: Writes[Title] = Json.writes
// можно передавать только эксплицитно, т. к. есть не все поля
val onlyEnRuWrites: Writes[Title] = (o: Title) => Json.obj(
"en" -> o.en,
"ru" -> o.ru
)
// то же
val onlyRuEnReads: Reads[Title] = (
(__ \ "ru").readWithDefault("") and
(__ \ "en").readWithDefault("")
) (Title.apply(_, _))
}
object Helpers {
implicit class ListExtended[T](val items: List[T]) extends AnyVal {
def groupById[I](getId: T => I): Map[I, T] = {
items.map(item => getId(item) -> item).toMap
}
}
}
Если вы используете неявные преобразования, то вы должны убедиться, что другой программист может понять семантику использования, не читая само неявное определение. Неявные преобразования имеют довольно сложные правила и при чрезмерном использовании могут сделать кодовую базу трудной для понимания.
Если вы хотите использовать неявное преобразование, всегда спрашивайте себя, есть ли способ достичь того же самого без их помощи.
throw
, аннотаций @throw
и т.д.Не используйте методы, которые выбрасывают исключения.
// плохо, выбрасывает исключение
require(limit > 0)
// хорошо
if (limit > 0) {
...
} else {
...
}
Это верно даже когда метод, бросающий исключение, гарантированно не сработает:
// нельзя
list.groupBy(_.id).mapValues(_.head)
// можно
list.groupBy(_.id)
.flatMap { case (id, list) => list.headOption.map((id, _)) }
head
и get
для option и коллекций. Вместо этого используйте headOption
и раскрытие через map/flatMap/etc.
Scala предоставляет монадическую обработку ошибок (через Try
, Success
и Failure
), что облегчает цепочки действий. Используйте их, если вам ясна семантика ожидаемых ошибок и исключений. В противном случае предпочтительнее использовать try/catch
.
Старайтесь не использовать Try
в качестве возвращаемого типа для метода или переменной.
// не рекомендуется
def get(releaseId: Int): Try[Release] = ...
// хорошо
def get(releaseId: Int): Either[String, Release] = ...
Option
когда значение может быть пустым. По сравнению с null
, у Option
явно указано в API что занчение может быть None
.При построении Option
используйте Some
для переменных, которые не могут вернуть null
.
def get(release: Release): Option[Release] = Some(release)
Если же при построении Option
используется преобразование, которое может вернуть null
, то используйте Option
.
// хорошо, вернет None если transform вернет null
def get1(release: Release): Option[Release] = Option(transform(release))
// плохо, вернет Some(null) если transform вернет null
def get2(release: Release): Option[Release] = Some(transform(release))
get
для Option
.Одной из интересных особенностей Scala являются монады. Почти все (например, коллекции, Option
, Future
, Try
) является монадой, и операции на них могут быть сцеплены вместе. Это невероятно мощная концепция, но сцепление следует использовать экономно. В частности:
for
.for
для 1 операции, если то же самое можно сделать через map/flatMap/etc.
map
/flatMap
/foreach
/for
, для этого используйте один из инструментов для работы с nested effects (например, в cats это OptionT
, EitherT
и др.)Цепочку часто можно сделать более понятной, дав промежуточному результату имя переменной, явно введя переменную и разбив ее на более процедурный стиль.
Await
в конечной версии кода, но можно в ситуациях перехода с блокирующего API на неблокирующий. Например, есть устаревшая библиотека Salat
и новая Reactive Mongo
, и мы хотим постепенно переходить на вторую.Если используете блокирующие библиотеки, то проверьте, необходимо ли для них настроить отдельных пул потоков. Это верно для Play и Akka, вероятно, верно и для всех асинхронных фреймворков.
Про асинхронные веб-серверы доступно здесь.
Для цепочек Future
всегда следите за тем, могут ли они выполняться параллельно.
До выполнения предыдущего Future
в for
следующий несозданный Future не будет запущен,
из-за чего может увеличиться время выполнения.
// будет работать последовательно
val flatRes: Future[List[Int]] = for {
scalaFooRes <- scalaFoo
scalaBarRes <- scalaBar
scalaBazRes <- scalaBaz
} yield (scalaFooRes ++ scalaBarRes ++ scalaBazRes)
// параллельно
val foo = scalaFoo
val bar = scalaBar
val baz = scalaBaz
for {
scalaFooRes <- foo
scalaBarRes <- bar
scalaBazRes <- baz
} ...
scala.collection.concurrent.Map
вместо java.util.concurrent.ConcurrentHashMap
java.util.concurrent.ConcurrentHashMap
. Это связано с тем, что метод getOrElseUpdate
в scala.collection.concurrent.Map
не являлся атомарной и был исправлен только в версии Scala 2.11.6 SI-7943Существует 3 рекомендуемых способа обеспечения одновременного доступа к общим состояниям. Не смешивайте их, потому что это может сделать программу очень трудной для понимания и привести к взаимоблокировкам.
scala.collection.concurrent.Map
: Используйте, когда все состояния захвачены на карте, и ожидается высокая степень конкуренции.
private[this] val currentMap = new scala.collection.concurrent.Map[String, String]
java.util.Collections.synchronizedMap
: Используйте, когда все состояния захвачены на карте, и конкуренция не ожидается, но вы все равно хотите сделать код безопасным. В случае отсутствия конкуренции JVM JIT-компилятор может удалить служебные данные синхронизации путем смещенной блокировки.
private[this] val currentMap = java.util.Collections.synchronizedMap(new java.util.HashMap[String, String])
Явная синхронизация путем синхронизации всех критических разделов: может использоваться для защиты нескольких переменных. Подобно предыдущему пункту, 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: _*) }
Пакет java.util.concurrent.atomic
предоставляет примитивы для свободного доступа к примитивным типам, таким как AtomicBoolean
, AtomicInteger
и AtomicReference
.
Всегда предпочитайте Atomic переменные в @volatile
. Они имеют строгий набор функциональных возможностей и более заметны в коде. Atomic переменные реализуются с использованием @volatile
под капотом.
Предпочитайте Atomic переменные при явной синхронизации, когда:
getAndSet
.// хорошо: четко и эффективно выражается только однократное выполнение параллельного кода
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 }
}
В целом, логика параллелизма и синхронизации должна быть изолирована настолько, насколько это возможно. Это фактически означает:
@Singleton
сервисы, DAO и акторы. Для контроллеров это не нужно, Play работает с ними сам.http.secret.key
(а не копировать из другого проекта) при помощи команды playGenerateSecret
в sbt
.errors/
вашего проекта, при этом логически разделяя их по папкам и классам (например: внешние и внутринние ошибки в папку external
и internal
соответсвенно; ошибки клиента и сервиса в ClientErrors.scala
и ServiceErrors.scala
соответственно и т.д.).Внутри Result
не должно быть дополнительных преобразований, фильтрации и прочего.
// плохо
Ok(
cinemas.map { cinema =>
cinema.filter(_.isFondKino)....
....map(Json.toJson)
}
)
// хорошо
Ok(Json.toJson(cinemas))
NoContent
, не Ok
и уж тем более не Ok(Json.toJson("status" -> "ok"))
!case class
, object
и trait
для Writes/Reads
, Headers
и DSL
.Для Writes
всегда обозначайте принимаемую модель буквой o
. Благодаря этому, при создании модели более удобно копипастить структуру из существующей модели. К тому же, метод находится в модели и применяется для нее же, поэтому нет смысла указывать полное имя.
// плохо
def writes(agency: Agency) = ...
// хорошо
def writes(o: Agency) = ...
Writes
разрешается создавать как в нотации функции, так и в стандартной нотации. Однако старайтесь в рамках одного проекта придерживаться конкретной нотации.
// хорошо
implicit val writes: Writes[Agency] = (o: Agency) => { ... }
// хорошо
implicit val writes = new Writes[Agency] {
def writes(o: Agency): JsObject = Json.obj(
...
)
}
case class SomeModel(value: String)
trait SomeModelBson { ... }
trait SomeModelJson { ... }
trait SomeModel extends SomeModelBson with SomeModelJson
play-json
, Reactive Mongo
и др. везде, где это позволяет избавиться от boilerplate-кода и повысить читаемость.JsonNaming
для play-json
, аннотация @Key
для Mongo
).Грабли: при использовании using
в макросах Reactive Mongo
или play-json
(они практически одинаковые) будет сбрасываться конфигурация:
// из-за using snake case не применится
implicit def config = MacroConfiguration(SnakeCase)
implicit val handler: BSONDocumentHandler[SeanceExternal] = Macros.using[MacroOptions.ReadDefaultValues].handler
// все сработает
implicit def config: Aux[MacroOptions.ReadDefaultValues] = MacroConfiguration(SnakeCase)
implicit val handler: BSONDocumentHandler[SeanceExternal] = Macros.handler
Reactive Mongo
(Salat
- deprecated).Рассмотрим структуру работы с акторами в Play Framework на примере синхронизации оповещений:
В директорию 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
...
}
}
services
добавьте файл NotificationSyncService.scala
где будут располагаться методы обновления оповещений.В директорию 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")
}
}
Затем, в conf/application.conf
добавьте:
...
play.modules.enabled += "modules.NotificationModule"
...
Cтоит сделать важное уточнение, что решение о том, распологать ли основной код в самом акторе (внутри receive
или ниже recieve
), либо поместить код в отдельный класс решать вам.
Вместо .tell
и .ask
используйте !
и ?
соответсвенно.
// плохо
actorFirst.tell(msg)
actorSecond.ask(msg)
// хорошо
actorFirst ! msg
actorSecond ? msg
Для ?
используйте неявный timeout
.
// плохо
(actorExample ? msg)(5.minutes)
// хорошо
implicit val timeout = Timeout(5.minutes)
actorExample ? msg
Для подавляющего большинства кода, который вы пишете, производительность не должна быть проблемой. А если она возникает, то почти наверняка из-за незнания базовых структур данных и неверных представлениях о сложности используемых операций.
Первым делом ознакомьтесь с характеристиками производительности. Вероятно вы просто неправильно используете коллекции или методы.
И если после этого вы не испытываете проблем с производительностью, то можете пропустить этот раздел.
Однако для чувствительного к производительности кода вот несколько советов.
Очень сложно написать хороший тест производительности, потому что компилятор Scala и JVM JIT-компилятор делают много магии для кода. Чаще всего, ваш microbenchmark не измеряет то, что вы хотите измерить.
Используйте jmh если вы пишете тесты производительности кода. Убедитесь, что вы прочитали все примеры тестов производительности так, что вы понимаете эффект устранения мертвого кода, постоянное складывание и развертывание цикла на microbenchmarks.
Используйте 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
}
Для кода, чувствительного к производительности, предпочитайте null
значение над Option
, чтобы избежать вызовов виртуальных методов и упаковки. Четко обозначьте поля с нулевым значением с помощью Nullable
.
@javax.annotation.Nullable
private[this] var nullableRelease: Release = _
Для кода, чувствительного к производительности, предпочитайте библиотеку коллекции Java поверх Scala, поскольку библиотека коллекции Scala часто медленнее, чем Java.
Для кода, чувствительного к производительности, предпочитайте private[this]
вместо private
, так как private[this]
генерирует поле, а не создает метод доступа. JVM JIT-компилятор не всегда может использовать методы доступа к полю private
, поэтому безопаснее использовать private[this]
, что гарантирует отсутствие вызова виртуального метода для доступа к полю.
test
вашего проекта.class
, что расширяется с помощью PlaySpec
. Также при необходимости расширяется с помощью MockitoSugar
, Results
, WordSpec
, Matchers
, Inspectors
.Spec
на конце. Например, если вы хотите протестировать ExampleController.scala
, то ваш файл с тестами должен называться ExampleControllerSpec.scala
.Пример теста:
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 = ...
}
Для наших методов API в контроллерах мы используем синтаксис документирования Apidoc.
Придерживайтесь следующих рекомендаций:
headers
, то укажите @apiHeader
и @apiHeaderExample
.json
, то укажите все описания полей через @apiParam (body)
и пример входящего json
через @apiParamExample {json}
.@apiErrorExample
со всеми возмоными ошибками.[]
.--fixup
и --autosquash
, но можно и просто через rebase
.Рекомендуется использовать форматеры, для автоматического применения кодстайла ко всему проекту, например scalafmt, конфигурацию которого можно найти в этом же репозитории и которая задает кодстайл принятый в компании (файл .scalafmt.conf).
Для поддержания кода в чистоте так же рекомендуется использовать scalafix, в частности помогает с организацией импортов в нужном порядке, рекомендуемую конфигурацию, так же можно найти в этом репозитории (файл .scalafix.conf)
Старайтесь делать контроллеры тонкими.
Старайтесь делать акторы также тонкими, то есть отвечающими за отправку сообщений и делегирующими остальную работу сервисам. Возможно нарушение этого принципа, если сам актор рассматривается как сервис (в нем нет особой логики обмена сообщениями, например простой алгоритм, выполняющийся по расписанию).
Место использования DAO - всегда сервис (или актор, если мы рассматриваем его как сервис).
Копипаст в рамках одного репозитория запрещен. В особых случаях это допустимо, но должно обсуждаться.
Если код переиспользуется несколькими подпроектами, то он должен выноситься в еще один подпроект, примерно где-то в папке base
или common
.
Подпроекты должны быть адекватного размера, не слишком большими, но и достаточно маленькими, разбитыми по зонам ответственности.
Документирование архитектуры: описывать все ключевые моменты принятых архитектурных решений (лучше всего это писать в README.md
). Например:
Другими словами все, что поможет принять оптимальные решения при последующих доработках проектов.
Когда есть существующий хорошо проверенный метод или библиотека или фреймворк, которые способны решить вашу задачу и не имеют проблем с производительностью, то используйте их. Переосмысление такого метода может привести к ошибкам и потребовать время на написание тестов (если вы не забудете их написать!). Если вы не знаете о существующих технологиях, то потратьте время на исследование. Как правило, нескольких часов поиска в интернете хватает, для того, чтобы определить варианты решения вашей задачи.
// плохо, зачем плодить новый класс если есть готовая реализация ниже
class Sha256 {
...
}
// хорошо
deg toSha256Hex(value: String) = MessageDigest.getInstance(SHA256).digest(value).toHex
Исключения
Правило бойскаута:
Оставь место стоянки чище, чем оно было до твоего прихода.
Из книги Роберта Мартина Чистый код:
Если мы все будем оставлять свой код чище, чем он был до нашего прихода, то код попросту не будет загнивать. Чистка не обязана быть глобальной. Присвойте более понятное имя переменной, разбейте слишком большую функцию, устраните одно незначительное повторение, упростите сложную цепочку условий. Представляете себе работу над проектом, код которого улучшается с течением времени? Но может ли профессионал позволить себе нечто иное? Разве постоянное совершенствование не является неотъемлемой частью профессионализма?
В соответствии с этими правилами, старайся следовать следущим рекомендациям: