小規模システム開発の最終成果物とプロセス
小規模システム開発の最終成果物は、以下の2つだけ*1でいい気がするという妄想をメモしてみた。
- チェックリスト形式の Prerequisites(Problem Definition, Requirements, Architecture)
- リリース直前にチェックリストを全てチェックすることにより、ソースコードが Prerequisites を満たすことを証明できる。
- チェックリスト形式にすることにより、Prerequisites とソースが乖離してしまうリスクがなくなる。
- Prerequisites は何でもアリ。*2
- 外部仕様をすべて入れる(決定稿でなくても仮置きで入れる) *3
- 初期 Prerequisites 完成までのコストは、プロジェクト全体のコストのうち、工数的には10-20%、リードタイム的には20-30%を割く感じ。*4
- 初期 Prerequisites 作成はソースコード開発を含むイテレーションを開始する前にすべて完了する必要がある点に注意。*5
- 実現可能性検証を Prerequisites 開発フェーズに含めてしまうという手もアリ。*6
- ソースコード
*1:プロジェクトの規模が小さく、長期保守が不必要で、プロジェクトチーム自体がアプリの要求定義に関わる唯一のステークホルダーであり、メンバーの入れ替えもないということであれば、Prerequisites はプロジェクト完成後に破棄して、『ソースコードの現状をもって仕様とする』としてもいいと思われる。Prerequisites の保守コストはそれなりに大きいので。
*2:『要求定義とは一般的にこういうものだ』的な思考は危険。例えば、要件定義に実装技術を入れるべきではないというべき論は、顧客が特定の実装技術を要求する場合に矛盾を引き起こす。このような場合に顧客の要求と開発者のべき論の辻褄合わせのために顧客の要求を Prerequisites に入れず設計書に入れたりすると、事情を知らない後任者が別の実装技術を使ってしまう危険がある。
*3:システムを扱うユーザを全て識別し、システムがユーザに提供するインタフェース仕様をすべて定義する。GUIの見た目や画面遷移、CUIの接続仕様、それらの振る舞い、など全てが含まれる。これらの仕様は往々にして抜けや矛盾をはらみ、開発後期には誰にも使われないお荷物となるため、ドメインモデルの作成を通じて抜けおよび矛盾を排除する。そのため、ドメインモデルは Prerequisites としての成果物となる。ドメインモデルについては、インタフェース定義、疑似コード、仕様ロジックを含むAbstractClass、テストケース、まで含めることが可能。事前に予測可能な工数を支払うことにより予測不可能な手戻り工数を削減できる。固定金利と変動金利のスワップのようなもの。データ周りの手戻りはドメインモデルに決定的な手戻りを起こす可能性があるので、DBの利用が確定していてDBの性能要件が厳しいプロジェクトの場合はテーブル定義と疑似コード上でのSQL文確定までやってしまうという手もある。
*4:工数とリードタイムに差が出るのは、一般的に、初期 Prerequisites 完成までの段階では開発者の人数が少なく、それ以降の工程で開発者が増えるため。
*5:建築に例えるなら、小さな家からイテレーション開発しても漸近的に高層ビルは作れないし、建築開始後に家の場所は変更できない。また、防音要件・電磁遮蔽要件・部屋の積載荷重・柱の位置などは一見後で決められそうな細かな条件により変わる可能性があり、アーキテクチャに大きな影響を及ぼす詳細仕様のみを洗い出そうとしても判断に多くの工数が必要とされる上に正しく洗い出せていることを証明する方法が無く、それなら思いつくだけの細かな条件を全部出してしまったほうが早いし抜けが少ない。どこまで詳細化するかの判断は最終的には勘と経験に依存するが、これ以上の詳細化は明らかに必要ないと断言できるまで詳細化しておくというのが安全策。
*6:基本的にはソースコード開発フェーズの最初にやって、問題があれば Prerequisites を修正すればよいのだが、会社やプロジェクトメンバにとって経験のないライブラリやミドルウェアなどを採用する場合や、ソースコード開発フェーズに人数を増やす場合は、実現可能性検証用の使い捨ての仕様作成と実装を Prerequisites 開発フェーズに中に行い、一通りのアーキテクチャ検証をこなしておくのは有効だと思われ。
■
Some programmers do know how to perform upstream activities, but they don't prepare because they can't resist the urge to begin coding as soon as possible.
Code Complete: A Practical Handbook of Software Construction, Second Edition: Steve McConnell: 0790145196705: Amazon.com: Books
吹いたw
kotlin tips: 自己を参照される可能性のあるコールバックとか単一処理を複数のコールバックで扱うことについて
※とりあえず五月雨式にメモしておく。結論はまだない。
戻り値方式とクロージャ方式の例
下記の returnValue1.kt は、Model の関数が結果を戻り値で返しています。
// // returnValue1.kt // package returnValue1 import returnValue1.AppendResult.Correct import returnValue1.AppendResult.Wrong interface Model { val isCompleted: Boolean fun append(c: Char): AppendResult } sealed class AppendResult { class Correct(val isComplete: Boolean) : AppendResult() class Wrong : AppendResult() } class ModelImpl : Model { override var isCompleted = false private set private val inputLetters = StringBuilder() override fun append(c: Char): AppendResult { return when (c) { in '0'..'9' -> { inputLetters.append(c) Correct((inputLetters.length == 2).apply { isCompleted = this }) } else -> { Wrong() } } } } object view { fun onCorrectLetter(model: ModelImpl) { println("『ピッ!』") } fun onWrongLetter(model: ModelImpl) { println("『ブブーッ!』") } fun onComplete(model: ModelImpl) { println("『ピロリロリン!』[isCompleted = ${model.isCompleted}]") } } // controller fun main(args: Array<String>) { val model = ModelImpl() fun append(c: Char) { with(model.append(c)) { when (this) { is Correct -> { view.onCorrectLetter(model) if (isComplete) view.onComplete(model) } is Wrong -> view.onWrongLetter(model) } } } append('1') // 数字なので正常受付。『ピッ!』と鳴る。 append('a') // 数字じゃないのでエラーを表示。『ブブーッ!』っと鳴る。 append('2') // 数字なので正常受付。『ピッ!』と鳴る。入力完了音が『ピロリロリン!』と鳴る。 }
実行結果
『ピッ!』 『ブブーッ!』 『ピッ!』 『ピロリロリン!』[isCompleted = true]
下記の closure1.kt は、Model 関数にクロージャが渡され、結果をそのクロージャ経由で通知しています。
結果毎の処理を Model 側でキックしてくれるので、controller 側に条件分岐が必要無くなっています。
// // closure1.kt // package closure1 interface Model { val isCompleted: Boolean fun append(c: Char, onCorrectLetter: (model: ModelImpl) -> Unit, onWrongLetter: (model: ModelImpl) -> Unit, onComplete: (model: ModelImpl) -> Unit) } class ModelImpl : Model { override var isCompleted = false private set private val inputLetters = StringBuilder() override fun append(c: Char, onCorrectLetter: (model: ModelImpl) -> Unit, onWrongLetter: (model: ModelImpl) -> Unit, onComplete: (model: ModelImpl) -> Unit) { when (c) { in '0'..'9' -> { inputLetters.append(c) onCorrectLetter(this) if (inputLetters.length == 2) { isCompleted = true onComplete(this) } } else -> { onWrongLetter(this) } } } } object view { fun onCorrectLetter(model: ModelImpl) { println("『ピッ!』") } fun onWrongLetter(model: ModelImpl) { println("『ブブーッ!』") } fun onComplete(model: ModelImpl) { println("『ピロリロリン!』[isCompleted = ${model.isCompleted}]") } } // controller fun main(args: Array<String>) { val model = ModelImpl() model.append('1', view::onCorrectLetter, view::onWrongLetter, view::onComplete) // 数字なので正常受付。『ピッ!』と鳴る。 model.append('a', view::onCorrectLetter, view::onWrongLetter, view::onComplete) // 数字じゃないのでエラーを表示。『ブブーッ!』っと鳴る。 model.append('2', view::onCorrectLetter, view::onWrongLetter, view::onComplete) // 数字なので正常受付。『ピッ!』と鳴る。入力完了音が『ピロリロリン!』と鳴る。 }
実行結果
『ピッ!』 『ブブーッ!』 『ピッ!』 『ピロリロリン!』[isCompleted = true]
下記の closure2.kt は、Model にあらかじめクロージャが渡され、関数が結果をそのクロージャ経由で通知しています。
関数呼び出し毎に異なるクロージャを渡す必要がない場合はこちらのほうがさらにシンプルです。
// // closure2.kt // package closure2 interface Model { val isCompleted: Boolean fun append(c: Char) } class ModelImpl(val onCorrectLetter: (model: ModelImpl) -> Unit, val onWrongLetter: (model: ModelImpl) -> Unit, val onComplete: (model: ModelImpl) -> Unit) : Model { override var isCompleted = false private set private val inputLetters = StringBuilder() override fun append(c: Char) { when (c) { in '0'..'9' -> { inputLetters.append(c) onCorrectLetter(this) if (inputLetters.length == 2) { onComplete(this) isCompleted = true } } else -> { onWrongLetter(this) } } } } object view { fun onCorrectLetter(model: ModelImpl) { println("『ピッ!』") } fun onWrongLetter(model: ModelImpl) { println("『ブブーッ!』") } fun onComplete(model: ModelImpl) { println("『ピロリロリン!』[isCompleted = ${model.isCompleted}]") } } // controller fun main(args: Array<String>) { val model = ModelImpl(view::onCorrectLetter, view::onWrongLetter, view::onComplete) model.append('1') // 数字なので正常受付。『ピッ!』と鳴る。 model.append('a') // 数字じゃないのでエラーを表示。『ブブーッ!』っと鳴る。 model.append('2') // 数字なので正常受付。『ピッ!』と鳴る。入力完了音が『ピロリロリン!』と鳴る。 }
クロージャ方式にてバグを埋め込む可能性のある例
closure1.kt と closure2.kt については、Model が受け取るコールバック内部から自身が参照される場合、バグを埋め込む可能性があります。
具体的には、以下のようにコードを変更すると結果表示が変わってしまいます。
// 正しい順序 inputLetters.append(c) onCorrectLetter(this) if (inputLetters.length == 2) { isCompleted = true // isCompleted を true に変更してから onComplete(this) を呼び出している onComplete(this) } // 順序を変更してバグを埋め込む例 inputLetters.append(c) onCorrectLetter(this) if (inputLetters.length == 2) { onComplete(this) // この時点では isCompleted を true に変更する前に onComplete(this) を呼び出している isCompleted = true }
実行結果
『ピッ!』 『ブブーッ!』 『ピッ!』 『ピロリロリン!』[isCompleted = false]
returnValue1.kt のように戻り値を返す方式の場合、間違えようとしても間違えようがないので、コールバック方式よりも戻り値方式のほうが保守性が高そうです。
要件変更への耐性
余談になりますが、要件変更への耐性の例として、以下の要件変更を考えてみる。
- 完了時は『ピッ!』という音を出さずに『ピロリロリン!』だけを鳴らす。
戻り値方式の場合の変更点
1行削除でOK。
when (this) { is Correct -> { view.onCorrectLetter(model) // ← この1行を削除すればOK if (isComplete) view.onComplete(model) } is Wrong -> view.onWrongLetter(model) }
コールバック方式の場合
controller と model の両方に変更が発生する例。
// // closure3.kt // package closure3 interface Model { val isCompleted: Boolean fun append(c: Char) } // 変更前: class ModelImpl(val onCorrectLetter: (model: ModelImpl) -> Unit, val onWrongLetter: (model: ModelImpl) -> Unit, val onComplete: (model: ModelImpl) -> Unit) : Model { class ModelImpl(val onCorrectLetter: (isCompleted: Boolean, model: ModelImpl) -> Unit, val onWrongLetter: (model: ModelImpl) -> Unit) : Model { override var isCompleted = false private set private val inputLetters = StringBuilder() override fun append(c: Char) { when (c) { in '0'..'9' -> { inputLetters.append(c) // 変更前 // onCorrectLetter(this) // if (inputLetters.length == 2) { // onComplete(this) // isCompleted = true // } // 変更後 onCorrectLetter((inputLetters.length == 2).apply { isCompleted = this }, this) } else -> { onWrongLetter(this) } } } } object view { fun onCorrectLetter(model: ModelImpl) { println("『ピッ!』") } fun onWrongLetter(model: ModelImpl) { k println("『ブブーッ!』") } fun onComplete(model: ModelImpl) { println("『ピロリロリン!』[isCompleted = ${model.isCompleted}]") } } // controller fun main(args: Array<String>) { // 変更前 // val model = ModelImpl(view::onCorrectLetter, view::onWrongLetter, view::onComplete) // 変更後 val model = ModelImpl( { isCompleted, model -> when (isCompleted) { true -> view.onComplete(model) false -> view.onCorrectLetter(model) } }, view::onWrongLetter ) model.append('1') // 数字なので正常受付。『ピッ!』と鳴る。 model.append('a') // 数字じゃないのでエラーを表示。『ブブーッ!』っと鳴る。 model.append('2') // 数字なので正常受付。入力完了音が『ピロリロリン!』と鳴る。 }
controller 側のみに変更を局所化することも一応可能ではあるという例。
フラグ立てまくりでカオスwww
// // closure4.kt // package closure4 interface Model { val isCompleted: Boolean fun append(c: Char) } class ModelImpl(val onCorrectLetter: (model: ModelImpl) -> Unit, val onWrongLetter: (model: ModelImpl) -> Unit, val onComplete: (model: ModelImpl) -> Unit) : Model { override var isCompleted = false private set private val inputLetters = StringBuilder() override fun append(c: Char) { when (c) { in '0'..'9' -> { inputLetters.append(c) onCorrectLetter(this) if (inputLetters.length == 2) { onComplete(this) isCompleted = true } } else -> { onWrongLetter(this) } } } } object view { fun onCorrectLetter(model: Model) { println("『ピッ!』") } fun onWrongLetter(model: Model) { println("『ブブーッ!』") } fun onComplete(model: Model) { println("『ピロリロリン!』[isCompleted = ${model.isCompleted}]") } } // controller fun main(args: Array<String>) { var isCorrectLetter = false fun onCorrectLetter(model: Model) { isCorrectLetter = true } var isComplete = false fun onComplete(model: Model) { isComplete = true } val model = ModelImpl(::onCorrectLetter, view::onWrongLetter, ::onComplete) fun append(c: Char) { model.append(c) if (isCorrectLetter) { if (isComplete) view.onComplete(model) else view.onCorrectLetter(model) } isCorrectLetter = false isComplete = false } append('1') // 数字なので正常受付。『ピッ!』と鳴る。 append('a') // 数字じゃないのでエラーを表示。『ブブーッ!』っと鳴る。 append('2') // 数字なので正常受付。入力完了音が『ピロリロリン!』と鳴る。 }
AndroidでUIメソッド呼び出しの共通時刻を取得する
概要
UIスレッドから呼び出される処理全体で初期化やバケツリレー無しで共通時刻を用意したいなーという話。
下記のメソッド1~Nすべてでボタン押下時刻を共通時間として取得するとか。
ボタン押下 → onClick()実行 → メソッド1呼び出し → メソッド2呼び出し … メソッドN 呼び出し
本題
Android アプリ開発では、時間情報として currentTimeMillis() や uptimeMillis() を扱うことがあります。
これらのメソッドは現在時刻を返すので、同一スレッド内で連続で呼び出しても異なる時刻が返されます。*1
連続した処理で同じ時刻を扱いたい場合、どこかで時刻を取得して、それをメソッドの呼び出し階層でバケツリレーする方法で対応することが可能です。
しかし、含まれるメソッド数が大きい場合や、自身は時刻情報を利用しないのにバケツリレーのためだけに引数を受け取るメソッドが多数あるような場合は面倒です。そのような場合は独自にスレッドを生成して ThreadLocal にスレッド生成直後の時刻を設定するという方法も考えられます。
しかし、UIスレッドの場合、既にスレッドが存在しているし、描画開始タイミングもアプリ側で制御できません。
そこで、main looper が message の dispatch を開始するタイミングで時刻を設定する方法を試してみました。
object MainLooperMessageDispatchUptimeMillisManager { private var isAvailable = false private set var mainLooperMessageDispatchUptimeMillis: Long = -1 get() { assertMainThread() return field } private set init { assertMainThread() Looper.getMainLooper().setMessageLogging { // 開始タイミングのみ時刻を記録する。 // 最初に実行されるのは、init{} が実行された後で最初に main looper が message を dispatch した時。 // init{} が実行された時の main looper の dispatch 終了タイミングでは呼び出しが無い点に注意。 isAvailable = !isAvailable if (isAvailable) mainLooperMessageDispatchUptimeMillis = SystemClock.uptimeMillis() } } private fun assertMainThread() { if (Thread.currentThread() != Looper.getMainLooper().thread) throw IllegalStateException("Current thread is not main thread.") } }
これを Application#onCreate()などで一度評価することによって初期化します。*2
class MyApplication : Application() { override fun onCreate() { super.onCreate() // 式として評価して初期化する MainLooperMessageDispatchUptimeMillisManager } }
使うときは static import しておくと便利。
import mypackage.MainLooperMessageDispatchUptimeMillisManager.mainLooperMessageDispatchUptimeMillis as uptimeMillis
呼び出し例
val handler1 = Handler() val handler2 = Handler() fun f1(handler: String, name: String) { println("f1(): $handler: $name: $uptimeMillis"); Thread.sleep(10) } fun f2(handler: String, name: String) { println("f2(): $handler: $name: $uptimeMillis"); Thread.sleep(10) } handler1.postDelayed(100) { f1("handler1", "a1"); f2("handler1", "a2") } handler1.postDelayed(100) { f1("handler1", "b1"); f2("handler1", "b2") } handler2.postDelayed(100) { f1("handler2", "c1"); f2("handler2", "c2") } handler2.postDelayed(100) { f1("handler2", "d1"); f2("handler2", "d2") }
実行結果
f1(): handler1: a1: 844721417 ← 同一スレッドでは時間が共通となる。 f2(): handler1: a2: 844721417 ← 同一スレッドでは時間が共通となる。 f1(): handler1: b1: 844721438 ← main looper から dispatch されるタイミングが異なるので上記の処理と時刻がずれる。 f2(): handler1: b2: 844721438 f1(): handler2: c1: 844721459 ← 同一 handler でも時刻が異なるのだから別 handler でも当然そうなる。 f2(): handler2: c2: 844721459 f1(): handler2: d1: 844721480 f2(): handler2: d2: 844721480
Looper#setMessageLogging(android.util.Printer) はどこからでもセットできてしまう点は注意すべきですね。
kotlin tips:extension function reference
kotlin のドキュメントに拡張関数参照の方法がさらっと書かれている。
あまりにもさらっと書かれすぎていて一度スルーしてしまったので、例を交えてメモしておくw
If we need to use a member of a class, or an extension function, it needs to be qualified. e.g. String::toCharArray gives us an extension function for type String: String.() -> CharArray.
Reflection - Kotlin Programming Language
以下は、本家にあった拡張関数のコピペ。
fun MutableList<Int>.swap(index1: Int, index2: Int) { val tmp = this[index1] // 'this' corresponds to the list this[index1] = this[index2] this[index2] = tmp }
上記の拡張関数を参照として扱いたい場合のコードは以下。
MutableList<Int>::swap
kotlin小ネタ:Handler#postDelayed()のextension function化
普通に Handler#postDelayed() を利用すると以下のようになり、結構見づらい。
mHandler.postDelayed( { println("postDelayed()") }, 2000)
kotlinでは、関数の最終引数が関数の場合には、ラムダ式を括弧の外側に記述できる。
Higher-Order Functions and Lambdas - Kotlin Programming Language
In Kotlin, there is a convention that if the last parameter to a function is a function, and you're passing a lambda expression as the corresponding argument, you can specify it outside of parentheses
ということで、以下のような extension function を用意する。*1
fun Handler.postDelayed(delayMillis: Long, r: () -> Unit) { postDelayed(r, delayMillis) }
すると、ラムダ式を関数の body のような感じで書ける。
mHandler.postDelayed(2000){ println("postDelayed()") }
いい感じ(`・ω・´)
やる気とかいろいろ
五月雨式なメモ。
※『やる気』と『動機』は違う。以降、『やる気』という言葉は気分的な意味合いで使用する。
- やる気という考え方がそもそもおかしい。『やる気が無いのでやらなかった』と『実力不足でできなかった』は同じこと。『やる気さえあればできる』というのはボクサーが『ラッキーパンチさえ当たれば勝てる』と言っているのと同じ。
やる気は無いのが前提。← (追記)決定性なので間違いやる気は偶然の幸運。← (追記)決定性なので間違い- やる気が必要なことは計画に組み込まない。
- 突然現れる直感は大体信じてOK。*1
- 迷いは強引な即決か思考停止で対応。*2
- 集中力の切れた状態やら頭の飽和状態を現実逃避と一喝するようなストイックな考えは逆効果。とりあえず休めwww*3
- やる気のメカニズム(おいら調べ)
- やる気の強さに意味はなく、やるかやらないかに意味がある。
- 判断自体を減らすことが重要。
- やる気という概念はやるやらないという判断に迷った際に意味を持つ。逆を言えば、判断しないでよい状況を作り出してしまえばやる気というものを考える必要がなくなる。*7
- 予定表や計画書
*1:直感は純粋に最適解であることが多く、直感の通りに活動することは楽じゃないことが多い。そのため、直感を元にその先を思考するのはOKだが、直感を一案として総合的に思考するのと直感を否定する言い訳探しになり、導かれる結果は怠惰でナイーブな願望となる
*2:人生は有限なので、時間をかけて正しい選択をするよりも短時間で数多くの選択を重ねるべき。また、迷いに対する思考は往々にして自己正当化のための精神活動であり、結論は怠惰でナイーブな願望となる。
*3:やる気は決定性の変数なので、特に大きな出来事があるわけでもなくやる気が落ちてきた場合は、その原因が必ず存在する。多くの場合は飽きや疲れからなので、戻る時に葛藤が生じない形で休憩を取るのが正解。何もない部屋で横になるのがおすすめで、ネットサーフィンとかメールチェックなどは戻る時に葛藤が生じて結果として戻れなくなる可能性が高いので注意。ストイックなやり方は、絶対に失敗しない保証がある場合は有効で、今後のやる気の底上げにもなる。ただし、失敗の悪影響が甚大なので注意。
*4:例1:毎朝食事することが当たり前となって10年たった人は、いつも通りの翌朝には朝食をとる。この朝食をとるという行動は葛藤なく自然に行われているため『やる気』という概念は適用できない。
*5:例2:毎朝食事しないことが当たり前となって10年たった人は、いつも通りの翌朝には朝食をとらない。この朝食をとるという行動は葛藤なく自然に行われているため『やる気』という概念は適用できない。
*6:やる気は人格(本能的な快と不快の基準と過去の記憶)と状況によって決定されるため、曖昧なものではなく、決定性である。もちろん、それらを測定できるわけではないのだが、予測可能であるということが重要。たとえば、特殊な理由もなくなんとなくやる気が出ないものは、明日になっても明後日になっても同程度のやる気である可能性が高いという予測が可能。
*7:服の整理がめんどくさい人はノームコアにしてしまえばいいし、食事の買い出しや準備が面倒ならメニューを固定してしまえばいい。このように判断する箇所を減らすことによりやる気という概念自体を無効化する方向に持って行くことが可能。
*8:たとえば小学生の夏休みの予定表は、一度でも守れなかったらリスケが必要。『明日はきっと』はまず無理。なぜなら明日も今日と同じやる気の可能性が最も高く、結果としてまた守れない可能性が最も高くなる。
*9:負けケースを上回る勝ちケースで表向きの相殺を計ることはできるが、負けない気概は 負けた回数/勝った回数 という単純な式のようなもので、一定以上の年齢となると負けた回数を買った回数で取り戻すことが不可能になる。