やる気とかいろいろ

五月雨式なメモ。

※『やる気』と『動機』は違う。以降、『やる気』という言葉は気分的な意味合いで使用する。

  • やる気という考え方がそもそもおかしい。『やる気が無いのでやらなかった』と『実力不足でできなかった』は同じこと。『やる気さえあればできる』というのはボクサーが『ラッキーパンチさえ当たれば勝てる』と言っているのと同じ。
  • やる気は無いのが前提。← (追記)決定性なので間違い
  • やる気は偶然の幸運。← (追記)決定性なので間違い
  • やる気が必要なことは計画に組み込まない。
  • 突然現れる直感は大体信じてOK。*1
  • 迷いは強引な即決か思考停止で対応。*2
  • 集中力の切れた状態やら頭の飽和状態を現実逃避と一喝するようなストイックな考えは逆効果。とりあえず休めwww*3
  • やる気のメカニズム(おいら調べ)
    • やる気とは、『やる』か『やらないか』という葛藤の『やる』という選択肢の状態。
    • 葛藤状態に無いものは『やる気』という概念では扱えない。*4*5
    • やる気は決定性の変数*6
  • やる気の強さに意味はなく、やるかやらないかに意味がある。
  • 判断自体を減らすことが重要。
    • やる気という概念はやるやらないという判断に迷った際に意味を持つ。逆を言えば、判断しないでよい状況を作り出してしまえばやる気というものを考える必要がなくなる。*7
  • 予定表や計画書
    • 予定表や計画書は『備忘録』であって『目標』ではない。不測の事態が起きても達成可能な余裕を持たなくてはならない。
    • 予定には完了日次と成果物(成果としての状況)を記述する。Howについては言及しない。
    • 不足な事態が起こらなくても『やる気がない』からやらないというような可能性があるものは計画に含めてはいけない。*8
    • 予定を守れなかった場合は『負け癖』がついてしまう。これは強力な呪いで解除の方法が存在しない。*9

*1:直感は純粋に最適解であることが多く、直感の通りに活動することは楽じゃないことが多い。そのため、直感を元にその先を思考するのはOKだが、直感を一案として総合的に思考するのと直感を否定する言い訳探しになり、導かれる結果は怠惰でナイーブな願望となる

*2:人生は有限なので、時間をかけて正しい選択をするよりも短時間で数多くの選択を重ねるべき。また、迷いに対する思考は往々にして自己正当化のための精神活動であり、結論は怠惰でナイーブな願望となる。

*3:やる気は決定性の変数なので、特に大きな出来事があるわけでもなくやる気が落ちてきた場合は、その原因が必ず存在する。多くの場合は飽きや疲れからなので、戻る時に葛藤が生じない形で休憩を取るのが正解。何もない部屋で横になるのがおすすめで、ネットサーフィンとかメールチェックなどは戻る時に葛藤が生じて結果として戻れなくなる可能性が高いので注意。ストイックなやり方は、絶対に失敗しない保証がある場合は有効で、今後のやる気の底上げにもなる。ただし、失敗の悪影響が甚大なので注意。

*4:例1:毎朝食事することが当たり前となって10年たった人は、いつも通りの翌朝には朝食をとる。この朝食をとるという行動は葛藤なく自然に行われているため『やる気』という概念は適用できない。

*5:例2:毎朝食事しないことが当たり前となって10年たった人は、いつも通りの翌朝には朝食をとらない。この朝食をとるという行動は葛藤なく自然に行われているため『やる気』という概念は適用できない。

*6:やる気は人格(本能的な快と不快の基準と過去の記憶)と状況によって決定されるため、曖昧なものではなく、決定性である。もちろん、それらを測定できるわけではないのだが、予測可能であるということが重要。たとえば、特殊な理由もなくなんとなくやる気が出ないものは、明日になっても明後日になっても同程度のやる気である可能性が高いという予測が可能。

*7:服の整理がめんどくさい人はノームコアにしてしまえばいいし、食事の買い出しや準備が面倒ならメニューを固定してしまえばいい。このように判断する箇所を減らすことによりやる気という概念自体を無効化する方向に持って行くことが可能。

