kotlin小ネタ:coding conventions メモ
実際のコーディングで考察した内容を五月雨式にメモ。おいら自身が理解できればOKという最低限の記述。出そろってきたら整理しましょう。
kotlin とか coding conventions とか無関係なメモになってるっすね、、、(´・ω・`)
- 演算子の優先順位を等価とみなして括弧を利用する。*1
- 視覚的に理解しやすい。
- 意図が明確になる。(誤修正されにくくなる)
- 推移的な情報の使用を可能な限り避ける*2
- 関数型プログラミングを採用する。
- 各種要件とのトレードオフが必要な場合は検証を実施しその情報を残す。*3
- 使命が果たせない状況では実行時例外をスローする。*4
- クラスの命名はシンプルにする。
- 関数型のような多段の呼び出しは、保守者が各段の意味を理解しづらいと思える場合は Extract Variable や呼び出し毎にコメントを付けるなどの対応をする。
- fun/get()/set() では {} を使用する。
- backing field を持つ property は this を使わない限り(closure生成を含むもなど)すべて primary constructor の arguments にする。
- body 内部に記述した場合は property initializer もしくは initializer ブロックで初期化する必要がありクラス階層を縦断した参照を把握する必要があるが、this の参照も可能なため、fun/get()/set()内部からもの参照も含めた事実上全ソースを把握する必要がある。しかし、primary constructor の arguments にした場合は、当該クラスの constructor arguments の順番だけ把握すればよい。
- kotlin では全ての constructor arguments を評価し終わるまで(つまり instance の生成完了まで)は this の参照ができないため、複雑な過程を経て constructor arguments を作成するような場合の一時的な配置場所が取りづらい。一次変数を constructor argument のデフォルト値として入れ、それを用いて set していくような方法とか、ThreadLocal に値を入れるとか、あまりスマートでない方法をとらざるを得ない。しかし、基本的に参照透明性が得られない状況はほとんどないはずなので、よほど性能要件が厳しい状況でない限りは、毎引数毎に全部計算すればよいと思われ。
- 現在時刻の扱い
- リスコフの置換原則を厳守する。*8
- immutability にこだわる。*9
- パフォーマンスチューニング
- 変数名
- プロジェクトの設計書に命名規約を用意してそれに準じる。*15
- メソッド
- 順次コード
- 同一変数間の利用間隔及び生存期間を最小化する。
- 変数の生存範囲が重ならない、または入れ子になるようにし、乱雑に重ならないようにする。
- 選択コード
- body は {} 必ず入れる。保守性向上と、必ず入れると決めることにより{}に入れるべきかどうかを判断する必要が無くなるというメリットがある。
- 一般的(選択される可能性が高い)なケースを優先し、特殊なケースを後に回す。
- 可読性を低下させる深いネストは避ける。しかし、深いネストを避ける技術を用いて可読性が低下する場合は深いネストのままにしましょう。*17
- if に対して else の有無を検討し、else のない理由が自明と断言できる場合以外は空の else を作成し、内部に理由をコメントする。既に空の else が説明付きで書かれている場合は削除しない。*18
- 条件チェックの抜けが無いようにする。kotlin なら when や if を式として扱えば抜けはコンパイルエラーとしてチェックできる。その他の場合は else や default で抜けのハンドリングを行う。
- 反復コード(while, for, forEach 等)
- body は {} 必ず入れる。保守性向上と、必ず入れると決めることにより{}に入れるべきかどうかを判断する必要が無くなるというメリットがある。
- kotlin の collections 系ライブラリを使いこなす。for や while のほうが適切なケースはかなり稀。
- 単一の反復コードには単一のまとまった処理をさせる。*19
- 過剰な ExtractMethod を避ける。*20
- コード設計方針
- 最低でも3つ以上の方法を考えてみる。ブランチを切って疑似コードレベルで書いてみるとよい。
- どれがベストか迷った場合、それが十分にベターな案であれば、1つに決めてその経緯をコメントしておく。
- ベターな案すら出ない場合は多くの人を巻き込んで検討する。*21
*1:Extract Variable を用いたほうが保守しやすい場合はそちらを利用する。
*2:各種要件を超えない範囲で可能な限り。
*3:検証内容、実測値、考察など。
*4:使命が果たせない状況が起こり得る場合は設計不備を疑う。例えば、渡されたデータ内容の不備については null safety や validation 済みを保証するカプセル化で対応できることが多い。ユーザーの文字列入力など誤入力が織り込み済みの場合はその対応までが使命に含まれる。
*5:この名前空間の単位で module 化するのが理想。現在の開発環境では細かく module に分けるのはコストが大きい(gradleのmoduleとして細かく分けるのは現実的じゃない)ので特定のパッケージをモジュール的に扱うことも多いと思われる。
*6:表形式のほうがCSV形式より見やすいことが多いのと同じ。
*7:WebApplication の controller の場合は ThreadLocal に時刻情報が含まれる保証をしてしまえばよいし、Android の ui なら mainLooper のコールバックで値を設定するようにすればよい。その他の場合でもMVC系の設計の場合は controller で取得した時刻をバケツリレーすると決めてしまうとラクチン。
*8:特に意味論的に逸脱しないことが重要なので、そこら辺を保証する仕組みをプロジェクトに導入する必要がありますね。
*9:生成と初期化完了を同時に行い、以降は不変(参照をどれだけ辿っても不変)。
*10:変数を利用する場合、変数の生存期間内の全コードが coupling しているということになる。extract method のようなリファクタリングの阻害要因にもなるし、リファクタリングツールが syntactic にのみ実行されるような場合はバグ(変数の変更が含まれるルーチンをextractした場合など)になる。
*11:オブジェクトツリー全体の一か所でも変更可能なオブジェクトへの参照がある場合、そのオブジェクトの参照の生存期間内の全コードが coupling していることになる。
*12:forEach など推奨。forEachIndexed なども書き込みを伴わないのでOK。
*13:通常の var 利用の問題点は普通にある。その上、DBに保存されるデータや外部システムで管理されるデータをオンメモリ上で新規作成や変更して持っていると、それを正しいデータと勘違いして利用されることがあり得る。DBや外部システムからシステム内部に渡された段階でValueObjectとして扱うのが正解。新規作成や変更を行う場合は、各要素を個別の引数で渡す domain model の service interface を用いるか、setter を持つ専用の bean とそれを分解して service interface を利用する extension function の組み合わせなどを利用したりしましょう。
*14:性能向上の代わりに可読性等の品質を落とすもの
*15:ルール、ルールに関するデータ、ルール適用の具体例は必須。プロジェクト内で利用可能な変数名を集中管理して登録制にしてそれ以外の変数名はエラーになる自動チェック用の仕掛けをビルド及びIDEのリアルタイムチェックに入れられるといいかも。
*16:特定のインスタンスにのみ依存しているという理由だけでそのインスタンスにメソッドを追加してはいけない。例えば、class Point(val x: Int, val y: Int) というクラスがあるとして、sum = p.x + p.y のような処理が各所に散らばっているとする。この場合、p.sum() のようなメソッドを用意したほうが全体的にはコードがシンプルになる(オブジェクト指向厨の落とし穴ですね)。しかし、Point 自体の視点で x + y が意味を持たないのであれば、Point クラスが x + y を計算している全コードに意味的に依存(coupling)してしまうことになり、プログラム全体の複雑性が増す。もし Point 内部に x + y が内包されるのであれば、おそらく Point というクラス名は設計不足なので設計変更や命名変更が必要。Point の視点で x + y が意味を持たないのであれば、その意味が所属する場所(クラスではなく kt ファイル直下とかでもOK)に Point.sum() = x + y のような extension を定義したほうがよい。別の言い方をすると、x + y, x - y, x * y, x / y, 2 * (x + y) のようなメソッドが1000種類 Point 外部で使われるとしたら Point クラスにそれらを全部追加するのか?それらが変更されるたびに毎回 Point クラスを変更するのか?という話。
*17:プロジェクトメンバ全員が同じ判断を行うためには、具体例の提示や曖昧性の低い判断基準が必要ですね。
*18:少なくとも書いた人にとっては自明ではなかったのだから、自明だと思った人が else を削除するようなことはしてはいけない。
*19:例えば、ユーザーのリストから名前のリストと電話番号のリストを作成する場合、ユーザーのリストでループして内部に名前のリストと電話番号のリストを作成する処理を入れるというのはダメ。コードは短くなるが、ループ全体が2つの処理に依存してしまう。そうなると、例えばユーザー名と電話番号リストに個別にフィルタ条件や書式ロジックなどの追加や変更が入った場合、非常に複雑なコードになってしまう。途中で break する条件など入るとカオス。ExtractMethodなどもできない。そもそも本質的に名前リストと電話番号リストが欲しいのであれば、それらを並べるだけでいいはず。トップダウンで設計していれば自然とそうなるのだが、コードをいきなり書き始めてしまうとこういう罠にはまってしまう。トップダウンで疑似コードを書いてからソースを書き始めればこのようなことは起こらないので、ちゃんと疑似コードを書きましょう。個別の性能最適化は厳禁。
*20:特に複雑さを感じないならメソッドに分割する必要は無い。むしろ可読性が落ちる。ここら辺は普遍的な考え方と、プロジェクトに特化したルールの両方が必要ですね。後者が必要なのは、プロジェクトの品質管理基準によって変わるため。例えばユニットテストのカバレッジを重視するプロジェクトであれば、多くの処理の塊を private 関数として companion object に移動したほうが、ユニットテストのカバレッジを上げやすい。
*21:現状のメンバーは視点が狭くなっている可能性が高い。
kotlin小ネタ:code arrangement rule メモ
kotlin の code arrangement に関するおいらの独自ルールメモ。
IntelliJ IDEA の kotlin の rearrange code 機能については中の人が 2016-11-21付で以下のような回答をしてるので、実装は気長に待つのが良さそうですね。
以下おいら独自ルール(とりあえずのやっつけザックリ版)
配置順は優先順位が同一の場合は基本的にスコープの広いもの順とアルファベット順。
interface の場合の配置順
- val(alphabetical order)
- var(alphabetical order)
- fun(alphabetical order)
- nested classes
- inner classes
- companion class
class の場合の配置順
- Arguments of primary constructor.(直観的にわかりやすい順番 *1 )
- Properties which have backing fields.*2
- init block*3
- Members(backing field への assignment のあるもの *4 )
- Members(backing field への assignment のないもの *5 )
- nested classes*6
- companion class
- object StaticFunctions*7
考察
- functions と properties を混在させているのは、kotlin では property に手続きを記述することができ、これらと function を置き換えたくなることが多いため。下記のようなことが起こるたびにメンバの順番を入れ替えなきゃいけないというのはめんどい。
// 元のソース fun getName(sessionId: Int): String // sessionId が文脈から得られるようになったら以下のように変更可能 fun getName(): String // それなら以下のようにしたい val name: String
まだあまり深く考えていないので、既存の coding conventions を参考に練っていきましょうかね。
*1:直観的にわかりやすい順番というのはザックリ過ぎるのでもう少し厳格な基準を設けたほうがいい気がする。しかし alphabetical order は現実的じゃない(オプション的な boolean が先頭に来るとか varargs/default value との兼ね合いとか)。
*2:アルファベット順を強制するため、定義のみで値の初期化は行わない。var の override 時など定義時に初期化が必須の場合は null をセットしておく。
*3:backing field を持つ properties の初期化はここで行う
*4:assignment と backing field に格納されているオブジェクトの状態を変えることを混同しないように注意。
*5:StaticFunction の関数を1つ呼び出すだけ。
*6:まだ深く考えてない。
*7:参照透明性のある関数群
kotlin小ネタ:forEach から break っぽいことを行う
このようなコードを無理やり forEach でやろうと思った場合、
for (v in 0..1) { println(v) break }
こんな感じになる。
// forEach からの break っぽいことを行う例 fun main(args: Array<String>) { fun foo() { // label は expression に設定する必要があるので、forEach の外部に便宜的な // place holder 用の lambda expression を用意する。 run exit@ { (0..1).forEach { // 下記で forEach から抜けなければ "0", "1" と表示される。 // 実際には抜けてしまうので "0" のみが表示される。 println(it) // 外側の forEach を抜ける return@exit } } } foo() // 出力は "0" のみ }
label は expression に対して設定するので以下のようにもできる。
fun main(args: Array<String>) { val expression = exit@ { (0..1).forEach { // 下記で forEach から抜けなければ "0", "1" と表示される。 // 実際には抜けてしまうので "0" のみが表示される。 println(it) // 外側の forEach を抜ける return@exit } } fun foo() { expression() } foo() // 出力は "0" のみ }
kotlin小ネタ:elvis operator から return する際ににまとまった処理を書きたい場合
// elvis operator, return, closure の実行値, という組み合わせ。 // この例では return で f2() の値が返る。 val value = next() ?: return run { f1(); f2() } // これでも大丈夫。 val value = next() ?: return { f1(); f2() }()
kotlin小ネタ:expression の値が null の場合のみ expression の値を変えずに処理を行う例
// オリジナル val currentValue = nextValue() if (currentValue == null) finish() // 等価コード1 val currentValue = nextValue().apply { if (this == null) finish() } // 等価コード2 val currentValue = nextValue() ?: { finish(); null }()
elvis operator を使う場合に expression が null を返すのが面倒なら
こういうのを作っちゃっう方法もありかも。
/** * Calls the specified function [block] with `this` value as its receiver only if `this` is null and returns `this` value. */ inline fun <T> T.applyIfNull(block: T.() -> Unit): T { if (this == null) block(); return this }
// 等価コード3 val currentValue = nextValue().applyIfNull { finish() }
kotlin小ネタ:ある値が null の場合は null を、そうでない場合にはその値を用いて初期化された値を取得するサンプル
class Sample { private var mStringBuilder: StringBuilder? = null // letters が null の場合は null を、そうでない場合は letters.length で初期化された StringBuilder を生成する fun init(letters: String?) { mStringBuilder = letters?.let { StringBuilder(it.length) } } }
jsvc経由でTomcatを起動する際のタイムアウト設定
tomcat を jsvc 経由で起動する際に、コンテナ起動時にエラーが発生していなくても exit status が 1 になる現象に遭遇したのでメモ。
原因
tomcat の起動(全warの起動含む)が、jsvc 内部で SERVICE_START_WAIT_TIME 変数として指定された時間内に完了していなかったため、結果としてコンテナは正常に起動しても exit status 1 を返していた。