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
  • リスコフの置換原則を厳守する。*8
  • immutability にこだわる。*9
    • var は極力利用しない。*10
    • インスタンスの参照ツリー全体が各々のクラスレベルでイミュータブルになるようにする。もしくはリフレクションを使わないで得られるinterfaceレベルでイミュータブルになるようにする。*11
    • 変数への書き込みを伴うループカウンタなども極力避ける。*12
    • DBに保存されるデータや外部システムで管理されるデータは特に気を付ける。*13
  • パフォーマンスチューニング
    • 性能要件を満たす限りはパフォーマンスは一切無視する。
    • 性能要件を満たせない場合は、性能を意識した仕組み*14の導入は、実装前にアーキテクチャ設計に盛り込むか、全機能実装完了後に実測ベースで行う。
  • 変数名
    • プロジェクトの設計書に命名規約を用意してそれに準じる。*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:現状のメンバーは視点が狭くなっている可能性が高い。