*8:たとえば小学生の夏休みの予定表は、一度でも守れなかったらリスケが必要。『明日はきっと』はまず無理。なぜなら明日も今日と同じやる気の可能性が最も高く、結果としてまた守れない可能性が最も高くなる。

*9:負けケースを上回る勝ちケースで表向きの相殺を計ることはできるが、負けない気概は 負けた回数/勝った回数 という単純な式のようなもので、一定以上の年齢となると負けた回数を買った回数で取り戻すことが不可能になる。

kotlin小ネタ:Activity 関連の試行錯誤

Activity を扱う際の実験コード。

とりあえず思い付きで作ってみた。
ユニットテスト重視版。
たぶんおいらのプロジェクトではこんなことやらんだろうけどw

/**
 * Activity を扱う際のいろんなネタを詰め込んでみた実験コード。
 * ※書式だけまとめたもので、実際に動くコードではない。
 *
 * ポイントは以下。
 * ・Intent を利用せずに Activity を起動するチート。
 * ・XxxActivityHelper を分離することにより Activity の制約とは無縁のインスタンスで Activity の処理を記述できる。
 * ・コンストラクタ引数の評価時(thisが生成される前)に可能な限り初期化を行うことにより初期化前のプロパティを参照
 *  してしまうバグの入れ込みを不可能にする。※コンパイラレベルで排除される
 * ・テスト用にコンストラクタの動的な差し替えが可能。※newInstance を var で定義している。
 * ・ほぼすべてのメソッドが単体でユニットテスト可能。this も差し替えられるように extension を使用している。
 * ・メソッド階層をすべて辿ったうえで var への代入が行われるコードかどうかをコンパイラレベルでチェック可能。
 */
package com.example

import android.app.Activity
import android.content.Intent
import android.os.Bundle
import android.support.v7.app.AppCompatActivity
import com.example.XxxActivityHelperImplDelegate.getPcaSomethingCreatedByConstructor
import com.example.XxxActivityHelperImplDelegate.onBackPressed_
import com.example.XxxActivityHelperImplDelegate.onBeforeConstructor
import com.example.XxxActivityHelperImplDelegate.onPause_
import org.jetbrains.anko.AnkoLogger
import org.jetbrains.anko.startActivity
import com.example.XxxActivityHelperImpl as Assignable
import com.example.XxxActivityHelperImpl as Impl

/**
 * [Activity] 本体
 *
 * 以下の理由で [onCreate] 以外の処理をすべて [XxxActivityHelper] に委譲している。
 * ・ヘルパーを利用することにより [onCreate] 以降に生成された値を  [XxxActivityHelper] の val として扱える。
 * ・[Activity] の仕様に特化した処理とそれ以外を分離できる。※[onPause] で super.onPause() を呼ぶ処理など。
 */
class XxxActivity : AppCompatActivity(), AnkoLogger {
    // backing field を持つ唯一のプロパティ
    // これ以外はすべて Helper 側に含まれる
    private lateinit var helper: XxxActivityHelper
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        // Intent を利用せずに Activity を起動するチート用の処理。
        // XxxHelper に委譲する前に必要な処理なのでここに記述している。
        val activityArg1 = activityArg1
        val activityArg2 = activityArg2
        if (activityArg1 != null && activityArg2 != null) {
            helper = XxxActivityHelper.newInstance(this@XxxActivity, activityArg1, activityArg2)
            Companion.activityArg1 = null
            Companion.activityArg2 = null
        }
        else {
            finish()
        }
    }
    
    override fun onBackPressed() {
        helper.onBackPressed()
    }
    
    override fun onPause() {
        super.onPause()
        helper.onPause()
    }
    
    companion object {
        /**
         * [Intent] を利用せずに [Activity] を起動するチート用の情報
         * [startActivity] 経由で起動される保証はないので lateinit にはできない。※端末の履歴機能からの Activity 起動など。
         */
        private var activityArg1: Something1? = null
        private var activityArg2: Something2? = null
        
        /**
         * [Intent] を利用せずに [Activity] を起動するチート
         */
        fun startActivity(activity: XxxActivity, activityArg1: Something1, activityArg2: Something2) {
            Companion.activityArg1 = activityArg1
            Companion.activityArg2 = activityArg2
            activity.startActivity<XxxActivity>()
        }
    }
}

