読者です 読者をやめる 読者になる 読者になる

kotlin小ネタ:coding conventions メモ

実際のコーディングで考察した内容を五月雨式にメモ。おいら自身が理解できればOKという最低限の記述。出そろってきたら整理しましょう。
kotlin とか coding conventions とか無関係なメモになってるっすね、、、(´・ω・`)

  • 演算子の優先順位を等価とみなして括弧を利用する。*1
    • 視覚的に理解しやすい。
    • 意図が明確になる。(誤修正されにくくなる)
  • 推移的な情報の使用を可能な限り避ける*2
  • 使命が果たせない状況では実行時例外をスローする。*4
  • クラスの命名はシンプルにする。
    • クラスの命名は、最も小さいかつ整合性のとれた名前空間 *5 に合わせてシンプルなものにする。
    • クラスの利用時にクラスの simpleName の衝突や分かりづらさがある場合は、import で as を使い別名表記する。
  • 関数型のような多段の呼び出しは、保守者が各段の意味を理解しづらいと思える場合は Extract Variable や呼び出し毎にコメントを付けるなどの対応をする。
  • fun/get()/set() では {} を使用する。
    • IDE 上で collapse するとシンプルに表示できる。
    • 型の記述がコンパイラレベル強制される。
    • "=" と "{}" の混在する不揃い感が美しくない。主観の問題だけでなく不揃いなものを認識する作業は脳への負荷が大きい。*6
    • "=" と "{}" のどちらを利用するべきか考える必要性がなくなり、時間の節約とリファクタリングのピンポン防止になる。
  • 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 に値を入れるとか、あまりスマートでない方法をとらざるを得ない。しかし、基本的に参照透明性が得られない状況はほとんどないはずなので、よほど性能要件が厳しい状況でない限りは、毎引数毎に全部計算すればよいと思われ。
  • 現在時刻の扱い
    • 扱う方針はアーキテクチャレベルで設定される。
    • 基本的には controller 的な役割の中で決定する。*7

*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 で取得した時刻をバケツリレーすると決めてしまうとラクチン。

kotlin小ネタ:code arrangement rule メモ

kotlin の code arrangement に関するおいらの独自ルールメモ。

IntelliJ IDEA の kotlin の rearrange code 機能については中の人が 2016-11-21付で以下のような回答をしてるので、実装は気長に待つのが良さそうですね。

stackoverflow.com

以下おいら独自ルール(とりあえずのやっつけザックリ版)

配置順は優先順位が同一の場合は基本的にスコープの広いもの順とアルファベット順。

interface の場合の配置順

  1. val(alphabetical order)
  2. var(alphabetical order)
  3. fun(alphabetical order)
  4. nested classes
  5. inner classes
  6. companion class

class の場合の配置順

  1. Arguments of primary constructor.(直観的にわかりやすい順番 *1 )
  2. Properties which have backing fields.*2
  3. init block*3
  4. Members(backing field への assignment のあるもの *4 )
  5. Members(backing field への assignment のないもの *5 )
  6. nested classes*6
  7. companion class
  8. 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 を返していた。

対策1

  • daemon.sh を /etc/init.d/tomcat にコピーしてそれを直接叩く場合は、下記のようにタイムアウト時間(秒)を指定することにより、タイムアウト前に起動が完了すれば exit status として 0 を返すようになる。
/etc/init.d/tomcat --service-start-wait-time 25 start 

対策2

  • service を利用する場合は、bin/setenv.sh に以下のようにタイムアウト時間を設定すればOK。
SERVICE_START_WAIT_TIME=180