引数について

ふと思ったのでメモ。

要約:インタフェースメソッドの引数はインタフェース設計の視点で決定されるべきであり実装の都合を持ち込んではいけないかもしれないしそうでないかもしれないがおいらはそう思うw。

※サンプルを見やすくするため getter/setter は使っていない。
※プリミティブ型ではなくバリデーション等を含めてクラス化すべきとか言う話も今回は省略。

引数としてオブジェクトを渡すコード例:

interface User {
    val id: Long? // <- キモいw
    val name: String
    val address: String
}

interface UserService {
    fun register(newUser: User): User
}

必要な値のみ渡す例:

interface User {
    val id: Long
    val name: String
    val address: String
}

interface UserService {
    fun register(name: String, address: String): User
}

上記の2例は必要な値のみ渡す場合のほうが完成度が高いような気がする。しかし、User のメンバが100個あったらオブジェクトを渡す方式を採用してしまうことが多いのではないだろうか。

おいらの現段階での所感としては、そもそもバラすかどうかという考え方自体が本末転倒で、インタフェースの視点で必要な情報だけ渡せばいいと思う。

  • ユーザー登録時に id が決定されるので id を渡すのはそもそもおかしい。非 null を入れられたら実行時エラーにしなきゃならんとか無駄に複雑。
  • 別に User という形で渡す必要が無い。UIからユーザーが入力した値を元にUserを作成する場合になどわざわざ User を作るというのがナンセンス。

User の要素が100個あるような場合に一部データのみ変更したい場合は setter/getter のある UserInfo を作りたくなる気もするが、kotlin なら以下のように default argument と named arguments で対応できる。*1

interface User {
    val id: Long
    val name: String
    val address: String
}

interface UserService {
    fun register(name: String, address: String): User
    fun update(id: Long, name: String? = null, address: String? = null): User
    // 他での変更を考慮するなら下記のようにしてもいいかも。
    fun update(user: User, name: String? = null, address: String? = null): User
}

では、UserService.update() を叩く前にバケツリレーが必要な場合はどうかというと、確かに User の要素が100個あったらそれをバケツリレーするのはつらいものがある。しかし、UserService の視点では、そんなことは全く関係ない。つまり、UserInfo とか作りたいなら勝手に UserService の外でやってくれという話。しかし、以下のようなコードがソース中に分散されても困る。

// name, address, ... と100個あったら結構辛いものがある(´・ω・`) 
userService.register(user.name, user.address)

だからと言って、UserService に実装のツケを払わせるのは本質的におかしい。

重複するコードを抑えたいのであれば、以下のような方法で対処が可能。

interface UserInfo {
    var name: String
    var address: String
    fun register(userService: UserService)
}

しかし、UserInfo が UserService に依存するのも変。ならば、以下のように extension を使えばよい。

fun UserService.register(userInfo: UserInfo) {
    register(userInfo.name, userInfo.address)
}

extension の置き場とかはどこでもいいけど、UserService に入れるのはインタフェースの抽象化が崩れるので本末転倒。例えば、UserService が domain model 用のモジュール*2に配置され、UserInfo が domain model の実装用モジュールに配置されているとしたら、上記 extension も domain model の実装モジュールに入るのが正解ですかね。

*1:全ての引数に対して適切なデフォルト値を設定する必要がある。null が常に適切とは限らない。

*2:kotlin 用語としての module。

package private についてふと思ったこと。

ふと思ったので自分用にメモしておく。思い付きなので深く考えずにとりあえず殴り書きするだけっす。

あるオブジェクト内部の情報を外部に開示したくない場合に、情報隠蔽は有効な方法だと思われる。しかし、特定の相手には開示したいという場合もあり、その場合には java なら package private が利用できる。

おいら的には今は kotlin でのコーディングが大半を占めるようになったので、internal は使えても package private は使えない。kotlin 転向後に結構この点は心に引っかかったし、コミュニティでもこの点のディスカッション(Kotlin to support package protected visibility)が続いている。(たぶん永遠に終わらないwww)

当初はおいらも package private があったほうが良い派の意見だった。しかし、少なくとも現時点では全く困っていない。むしろ package private は多くの場合に保守性を低下させる要因になると考えるようになった。

package private 導入を望む人たちの意見は基本的に以下のコメントに集約されていると思われ。