/**
 * [XxxActivity] から処理を移譲されるヘルパークラス
 */
interface XxxActivityHelper {
    fun onBackPressed()
    
    fun onPause()
    
    companion object {
        // テスト時には必要に応じて newInstance を差し替える
        var newInstance = ::Impl
    }
}

class XxxActivityHelperImpl private constructor(
        override val activity: XxxActivity,
        override val activityArg: Something1,
        override val somethingCreatedByConstructor: Something3
) : XxxActivityHelper, XxxActivityHelperImplDelegate.NonAssignable {
    // Secondary constructors to create primary constructor arguments --------------------------------------------------
    // this を参照しなくても生成可能な情報はすべてコンストラクタ引数として扱う。
    // これにより、property initializer もしくは initializer block にて非 null に初期化のプロパティが null にならない
    // という勘違いによるバグを防ぐ。※初期化前に参照できるので
    @Suppress("unused") // for inspector's bug.
    constructor(activity: XxxActivity, activityArg1: Something1, activityArg2: Something2) : this(
            // どうしても評価前に行わなきゃならん処理がある場合の苦肉の策。
            onBeforeConstructor(activity, activityArg1, activityArg2).run {
                activity
            },
            activityArg1,
            getPcaSomethingCreatedByConstructor(activityArg2)) {
        // ここには可能な限りコードを書かない。
    }
    
    // Properties with backing fields that refer to this@Impl ----------------------------------------------------------
    override var isBackPressed = false
    
    // Initializer block -----------------------------------------------------------------------------------------------
    init {
        // ここには可能な限りコードを書かない。
    }
    
    // Delegate functions ----------------------------------------------------------------------------------------------
    
    override fun onBackPressed() {
        onBackPressed_()
    }
    
    override fun onPause() {
        onPause_()
    }
}

object XxxActivityHelperImplDelegate : AnkoLogger {
    // Constants -------------------------------------------------------------------------------------------------------
    
    // onBeforeConstructor ---------------------------------------------------------------------------------------------
    fun onBeforeConstructor(activity: XxxActivity, activityArg1: Something1, activityArg2: Something2) {
    }
    
    // Primary constructor argument initializers -----------------------------------------------------------------------
    fun getPcaSomethingCreatedByConstructor(activityArg: Something2): Something3 {
        return Something3()
    }
    
    // Property initializers for properties that internally refer to this@Impl -----------------------------------------
    
    // Initializer block delegation ------------------------------------------------------------------------------------
    
    // Direct delegates ------------------------------------------------------------------------------------------------
    
    fun Assignable.onBackPressed_() {
        isBackPressed = true
    }
    
    fun NonAssignable.onPause_() {
        with(activity) {
            // プロパティへの代入の無い処理をここで行う
        }
    }
    
    // Members that assign values to backing fields --------------------------------------------------------------------
    
    // Members that have side effects ----------------------------------------------------------------------------------
    
    // Non-assignable interface of implementation class ----------------------------------------------------------------
    // NonAssignable が receiver のメソッドはプロパティへの代入が無いことがコンパイラにより保証される。保守時に便利。
    // NonAssignable であってもイミュータブルである保証はない点に注意。
    interface NonAssignable : AnkoLogger {
        val activity: XxxActivity
        val activityArg: Something1
        val somethingCreatedByConstructor: Something3
        val isBackPressed: Boolean
    }
}

class Something1
class Something2
class Something3

kotlin小ネタ:initializer block や property initializer は可能な限り避ける

※あまり深く考えてないのでまだアイデアレベルのメモ。ちゃんと考えないと一般化はできないと思われる。

initializer block や property initializer を利用する状況というのは、インスタンスが生成された状態、つまり this が参照できる状態で val/var の初期化完了が保証されていない状況と言える。そのため、『val で宣言と同時に初期化してるから確実に値が入ってる』と思ったが実際は null でしたなんてことが起こり得る。


以下のコードを考えてみる。
※fullNameを取得する処理が非常に重く、毎回 firstName と lastName から作り出すのは要件的に許容できないと仮定する。

