Scala学習テキスト の学習が完了していることを前提としています。
- スタイルガイドの構造(章立て)は Google Java Style Guide に従う
- 各ルールにはレベル(MUST、SHOULD、MAY)をつける
- MUST: 必須
- SHOULD: 推奨
- MAY: 任意
- 自動で調整・チェックされない、注意が必要な項目には ★ を記載
- WartRemover で自動チェックされるルールは Warts:
org.wartremover.warts.Equals
のように表記
- 良い例・良くない例が明確になるようにする
- 「なぜこの書き方は良くないのか?」といった指針の存在意義が理解できるようにし、知識の応用や指針の改善をしやすくする
ファイル名とクラス名が一致していると、GitLab などでファイル名による検索がしやすくなります。
※ EditorConfig により自動的に設定されます。
ファイル名による検索がしやすくなるのに加え、package 変更のリファクタリングがより簡単に行えるようになります。
例外
- クラスの可視性が
private
の場合 private
なクラスをテストコードから参照する目的で package private とした場合sealed trait
を実装する場合(sealed trait
は 同一ファイル内でのみ継承可能なため)
Scalafmt と EditorConfig を使って自動的にフォーマットします
※ EditorConfig により自動的に付与されます。
ファイル末尾の改行が無いとファイル末尾に新たなコードを追加する場合、追加したコードの直前の行にも(改行コードを追加されたことにより)差分が出てしまい、レビューする際のノイズになるからです。
※ Scalafmt により自動的に付与されます。
リストに新たな要素を追加する場合、追加したコードの直前の行にも差分が出てしまい、レビューする際のノイズになるからです。
// NG!
Seq(
1,
2,
3
)
// OK!
Seq(
1,
2,
3,
)
Warts: lerna.warts.NamingClass
, lerna.warts.NamingObject
// OK
class MyClass
// OK
trait MyTrait
// OK
object MyObject
// OK
type StringList = List[String]
Warts: lerna.warts.NamingPackage
たとえ、複数の単語を組み合わせた名前であってもです!
// OK
package co.tis.jp.lerna
// OK
package object lerna {}
// OK
package co.tis.jp.lerna.transactionmanager
// NG!
package co.tis.jp.lerna.transaction-manager
Warts: lerna.warts.NamingDef
, lerna.warts.NamingVal
// OK
def myFairMethod = ???
// OK
val myFairField = ???
// OK
var myFairVariable = ???
例外
Enumeration
を使う場合は大文字で始まるキャメルケースで宣言
object ExampleCode extends Enumeration {
type ExampleCode = Value
// OK
val One: ExampleCode.Value = Value("01")
val Two: ExampleCode.Value = Value("02")
}
外界の影響を受けない、副作用のないメソッドであることを知らせるために ()
を省略し、あたかもプロパティであるかのように見えるようにするとコードを読むメンバーの認識負荷を下げられます。
逆に println()
のようなメソッドは外界に影響を与えるため副作用があるとみなされ、()
を付けるべきです。
// NG!
def size(): Int
// OK
def size: Int
// OK
class List[A] {
def map[B](f: A => B): List[B] = ???
}
適切なコメントはクラスやメソッドを使う他の開発者のヒントになります。
- 使い方
- 設計の意図
などを書き留めておくようにしましょう。
Warts: org.wartremover.contrib.warts.MissingOverride
ミックスインしたトレイトや、継承した親クラスのメソッドがリファクタリングによって名前変更や削除が行われた際、実装クラス側でその変更への追従が漏れていた場合、override
を付けておくと実装クラス側でコンパイルエラーになるため、問題に気づけるようになります。
trait Example {
def doSomething
}
class ExampleImpl {
// NG!
def doSomething = ???
}
class ExampleImpl {
// OK
override def doSomething = ???
}
Warts: org.wartremover.warts.Equals
==
や equals
は異なる型のオブジェクトを比較していてもコンパイルエラーにはなりません。
リファクタリングなどで比較対象の片方の型が変わった際、常に false
を返すようになるため不具合の原因になります。
コンパイラーが不具合を検出できるように ===
を使います。
// `===` を使うために必要
import jp.co.tis.lerna.payment.utility.lang.All._
// もしくは import lerna.util.lang.Equals._
"1" === 2 // コンパイルエラー
"1" === "2" // コンパイルできる
Enumeration はパターンマッチの網羅性チェックが行われず不具合に気づきにくいため、sealed trait
と case object
を用いて列挙型を定義しましょう。
sealed trait WeekDay
case object Mon extends WeekDay
case object Tue extends WeekDay
case object Wed extends WeekDay
case object Thu extends WeekDay
case object Fri extends WeekDay
case object Sat extends WeekDay
case object San extends WeekDay
例外
列挙型からコード値、コード値から列挙型の変換を両方行う必要がある場合は Enumeration のほうが簡潔に実装できるため利用を検討する。
object WeekDay extends Enumeration {
type WeekDay = Value
val Mon = Value(1)
val Tue = Value(2)
val Wed = Value(3)
val Thu = Value(4)
val Fri = Value(5)
val Sat = Value(6)
val San = Value(7)
}
WeekDay.Mon.id // => 1
WeekDay(1) // => Mon
Enumeration で各要素にメソッドやフィールドを持たせる方法は下記の記事を参照。
メソッドの処理が引数に特定の条件を要求する場合は、require
を使って事前条件を表現しましょう。
こうすることでこのメソッドが要求する条件がわかりやすくなるだけでなく、実行時にいち早くエラーに気づくことができるようになります。
// `require` を使うために必要
import jp.co.tis.lerna.utility.lang.All._
// もしくは import jp.co.tis.lerna.utility.lang.Assertions._
def doSomething(times: Int) = {
require(times > 0) // times に 0 よりも大きい数字を指定しなければならないことを表現
}
このようなプログラミングスタイルは一般的に契約プログラミングと呼ばれています。
契約プログラミング(けいやくプログラミング、Programming By Contract)または契約による設計(けいやくによるせっけい、Design By Contract)とは、プログラムコードの中にプログラムが満たすべき仕様についての記述を盛り込む事で設計の安全性を高める技法
Warts: lerna.warts.CyclomaticComplexity
条件分岐やループ処理の数によって複雑度を測る指標です。 循環的複雑度を 10 以下にするとバグ発生確率を 30% 以下抑えられると言われています。
処理を他のメソッドに切り出すなどして、各メソッドの複雑度を下げましょう。
Warts: lerna.warts.Awaits
Future
を用いた並列処理では大抵の場合、スレッド数に上限のあるスレッドプールを利用して処理が実行されます。Await.result
や Await.ready
は結果を待つ Future
の処理が完了するまでの間、スレッドを専有します。その結果、CPU に余裕があるにも関わらずスレッドプールが枯渇し、アプリケーション全体のパフォーマンスが劣化してしまう可能性があります。
Future
の map
や flatMap
、foreach
といったメソッドを用いたり、for
式を用いることで処理順序の制御を行うようにしましょう。
// NG!
val result = Await.result(future)
val nextValue = result + 1
// OK
val nextValue =
future.map { result =>
result + 1
}
// OK
val nextValue =
for {
result <- future
} yield result + 1
Future
を使った処理を実装していると、Scala コンパイラーに scala.concurrent.ExecutionContext.Implicits.global
をインポートするように指示されることがありますが(下記参照)、Akka を使っている場合は、Akka の executionContext を使うことが推奨されます。
error: Cannot find an implicit ExecutionContext. You might pass an (implicit ec: ExecutionContext) parameter to your method or import scala.concurrent.ExecutionContext.Implicits.global.
scala.concurrent.ExecutionContext.Implicits.global
はアプリケーション全体で使われる可能性があるため、他の部分が原因の(スレッドプールの枯渇といった)不具合の影響を受けやすくなってしまいます。Akka の executionContext を使うと、処理ごとにスレッドプールを分離したりといったことが設定ファイルの修正だけで実現できるので、そのような不具合の影響を受けにくくすることが簡単にできます。また、パフォーマンスチューニングも容易になります。
// NG!
import scala.concurrent.ExecutionContext.Implicits.global
// OK
// Actor の外側では ActorSystem から executionContext を取り出す (system は ActorSystem[_])
import system.executionContext
// OK
// Actor の内部では context.executionContext を使う
import context.executionContext
Future
同士を合成していないと例外が握りつぶされ、エラーハンドリングできなくなってしまいます。
もし Future
内の処理でエラーが発生しても、最悪の場合スタックトレースすら出力されないため、エラーを検知することが困難になります。
for
式や Future
のメソッドを使って Future
同士を合成するようにしましょう。
def doSomething(): Future[Int] = ???
// NG! - Future が合成されていない
try {
doSomethingA()
doSomethingB() // ⇐ 処理中に例外が発生しても握りつぶされる
sayHello()
} catch {
// Future の処理中に発生した例外は catch されないため、ほぼ無意味なコード
case NonFatal(cause) =>
log.error(cause)
}
// OK - Future が合成されている
val futureCountA: Future[Int] = doSomethingA()
val futureCountB: Future[Int] = doSomethingB()
val futureSum: Future[Int] =
for {
countA <- futureCountA
countB <- futureCountB
} yield countA + countB
futureSum.onComplete {
case Success(_) =>
sayHello()
case Failure(cause) =>
// 合成しておくことで Failure として例外をハンドリングできる
log.error(cause)
}
ShardRegion
宛にメッセージを送った場合、ネットワーク障害などでメッセージが届かないことがあります。送ったメッセージの応答を待つような実装をした場合は応答が返ってこないことを考慮して、タイムアウトを実装し、永遠に待ち続けることによる不具合を回避しましょう。
永遠に待ち続けた場合、メモリリークや処理が進まないことによるユーザビリティの低下など様々な障害の原因になります。
アクターが何かしらの処理結果を通知するメッセージを作成する場合は、成功・失敗を表現するメッセージを個別に作成し、共通の sealed trait
をミックスインすることを推奨します。こうすることで後記するシリアライズの問題を回避しつつ、メッセージのハンドリング漏れを(sealed trait
により)コンパイラがチェックできるようになり、より安全な実装が行えます。
一般的に失敗を表現するためのオブジェクトとして Exception
(を継承したクラス)が用いられますが、アクターのメッセージで Exception
を使うと、メッセージのシリアライズで問題が発生します。
デフォルトでは Exception
(Throwable
) のシリアライズには Java Serializer が使われます。
しかし、セキュリティや互換性の観点から Akka では Java Serializer の使用は推奨されません。
独自のシリアライザーを実装することで Exception
に Java Serializer が使われることを回避できますが、保守コストが高くなったり、実装漏れによって実行時例外が発生するリスクもあるため推奨しません。
Behaviors.receiveMessage[Command] {
case Ping(replyTo) =>
replyTo ! ProcessFailed(code = 1) // OK
// replyTo ! BadResponse(new RuntimeException()) // NG!
Behaviors.same
}
sealed trait Command
case class Ping(replyTo: ActorRef[Response]) extends Command
sealed trait Response
case class ProcessSucceeded() extends Response
case class ProcessFailed(code: Int) extends Response
// NG!
// case class BadResponse(cause: Throwable) extends Response
参照
Akka Cluster Sharding の種別を特定する typeName
にはそれぞれユニークなものを設定する必要があります。ShardRegion
の typeName
が重複した場合、メッセージが予期しないアクターに届くことがあります。大抵、この問題が発生した場合はログで unhandled message が報告されます。
ClusterSharding
を複数起動する場合は typeName
が重複しないよう一覧できるクラスを定義しておくことを推奨します。
object ClusterShardingTypeNames {
// typeName はここに列挙していく
// typeName の文字列と変数名を合わせるとコンパイラが重複チェックしてくれる
val fizz = "fizz"
val buzz = "buzz"
}
val FizzTypeKey = EntityTypeKey[Fizz.Command](ClusterShardingTypeNames.fizz)
val fizzRegion: ActorRef[ShardingEnvelope[Fizz.Command]] =
ClusterSharding(system).init(Entity(FizzTypeKey)(createBehavior = entityContext => Fizz(entityContext.entityId)))
Blocking I/O とは、外部システムへの API 呼び出しやファイルの書き込みの応答を待つときにスレッドがブロックされる入出力操作のことです。
API 呼び出しなどの処理結果が Future
で受け取れない場合は Blocking I/O になっていると考えられます。
Akka では Blocking I/O は推奨されません。 Blocking I/O はその特性上、多くのスレッドを消費します。 スレッド数が増え過ぎると、CPU のコンテキストスイッチが多く発生し CPU 使用率が低いにも関わらず性能が出ないという問題が生じます(C10K問題)。 この問題を回避するため、Akka は応答待ちでスレッドをブロックしない Non-Blocking I/O を前提とし、 少数のスレッドを使いまわすことでコンテキストスイッチを最小限に抑え、効率よく処理するというポリシーのもと構成されています。
Akka がデフォルトで処理に使うスレッドプールはこのポリシーによりスレッド数の上限が低く設定されています。 万一 Blocking I/O によりスレッドが全てブロックされた状態になると CPU には余裕があるにも関わらず他の処理が詰まったり全く実行できない状態に陥ります。
このような問題を回避するため、Blocking I/O をする処理はデフォルトのスレッドプールとは隔離された Blocking I/O 専用のスレッドプールを使うよう実装すべきです。 Non-Blocking I/O で処理する手段がある場合は、まずはその手段の採用を検討してください。
Blocking I/O 専用のスレッドプールを作成するには、Akka の機能が利用できます。
Solution: Dedicated dispatcher for blocking operations ― Dispatchers • Akka Documentation
上記のドキュメントを参考に、Blocking I/O 用のスレッドプール(ディスパッチャ)を作成し、次のコード例ように Future
でラップすると、Blocking I/O の処理を専用のスレッドプールで処理させることができ、デフォルトのスレッドプールから隔離できます。
private val system: ActorSystem[_] = ???
/**
* Blocking I/O 専用のスレッドプール
*/
private val blockingDispatcher: ExecutionContext =
system.dispatchers.lookup(DispatcherSelector.fromConfig("my-blocking-dispatcher"))
/**
* Blocking I/O の処理を Future でラップし、Blocking I/O 専用のスレッドプールで処理させる
*/
def execute(): Future[Result] = Future {
// Blocking I/O の処理を実行
val result = executeBlockingOperation()
Result(result)
}(blockingDispatcher) // Blocking I/O 専用のスレッドプールを明示的に指定
システム日付は lerna.util.time.LocalDateTimeFactory を使って日付を取得してください。
import jp.co.tis.lerna.utility.time.LocalDateTimeFactory
class MyComponent(dateTimeFactory: LocalDateTimeFactory) {
dateTimeFactory.now() // 処理時点のシステム日付を取得
}
ユニットテストで簡単に日付を固定値に変更できます。
テスト例: FixedLocalDateTimeFactorySpec.scala
設定ミスはアプリケーションの起動時に検知できる状態が望ましいです。 アプリケーションを起動するだけでほとんどの設定ミスを検知できる状態だと、設定ミスにより業務処理実行時にランタイムエラーが発生するリスクが少ないからです。 設定値をコンストラクタで読み込むように実装することで、アプリケーションの起動時に行われる DI コンテナの初期化処理でエラーを検知できます。
import scala.jdk.DurationConverters._
class MyComponent(config: Config) {
// MyComponent のインスタンスはアプリケーション起動時に作成される
// 設定値の不備などで設定値の読み込みに失敗した場合、MyComponent の初期化に失敗するためアプリケーションの起動自体が失敗する
val requestTimeout: FiniteDuration = config.getDuration("com.example.app.my-component.request-timeout").toScala
}
設定項目が複数ある場合は、設定値をまとめるクラスを作成すると便利です。
ここでもクラス内での設定値の読み込みは val
で行い、初期化処理でエラーを検知できるようにしておきます。
import scala.jdk.DurationConverters._
class MyComponentConfig(config: Config) {
private val componentConfig: Config = config.getConfig("com.example.app.my-component")
val requestTimeout: FiniteDuration = componentConfig.getDuration("request-timeout").toScala
val attempt: Int = componentConfig.getInt("attempt")
}
作成した Config 読み込み用のクラスはコンストラクタから受け取るようにすると DI により自動インジェクションされます。
class MyComponent(config: MyComponentConfig) {
val requestTimeout = config.requestTimeout
}
シンプルなアサーションの記述でエラーが下記のように見やすくなるので StandardSpec を使ってテストを実装することを推奨します。
java.lang.AssertionError:
expect("2" === "1")
| |
2 false
実装例: StandardSpecExample.scala
IntelliJ IDEA はタイプミスやあまり良くない書き方を警告してくれます。
IntelliJ IDEA のテーマを Material Theme UI の Dark 系にすることで警告が目立つようになります。
インストール
- Settings > Plugins > Browse Repositories > 「Material Theme UI」
配色設定
- Settings > Editor > Color Scheme > Scheme
Material Oceanic
など Dark 系の配色を設定。
※ 背景が明るい Material Lighter
などは警告に気づきづらい