if i understand it correctly your suggestion implies that if i want to have 1 public class and 9 package protected that do the actual job in java, i have to create a separate project declare those 9 as internal and then have this project compiled and accessible using Maven or Gradle or whatever one uses for the DI?

Imho that's an overkill...

Note that if i do 10 classes in one file 9 private i wont even be able to test those 9.

I personally never saw any code working around package protected access using same package name... it will just differ from your regular package structure and will constantly remind devs "fix me this is a shitcode".

言ってることはもっともだし、確かにそういうケースは多いと思う。しかし、言語の自由度を高くしたいという要求であれば、極論を言えばアセンブラを選択するべきであり、java vm 限定ならバイトコードを直接書くべきという結論に行きつく。当然ならがそういう話ではないわけで、行きつく先は、自由度をどのレベルに設定するかという話になる。

言語仕様がどうあるべきかという話なら、その言語がどのような設計方法論を支持するものなのかを考える必要があるんじゃなかろうか。

話は逸れるが、上記以外のコメントで java 言語仕様にあるんだから入れろ的なニュアンスの話もあったが、Java 言語が発表された 1995年当時と今では普及している設計方法論が異なるので、互換性のために java vm 仕様を踏襲しろというのは良いが java 言語仕様をそのまま新しい言語に取り込めというのは暴論だと思われる。

話は戻るが、べき論や抽象論で討論しても平行線なので、上記コメントにある、1つの public class が 9つの package private class を参照しているというケースをどのように扱うかという点について考えるのがこのディスカッションの落としどころになるんじゃないかという気がする。

この点において、ディスカッションは平行線を辿っている。『module にすればいいじゃん』 → 『それは overkill だ』的な感じでw。

あらゆる視点で考えるほどおいらはこの問題について考える気はないし、おいら自身が困っているわけじゃないのでどちらが正しいかというのも正直どうでもいい。しかし、おいらが困っていない理由だけは自分用にメモしておく。

『1つの public class が 9つの package private class を参照している』というようなケースに関して、それをおいらは package private にしようと思わない。理由は以下。

  • public A と package private の B, C というクラスのグループと、public D と package private の E, F というクラスのグループがあるとして、それらのグループ単体で package private を利用できるとする。その際、A と D を同一パッケージにしたいが、A から E, F、D から B, C にアクセスさせたくないとしたら、package private では対応できない。このような設計の拡張が無いことを保証するには、プロジェクトのソースコードの永久凍結宣言をするか未来予知ができるエスパーを雇わない限り無理。つまり、現代の多くのプロジェクトで採用されるクラス設計方針との相性が悪いという話。おいら的には、kotlin 転向後に、package private が使えないため auto completion の候補が多くなってウザーとか思った時期もあったが、これは慣れた。そもそも、上記の破綻例を解決できるようなパッケージングシステムがあったとしたら、現行の package private 方式もウザーとなるはず。慣れの問題ですね。
  • もう一つの理由は、(っていうか実はこれが唯一最大の理由と言ってもいいのだが)、package private を利用したい場合の大半は、静的な設計と動的な設計をごちゃまぜにしてるんじゃないだろうかという話。静的な設計の例としては、Stack 上に push, pop メソッドを公開して内部データは隠蔽するというような場合。この情報隠蔽は必要で、情報隠蔽をしないと Stack が外部要因によって自身の使命を果たせなくなる。クラス自身が自分の使命を全うするのは自身の責務。では、クラスAが package private のクラスBとCを参照するというケースで package private を public にしたらカプセル化が崩れるのかという点については、これは否となる。クラスとして B や C が外部から静的に参照可能でも全然困らない。重要なのは、動的なオブジェクトとして A が B, C を隠蔽していることである。*1

蛇足となるが、最後の理由はクラス設計の一般論としても重要で、動的視点を用いてアクセス制御を行おうとすると、クラス単体を設計する際にアプリケーション全体の動的設計まで考慮する必要が生じ、しかも未来予知ができるエスパーを雇わない限り保守性が著しく低下してしまう。現状のおいらの指針としては、モジュール設計はクラス設計の前に一通り完了させ*2、モジュール内のクラスはデフォルトで internal とし、実際に外部から呼ばれる必要が生じた段階で public にし、private に関しても実際にそうする必要が生じた際に適用するという感じですかね。