class Person(first: String, last: String) {
    val firstName: String = first                  // "John"
    val lastName: String = last                    // "Smith"
    val fullName: String = "$lastName, $firstName" // "Smith, John"
}

fun main(args: Array<String>) { with(Person("John", "Smith")) {
    println("firstName = [$firstName], lastName = [$lastName], fullName = [$fullName]") }
}

// 実行結果: firstName = [John], lastName = [Smith], fullName = [Smith, John]


このコードに対して、メンバをアルファベット順にするリファクタリングをしようとするとコンパイルエラーになる。

// メンバをアルファベット順にするリファクタリングをしようとしてコンパイルエラーになる例
class Person(first: String, last: String) {
    val firstName: String = first
    val fullName: String = "$lastName, $firstName" // <- Variable 'lastName' must be initialized
    val lastName: String = last
}

これを見る限り val を用いれば非 null が保証されるように思える。

しかし、関数を経由するとそうはならず、バグを埋め込むことになる。

// メンバをアルファベット順にするリファクタリングをしたつもりがバグを埋め込んでしまった例
class Person(first: String, last: String) {
    val firstName: String = first       // "John"
    val fullName: String = toFullName() // "null, John" ← null が入っている
    val lastName: String = last         // "Smith"

    private fun toFullName() = "$lastName, $firstName"
}

fun main(args: Array<String>) { with(Person("John", "Smith")) {
    println("firstName = [$firstName], lastName = [$lastName], fullName = [$fullName]") }
}

// 実行結果: firstName = [John], lastName = [Smith], fullName = [null, John]

非 null の val を参照して null が返るのは、property initializer *1 のコードが走る時点でインスタンスの生成が完了しているため、つまり this が参照可能な状態のためと考えられる。

ということは、this を参照する関数(fun/get()/set())を呼び出すコードを書く場合は、関数が直接的及び間接的に参照するプロパティをすべて洗い出し、それらが初期化されていることを確認する必要があるということになる。それは、プロパティの初期化に関するコードを変更する際にも、そのプロパティを参照する全関数のコードを調査する必要があるということになる。