追記

  • CodeComplete にも "Avoid friend classes" っていうのがありますね。C++ の話だけど java でいうところの package private と同様に考えていいんじゃなかろうか。

*1:具体的なモノで例えるなら、Car クラスと Engine クラスがあるとする。世界で初めての車で、Engine はその車用に作られた概念とする。現状では Engine が使いまわされるなんて思いもしないとする。この状態で Car を 運転者に対するモノとしてとらえた場合、Car が Engine をカプセル化し、同時に情報隠蔽が行われるのは妥当。現時点だと Engine は Car のみにしか使われないので、Engine は package private で扱われるのも妥当に見える。しかし、Engine というクラス自体が public であっても全く困らないので、package private にする妥当性は全体的な視点によるものになる。つまり、Engine のクラス設計をする際に、Engine 外(具体的には Car)の事情を踏まえて考えてしまっているということであり、概念レベルでは Engine が Car に依存しているということになってしまう(要は syntactically independent だが semantically dependent ということ)。ソースコード上では単方向の参照だが、概念設計レベルでは双方向参照になってしまっている。これは概念設計上致命的な問題じゃなかろうか。実際に、Engine を列車に適用しようとした途端に package private は破綻する。Car と Engine の例は極論だという反論も出そうなものだが、それなら自社内の自分のプロジェクトだけで使うおそらく一度きりしか使わない特殊な形式のテキストファイルを処理するエンジンだった場合はどうだろうか。このような場合でも、そのエンジンが将来的に拡張されるかどうかは未来予知能力がない限りわからないし、分からないということは package private にする妥当性が無い(むしろ設計レベルでの保守性を低下させる)ということに他ならない。

*2:一通り完了させるというのは完成版ができるという意味ではなく、もちろん後で手戻りする可能性は織り込み済み。そのうえで、トップダウンの視点でモジュール内の主要クラスの洗い出し程度までの詳細化の範囲内のベストエフォートで完成させるという意味。『イテレーションの中で詳細化するから今は大雑把でいいやw』とか考えたら後で死にます。

小規模システム開発の最終成果物とプロセス

小規模システム開発の最終成果物は、以下の2つだけ*1でいい気がするという妄想をメモしてみた。

  • チェックリスト形式の Prerequisites(Problem Definition, Requirements, Architecture)
    • リリース直前にチェックリストを全てチェックすることにより、ソースコードが Prerequisites を満たすことを証明できる。
    • チェックリスト形式にすることにより、Prerequisites とソースが乖離してしまうリスクがなくなる。
    • Prerequisites は何でもアリ。*2
    • 外部仕様をすべて入れる(決定稿でなくても仮置きで入れる) *3
    • 初期 Prerequisites 完成までのコストは、プロジェクト全体のコストのうち、工数的には10-20%、リードタイム的には20-30%を割く感じ。*4
    • 初期 Prerequisites 作成はソースコード開発を含むイテレーションを開始する前にすべて完了する必要がある点に注意。*5
    • 実現可能性検証を Prerequisites 開発フェーズに含めてしまうという手もアリ。*6
  • ソースコード
    • Prerequisites に含まれない全情報は全てソースコードとして記述することにより、資料の分散を防ぐ。
    • リリース直前に全ソースコードのレビュー(一字一句すべて目を通す)をすることにより、全情報の一貫性を保証できる。

*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') // 数字なので正常受付。入力完了音が『ピロリロリン!』と鳴る。
}

考察

なんか歯切れのよい結論が出せないのだけれども、戻り値方式でもクロージャ方式でも、同じ情報を扱えるので数学的には等価だと思われ。しかし、戻り値で対応できるケースをクロージャ渡しで代用するのは結構 error prone かつ仕様変更に弱い気がする。呼び出しが無限ループするバグを埋め込んだりも結構アリがちだし、、、w

機械的な判断基準が欲しいですね。そうすればビルド時に自動で検出できるし、、、。

ここら辺を論じている書籍や論文など知ってる人よろしくです m(_ _)m

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) はどこからでもセットできてしまう点は注意すべきですね。

*1:ミリ秒精度で一致することはありますが。

*2:生成と初期化を分けてもいいし方法は何でもいいですw

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