(´・ω・`)

それならば、this が作られる前に処理を行ってしまえばいいのでは?

// 引数をすべて primary constructor で扱うことにより this を参照することが無い保証をした場合
class Person private constructor(val firstName: String, val lastName: String, val fullName: String) {
    constructor(firstName: String, lastName: String) : this(
            firstName,                      // "John"
            lastName,                       // "Smith"
            toFullName(firstName, lastName) // "Smith, John"
    )

    companion object {
        private fun toFullName(firstName: String, lastName: String) = "$lastName, $firstName"
    }
}

fun main(args: Array<String>) { with(Person("John", "Smith")) {
    println("firstName = [$firstName], lastName = [$lastName], fullName = [$fullName]") }
}

// 実行結果: firstName = [John], lastName = [Smith], fullName = [Smith, John]

private primary constructor で対応することができた。

上記でバグ埋め込みの原因となった宣言順序変更と同様のリファクタリングを行ってみるとどうなるか。

// メンバの宣言順序をアルファベット順にリファクタリングする例
class Person private constructor(val firstName: String, val fullName: String, val lastName: String) {
    constructor(firstName: String, lastName: String) : this(
            firstName,                      // "John"
            toFullName(firstName, lastName),// "Smith, John"
            lastName                        // "Smith"
    )

    companion object {
        private fun toFullName(firstName: String, lastName: String) = "$lastName, $firstName"
    }
}

fun main(args: Array<String>) { with(Person("John", "Smith")) {
    println("firstName = [$firstName], lastName = [$lastName], fullName = [$fullName]") }
}

this を参照しない、つまり、受け取った値のみを利用しているため、初期化順序に影響されずに対応ができた。

実験のための実験として、primary constructor arguments の順序変更によりバグを埋め込む例が作れるか試してみる。

// 実験のための実験1
class Person private constructor(val firstName: StringBuilder, val lastName: StringBuilder, val fullName: StringBuilder) {
    constructor(firstName: StringBuilder, lastName: StringBuilder) : this(
            firstName,                                           // "John"
            lastName.apply { setLength(0); append("William")  }, // "William"
            toFullName(firstName, lastName)                      // "William, John"
    )
    
    companion object {
        private fun toFullName(firstName: StringBuilder, lastName: StringBuilder) = StringBuilder("$lastName, $firstName")
    }
}

fun main(args: Array<String>) { with(Person(StringBuilder("John"), StringBuilder("Smith"))) {
    println("firstName = [$firstName], lastName = [$lastName], fullName = [$fullName]") }
}

// 実行結果: firstName = [John], lastName = [William], fullName = [William, John]

上記の例では、secondary constructor の lastName の評価の際に引数に副作用を加えている。その結果、"Smith" が "William" に書き換えられている。

primary constructor の引数の順序変更をIDEリファクタリング機能で行ってみる。

// 実験のための実験2
class Person private constructor(val firstName: StringBuilder, val fullName: StringBuilder, val lastName: StringBuilder) {
    constructor(firstName: StringBuilder, lastName: StringBuilder) : this(
            firstName,                                          // "John"
            toFullName(firstName, lastName),                    // "Smith, John"
            lastName.apply { setLength(0); append("William")  } // "William"
    )
    
    companion object {
        private fun toFullName(firstName: StringBuilder, lastName: StringBuilder) = StringBuilder("$lastName, $firstName")
    }
}

fun main(args: Array<String>) { with(Person(StringBuilder("John"), StringBuilder("Smith"))) {
    println("firstName = [$firstName], lastName = [$lastName], fullName = [$fullName]") }
}

// 実行結果: firstName = [John], lastName = [William], fullName = [Smith, John]

IDEリファクタリング機能を使っただけなのに結果が変わってしまった。(fullName が "William John" ではなく "Smith John" になっている)
そうなるように書いたコードなので当然ですがw

ここで重要なポイントは、以下のようなものになると思う。

  • どんなコードでも実験のための実験2で書かれたような無茶なことはできる。
  • しかしそのようなコードが製品のコードとして実際に書かれる可能性は著しく低い。 *2


primary constructor 方式は this を参照するコードを書こうとしてもコンパイルエラーになるという点がステキかも。

// 保守時に『無用なバケツリレーをメンバ参照に改善しよう』と改善するつもりでコンパイルエラーを起こす例
class Person(val firstName: String, val fullName: String, val lastName: String) {
    constructor(firstName: String, lastName: String) : this(
            firstName,
            // Cannot access 'toFullName' before superclass constructor has been called と怒られる
            toFullName(),
            lastName
    )

    private fun toFullName() = "$lastName, $firstName"
}


では、primary constructor 引数だけで対応できるかというと、当然 this を必要とするケースでは対応できないし、get()/set() を伴う場合など言語仕様的に対応できないケースも多々ある。

以下、転落人生の例:

class Person(val firstName: String, val fullName: String, val lastName: String, address: String) {
    constructor(firstName: String, lastName: String, address: String) : this(
            firstName,
            toFullName(firstName, lastName),
            lastName,
            address
    )
    
    var address: String by Delegates.observable(address) {
        _, old, new ->
        println("address: [$old] -> [$new]")
    }
    
    companion object {
        private fun toFullName(firstName: String, lastName: String) = "$lastName, $firstName"
    }
    
    // 実行結果
    // firstName = Brandon, lastName = Walsh, fullName = Walsh, Brandon, address = Beverly Hills, 90210
    // address: [Beverly Hills, 90210] -> [No fixed abode]
    // firstName = Brandon, lastName = Walsh, fullName = Walsh, Brandon, address = No fixed abode
}

fun main(args: Array<String>) {
    with(Person("Brandon", "Walsh", "Beverly Hills, 90210")) {
        println("firstName = $firstName, lastName = $lastName, fullName = $fullName, address = $address")
        address = "No fixed abode"
        println("firstName = $firstName, lastName = $lastName, fullName = $fullName, address = $address")
    }
}

primary constructor の引数の評価が始まる前(や処理中)に処理を入れ要とする場合、強引に入れ込むなら以下のような方法がある。

class Something private constructor(val firstName: String, val lastName: String, val fullName: String) {
    constructor(firstName: String, lastName: String):this(doSomething().run { firstName }, lastName, "$lastName, $firstName")
    
    companion object {
        fun doSomething() = println("doSomething()")
    }
    
    // 実行結果: doSomething()
}

fun main(args: Array<String>) {
    Something("John", "Smith")
}

でも保守性悪そう(´・ω・`)
primary constructor と secondary constructor の signature が同じ場合は使えないし、、、。
そういう場合は設計から考え直すが吉ですかね。

次は、primary constructor arguments を作成するための一時変数が欲しい場合を考えてみる。
なんかの計算をしてその値を格納するクラス。処理が重いのでキャッシュが必須とする。

class Something(val a: Int, val b: Int, val c: Int) {
    // 計算処理がめちゃ重いのでキャッシュせざるを得ない
    val d = a + b
    // 計算処理がめちゃ重いのでキャッシュせざるを得ない
    val e = d + c
}

fun main(args: Array<String>) {
    Something(3, 5, 7).apply {
        println("a = $a, b = $b, c = $c, d = $d, e = $e")
    }
}

// 実行結果: a = 3, b = 5, c = 7, d = 8, e = 15


全て primary constructor arguments として入れ込もうとすると以下のようになる。

class Something private constructor(val a: Int, val b: Int, val c: Int, val d: Int, val e: Int) {
    // a + b の計算を 2 回やっているのがもったいない
    constructor(a: Int, b: Int, c: Int) : this(a, b, c, a + b, a + b + c)
}

上記の場合、a + b の計算を 2 回やっているのがもったいない

計算を重複して行わないよう、ThreadLocal 上に値を入れ込んでみる。

// ThreadLocal を利用して a + b の計算結果を格納する例
class Something private constructor(val a: Int, val b: Int, val c: Int, val d: Int, val e: Int) {
    constructor(a: Int, b: Int, c: Int) : this(a, b, c, calcD(a, b), calcE(c))
    
    companion object {
        private val tmp = ThreadLocal<Int>()
        private fun calcD(a: Int, b: Int) = (a + b).apply { tmp.set(this) }
        private fun calcE(c: Int) = tmp.get() + c
    }
}

上記の例は正しく動作するが、順序変更のリファクタリングをすることによりバグを埋め込むリスクが生まれる。

// change signature のリファクタリングをしてバグを入れ込む例
class Something private constructor(val a: Int, val b: Int, val c: Int, val e: Int, val d: Int) {
    constructor(a: Int, b: Int, c: Int) : this(a, b, c, calcE(c), calcD(a, b))
    
    companion object {
        private val tmp = ThreadLocal<Int>()
        private fun calcD(a: Int, b: Int) = (a + b).apply { tmp.set(this) }
        private fun calcE(c: Int) = tmp.get() + c // <- ここでヌルポが出る
    }
}

順序を変更しても問題ないバージョン。

class Something private constructor(val a: Int, val b: Int, val c: Int, val d: Int, val e: Int) {
    constructor(a: Int, b: Int, c: Int) : this(a, b, c, d(a, b, c), e(a, b, c))

    object args {
        private var d: ThreadLocal<Int>? = null
        private var e: ThreadLocal<Int>? = null

        private fun init(a: Int, b: Int, c: Int) {
            d = ThreadLocal<Int>().apply { set(a + b) }
            e = ThreadLocal<Int>().apply { set(d!!.get() + c) }
        }

        fun d(a: Int, b: Int, c: Int): Int {
            if (d == null) init(a, b, c)
            return d!!.get()
        }

        fun e(a: Int, b: Int, c: Int): Int {
            if (e == null) init(a, b, c)
            return e!!.get()
        }
    }
}

引数を毎回全部渡さなきゃならんのと、コード量が多いのが面倒ですね。
実際に有効なケースがどの程度あるのかは未知数ですね。

*1:厳密には initializer block も含む body 全体

*2:少なくともおいらは今までに一度も見たことが無い

kotlin小ネタ:クラス分割メモ

実用性未検証だけどとりあえずメモ。

  1. 平面上にメソッドとバッキングフィールドを持つメンバを点として配置する。
  2. メソッドからバッキングフィールドを持つメンバへの参照関係を線として表現する。
  3. 相互に到達し得るパスが存在しないグラフ毎に別クラス化を検討する。

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:現状のメンバーは視点が狭くなっている可能性が高い。

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" のみ
}