Roomの使い方

依存追加

build.gradle

apply plugin: 'kotlin-kapt'
// ...
dependencies {
    kapt "androidx.room:room-compiler:2.1.0-alpha06"
    implementation "androidx.room:room-runtime:2.1.0-alpha06"
    implementation "androidx.room:room-rxjava2:2.1.0-alpha06"
    androidTestImplementation "androidx.room:room-testing:2.1.0-alpha06"
}

for testing

Migrationのテストを書くなら、以下の設定も追加しておく。

build.gradle

android {
    ...
    defaultConfig {
        ...
        javaCompileOptions {
            annotationProcessorOptions {
                arguments = ["room.schemaLocation": "$projectDir/room_schemas".toString()]
            }
        }
    }
    sourceSets {
        androidTest.assets.srcDirs += files("$projectDir/room_schemas".toString())
    }
}

buildすると、dbのschema情報が、
app/room_schemas/example.com.myapplication.data.db.QiitaDb/{version}.json
のようにバージョン毎に保存されてく。
これもgitにcommitすること。

基本

Entity作成

Table定義にあたる作業.

例えば、以下のモデルをroomのentityとして書き換える場合、

「Qiitaのapiをパースするためのモデル」

@JsonSerializable
data class QiitaItem(
    @Json(name = "title") val title: String,
    @Json(name = "rendered_body") val renderedBody: String,
    @Json(name = "updated_at") val updatedAt: String,
    @Json(name = "user") val user: User
) {
    @JsonSerializable
    data class User(
        @Json(name = "name") val name: String,
        @Json(name = "description") val description: String?,
        @Json(name = "profile_image_url") val profileImageUrl: String
    )
}

こんな感じでannotationを足す。

@Entity(tableName = "qiita_items", primaryKeys = ["id"])
@JsonSerializable
data class QiitaItem(
    @Json(name = "id") @ColumnInfo(name = "id")val id: String,
    @Json(name = "title") @ColumnInfo(name = "title")val title: String,
    @Json(name = "rendered_body") @ColumnInfo(name = "rendered_body") val renderedBody: String,
    @Json(name = "updated_at") @ColumnInfo(name = "updated_at") val updatedAt: String,
    @Json(name = "user") @Embedded(prefix = "user_") val user: User
) {
    @JsonSerializable
    data class User(
        @Json(name = "name") @ColumnInfo(name = "name") val name: String,
        @Json(name = "description") @ColumnInfo(name = "description") val description: String?,
        @Json(name = "profile_image_url") @ColumnInfo(name = "profile_image_url") val profileImageUrl: String
    )
}

入れ子のオブジェクトには、@Embedded(prefix=...)をつける.
上の例だと、qiita_itemstableにuser_name, user_descriptionのようなcolumnが作られるようになる。

@Json(name = "name") @ColumnInfo(name = "name") val name: String
このあたりはannotationなくてもいいのだが、
refactoringなどで気づかず変数名変えてしまった場合に、
影響受けないよう明示的に宣言してる。

DAO作成

Queryを行うメソッドを作っていく。

@Dao
abstract class QiitaDao {
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    abstract fun insert(vararg item: QiitaItem)

    @Query("SELECT * FROM qiita_items ORDER BY updatedAt")
    abstract fun getAll(): LiveData<List<QiitaItem>>

    @Query("SELECT * FROM qiita_items WHERE id = :id")
    abstract fun get(id: String): Single<QiitaItem>

    @Delete
    abstract fun delete(item: QiitaItem)
}

LiveDataだと、dbで該当レコードが変更された時に変更通知してくれる。

Db作成

使うentities(table)を宣言して、daoへのアクセッサメソッドを用意する。

@Database(
    entities = [QiitaItem::class],
    version = 1,
    exportSchema = false
)
abstract class QiitaDb : RoomDatabase() {

    abstract fun qiitaDao(): QiitaDao
    
    // 本当はDIとか使って、singletonになるようdbのインスタンス作成すべき
    companion object {
        fun getInstance(context: Context): QiitaDb = Room.databaseBuilder(
            context.applicationContext,
            QiitaDb::class.java,
            "Qiita_database"
        ).build()
    }
}

これでok. daoインスタンスを介して、目的のdb操作を行えばok.

daoのメソッドは、main thread外で呼ぶの忘れないように。

val dao = QiitaDb.getInstance(context).qiitaDao()
dao.insert(qiitaItem)
dao.get(itemId).subscribe(...)

stetho追加しておくと、こんな感じでdbの中身見れるので開発中便利。

f:id:ishikota:20190423132047p:plain

Paging連携

DAOにこんな戻り値のメソッドを作って、

@Query("SELECT * FROM qiita_items ORDER BY updated_at")
abstract fun getAllPaged(): DataSource.Factory<Int, QiitaItem>

こんな感じでPagedListAdapterにsubmitできるLiveDataが作れる。

class MyViewModel: ViewModel() {
    val roomPagedList: LiveData<PagedList<QiitaItem>> by lazy {
        val factory = dao.getAllPaged()
        LivePagedListBuilder<Int, QiitaItem>(factory, 30).build()
    }
    // ...
}

dbの変更がRecyclerViewに勝手に通知されたりして便利だとか。

Migration

例として、created_at columnを追加してみる。

@Entity(tableName = "qiita_items", primaryKeys = ["id"])
@JsonSerializable
data class QiitaItem(
    @Json(name = "created_at") @ColumnInfo(name = "created_at") val createdAt: String,
    ...
) { ... }

DBのschema変更をしたら以下を行う。

  • DBクラスの@Databaseannotationに書いてるversionを上げて、
  • Migrationクラスを作成して、必要なmigration処理を記述する

Migrationクラスはこんな感じでdbクラスに紐づけて作るといい。

@Database(
    entities = [QiitaItem::class],
    version = 2,
    exportSchema = false
)
abstract class QiitaDb : RoomDatabase() {
    abstract fun qiitaDao(): QiitaDao

    companion object {

        @VisibleForTesting
        val MIGRATION_1_2 = object: Migration(1, 2) {
            override fun migrate(database: SupportSQLiteDatabase) {
                database.execSQL("ALTER TABLE qiita_items ADD COLUMN created_at TEXT")
            }
        }

        fun getInstance(context: Context): QiitaDb = Room.databaseBuilder(
            context.applicationContext,
            QiitaDb::class.java,
            "Qiita_database"
        )
            .addMigrations(MIGRATION_1_2)
            .build()
    }
}

複数migrationがあっても.addMigrations(MIGRATION_1_2, MIGRATION_2_3)のように追加してけば、
roomがversionの差分を計算して、適切なmigration処理をやってくれるよう。

fallbackToDestructiveMigration

migrationの度、古いdbをdropしていいのなら、
dbインスタンスbuild時にfallbackToDestructiveMigrationを呼ぶだけ。

Room.databaseBuilder(
    context.applicationContext,
    QiitaDb::class.java,
    "Qiita_database"
)
    .fallbackToDestructiveMigration()
    .addMigrations(MIGRATION_1_2)
    .build()

RecyclerViewに表示するデータのキャッシュとかあれば、
毎回作り直してokなのでこれをやる。

Test

アプリ立ち上げ直しながら、Migrationちゃんとできてるか確認するのは大変なので、
テストを書きながら挙動を確認した方がいい。

migrationテストの流れは以下、

  1. 古いバージョンのschemaのdbを作成し、レコードをinsertしとく
  2. migrationを実行する
  3. migration後のdbの値を見て、意図どおりになってること確認

この「古いバージョンのschemaのdbを作成」の際に、過去のschema情報が必要なので、
「依存追加のstep」で触れたschema情報を残す設定が必要になる。
dbクラスにつける@DatabaseannotationのexportSchemaをtrueにするのも忘れずに。

テストコードは以下のようになる。

@RunWith(AndroidJUnit4::class)
class MigrationTest {

    private val TEST_DB_NAME = "test-db"

    @get:Rule
    val testHelper = MigrationTestHelper(
        InstrumentationRegistry.getInstrumentation(),
        QiitaDb::class.java.canonicalName,
        FrameworkSQLiteOpenHelperFactory()
    );

    @Test
    fun migrationFrom1To2() {
        // given: setup db with old schema and insert data)
        val oldDb = testHelper.createDatabase(TEST_DB_NAME, 1)
        insertItem(oldDb, "qiita_items", mapOf(
            "id" to "id",
            "title" to "title",
            "rendered_body" to "rendered_body",
            "updated_at" to "updated_at",
            "user_name" to "name",
            "user_description" to "description",
            "user_profile_image_url" to "profile_image_url"
        ))
        oldDb.close()

        // when: execute migration
        testHelper.runMigrationsAndValidate(TEST_DB_NAME, 2, true, MIGRATION_1_2)

        // then: assertion
        val migratedDb = getCurrentSchemaRoomDatabase()
        val item = migratedDb.qiitaDao().get("id").blockingGet()
        assertEquals("description", item.user.description)
    }

    private fun insertItem(db: SupportSQLiteDatabase, tableName: String, data: Map<String, String>) {
        val values = ContentValues().apply {
            for ((k, v) in data) {
                put(k, v)
            }
        }
        db.insert(tableName, SQLiteDatabase.CONFLICT_REPLACE, values)
    }

    private fun getCurrentSchemaRoomDatabase(): QiitaDb {
        val db = Room.databaseBuilder(
            InstrumentationRegistry.getTargetContext(), QiitaDb::class.java, TEST_DB_NAME
        ).build()
        testHelper.closeWhenFinished(db)
        return db
    }
}

参考

github.com

medium.com

CocoaPodsの使い方

セットアップ

gem install cocoapods
installしたらpod setupする。

補足

CocoaPodsでinstallできるライブラリの情報(metadata)は、
以下のgit repositoryで管理されている。
https://github.com/CocoaPods/Specs

Cocoapodsは、~/.cocoapods/repos/以下にこのrepositoryをpullしてきて、
ローカルに落としたライブラリ情報を元にダウンロードを行う。

なので、ローカルの情報が古くて、
RxSwiftの最新版が見つからない!」みたいなことが起きうる。
そんな時は、pod repo updateでローカルの情報を最新にすると上手くいく。
(それでもダメな時は、project rootのPodsディレクトリ以下にある、
ダウンロードした実体も消してみる. ex. rm -rf Pods/SDWebImage

Podfile作成

インストールしたいライブラリは、Podfileというファイルに記述する。
(CocoaPodsを使う) Projectのrootに移動してpod initすると、Podfileの雛形を作ってくれる。

# Uncomment the next line to define a global platform for your project
# platform :ios, '9.0'

target 'SwiftTraining' do
  # Comment the next line if you're not using Swift and don't want to use dynamic frameworks
  use_frameworks!

  # Pods for SwiftTraining

  target 'SwiftTrainingTests' do
    inherit! :search_paths
    # Pods for testing
  end

end

Install

Podfileに追加したいライブラリの名前を次のように追加してpod install

# Uncomment the next line to define a global platform for your project
# platform :ios, '9.0'

target 'SwiftTraining' do
  # Comment the next line if you're not using Swift and don't want to use dynamic frameworks
  use_frameworks!

  # Pods for SwiftTraining
  pod 'Alamofire', '4.8.0`
  pod 'RxSwift', '~> 5'
  pod 'RxCocoa', '~> 5'
  pod 'My-Library' , :path => '{MyLibrary.podspecが置いてあるディレクトリへの絶対path}'  # ローカルの自前ライブラリを参照する場合

  target 'SwiftTrainingTests' do
    inherit! :search_paths
    # Pods for testing
  end

end

testでのみ使いたい依存は、target SwiftTrainingTestsの所に書けばいい。

installに成功したら、
適当なファイルに移動して、import Alamofireなど書いてエラーになってなければok。

XCodeでproject開く時、
{project名}.xcodeprojでなく、{project名}.xcworkspaceファイルを選択しないと、
import できないので気をつける。

pod installpod update

pod installをするとPodfile.lockというファイルが作られる。
ここに、実際にインストールされた各ライブラリのバージョン情報が記録される。

pod installpod updateそれぞれの挙動は次のようになる。

  • pod install
    • Podfile.lockに記述されてるが、ローカルにダウンロードされてないライブラリをダウンロード
    • Podfile.lockに記述されてないが、Podfileに記述されているライブラリを、ローカルにダウンロードして、Podfile.lockを更新
  • pod update [PODNAME]
    • Podfile.lockに書かれたバージョンを無視して、Podfileの記述に従って[PODNAME]のライブラリをダウンロード&Podfile.lockを更新
    • [PODNAME]指定なしだと、Podfileに書かれたライブラリ全てに更新がかかるそう

新しくライブラリを追加するときはPodfileを更新してpod install,
既存のライブラリのバージョンを変えたいときはpod update.
(で、大抵上手くいく)

自作podの作り方

雛形作成

pod lib create [PODNAME]で、[PODNAME]の名前でディレクトリが作られ、そこに雛形を作ってくれる。
雛形を作るにあたり、色々質問されるので答える。

What platform do you want to use?? [ iOS / macOS ]
 > iOS

What language do you want to use?? [ Swift / ObjC ]
 > Swift

Would you like to include a demo application with your library? [ Yes / No ]
 > Yes

Which testing frameworks will you use? [ Quick / None ]
 > None

Would you like to do view based testing? [ Yes / No ]
 > No

demoアプリのセットアップまで、やってくれるのは有り難い。

開発

defaultでは、[PODNAME]/Classes/以下にソースコードを置くようになってるので、
ここにライブラリ用のコードを追加していけば良い。
(apiとして公開したいclass, methodにはpublic修飾子をつけるの忘れないこと)

Exampleディレクトリ以下でpod update [PODNAME]すると、
demoアプリから変更したコードを参照できるようになる。

ライブラリ開発 -> pod update -> demoで意図通り動くか確認。とかすると良い。

アップロードに向けた準備

コードが出来上がったら、podのupload準備。

アップロードするには、podをgit上にuploadしておかないと駄目なのでuploadしておく。
合わせて[PODNAME].podspecファイルに、podのrepositoryのurlを設定する。

s.source = { :git -> "ここをpodをuploadしたgit repositoryのURLにする", :tag => s.version.to_s }

最後に、[PODNAME].podspecs.versionと同じ名前のtagをpodのrepositoryに作っておく。
これをしないとアップロードできない。

ここまで来たら、もろもろ設定できてるか次のコマンドで確認。

pod spec lint [PODNAME].podspec

- NOTE | xcodebuild: error: SWIFT_VERSION '3.2' is unsupported, supported versions are: 4.0, 4.2, 5.0. (in target 'App')
こんなエラーの時はpod spec lint [PODNAME].podspec --swift-version=5.0とかすると上手くいった。

あとはアップロードするだけ。

privateなpodの管理方法

まず、git上でprivateなrepositoryを作る。(名前はSpecsとかが無難)
ここにprivateなライブラリをアップロードすることになる。

まず、pod repo add [適当な名前 ex. MY_SPECS] [上で作ったrepositoryのurl]を実行。
続いて、[PODNAME].podspecがあるディレクトリに移動して、
pod repo push [適当な名前 ex. MY_SPECS] [PODNAME].podspecでアップロードできる。

privateなrepositoryをinstallする時は、Podfileにsourceの設定を追加する。

source '[上で作ったrepositoryのurl]'

use_frameworks!

target 'MyPodTest_Example' do
  pod '[PODNAME]', '0.1.0'
end

Interpreterパターン

概要

対応したい問題を「構文」として表現する。
そして、その構文を理解して、問題の解を出してくれるInterpreterをつくる。

そうすると、
動的に問題を受け取って解決したりできるようになる。

話の流れ

こんな上下左右4つの移動先を選びながら、 ゴールを目指すゲームを作った。

f:id:ishikota:20190504111655p:plain:w300

data class Maze(val row: Int, val column: Int, val map: List<String>,
                var current: Int = map.indexOf("S"))

/**
 * - - -
 * - X -
 * S X G
 */
val sampleMaze = Maze(
    row = 3,
    column = 3,
    map = listOf(
        "-", "-", "-",
        "-", "X", "-",
        "S", "X", "G"
    )
)

/**
 * @param maze maze object to execute the command
 */
abstract class Command(val maze: Maze) {
    /**
     * @return execute command on maze and return it
     */
    abstract fun execute(): Maze
}

class Up(maze: Maze): Command(maze) {
    override fun execute(): Maze {
        val above = if (maze.current - maze.column < 0) maze.current else maze.current - maze.column
        val next = if (maze.map[above] != "X") above else maze.current
        return maze.copy(current = next)
    }
}

class Down(maze: Maze): Command(maze) {
    override fun execute(): Maze {
        val below = if (maze.current + maze.column >= maze.map.size) maze.current else maze.current + maze.column
        val next = if (maze.map[below] != "X") below else maze.current
        return maze.copy(current = next)
    }
}

class Right(maze: Maze): Command(maze) {
    override fun execute(): Maze {
        val right = if ((maze.current + 1) % maze.column == 0) maze.current else maze.current + 1
        val next=  if (maze.map[right] != "X") right else maze.current
        return maze.copy(current = next)
    }
}

class Left(maze: Maze): Command(maze) {
    override fun execute(): Maze {
        val left = if (maze.current % maze.column == 0) maze.current else maze.current - 1
        val next = if (maze.map[left] != "X") left else maze.current
        return maze.copy(current = next)
    }
}

// How to play maze
fun main() {
    var maze = sampleMaze.copy()
    maze = Up(maze).execute()  // maze.current is updated to 3
    maze = Up(maze).execute()  // maze.current is updated to 0
    maze = Right(maze).execute()  // maze.current is updated to 1
    maze = Right(maze).execute()  // maze.current is updated to 2
    maze = Down(maze).execute()  // maze.current is updated to 5
    maze = Down(maze).execute()  // maze.current is updated to 8
    maze.map[maze.current]  // is "G"

}

追加機能として、サーバから迷路の情報と一緒に、
その答え(最短でゴールにたどり着く手順)を返すようにして、
ユーザが答えを確認できるようにしたい。

例えば、上の迷路の答えをこんな文字列で表現。
maze U U R R D D end
(上(Up)に2歩、右(Right)に2歩、下(Down)に2歩)

文法はこんな感じ

<maze> ::= maze <command list>
<command list> ::= <primitive command>* end
<primitive command> ::= U | R | L | D

この文法を解釈して、
Command(上下左右への移動を表現するインスタンス)列に変換してくれる
Interpreterを作る。

コード

/**
 * 構文木解析時の状態を保存するためのクラス。
 *
 * @param commandText string to interpret like "maze U U R R D D end".
 */
class Context(commandText: String, var maze: Maze) {

    private val tokens: MutableList<String> = commandText.split(" ").toMutableList()
    fun currentToken() = tokens[0]

    fun nextToken(): String {
        val next = tokens[0]
        tokens.removeAt(0)
        return next
    }

    fun skipToken(token: String) {
        if (token != currentToken()) {
            throw IllegalStateException("$token is expected but ${currentToken()} is found.")
        } else {
            nextToken()
        }
    }
}

// 構文木のノードのinterface
interface Node {
    fun interpret(context: Context): List<Command>
}

// 以下、構文に沿って3種類のNodeを用意していく。

// Represents <primitive command> ::= U | D | R | L
class PrimitiveCommandNode : Node {
    override fun interpret(context: Context): List<Command> {
        val currentToken = context.currentToken()
        val command = when (context.currentToken()) {
            "U" -> Up(context.maze)
            "D" -> Down(context.maze)
            "R" -> Right(context.maze)
            "L" -> Left(context.maze)
            else -> throw IllegalStateException("primitive token is expected but [$currentToken]")
        }
        context.nextToken()
        context.maze = command.execute()
        return listOf(command)
    }
}

// Represents <command list> ::= <primitive command>* end
class CommandListNode : Node {

    override fun interpret(context: Context): List<Command> {
        val commands = mutableListOf<Command>()
        while (true) {
            if (context.currentToken() == "end") {
                context.skipToken("end")
                break
            } else {
                val command = PrimitiveCommandNode().interpret(context)
                commands.addAll(command)
            }
        }
        return commands
    }
}

// Represents "<maze> ::= maze <command list>"
class MazeNode : Node {
    override fun interpret(context: Context): List<Command> {
        context.skipToken("maze")
        return CommandListNode().interpret(context)
    }
}

// How to play maze
fun main() {
    val commandText = "maze U U R R D D end"
    val context = Context(commandText, sampleMaze)
    val commands = MazeNode().interpret(context)

    var maze = sampleMaze.copy()
    maze = commands[0].execute()  // maze.current is updated to 3
    maze = commands[1].execute()  // maze.current is updated to 0
    maze = commands[2].execute()  // maze.current is updated to 1
    maze = commands[3].execute()  // maze.current is updated to 2
    maze = commands[4].execute()  // maze.current is updated to 5
    maze = commands[5].execute()  // maze.current is updated to 8
    maze.map[maze.current]  // is "G"
}

具体的な使い所

普段のアプリ開発では、
なかなかこのパターンが有用なケースに出会わないのでは。と感じています。

大抵の場合、
こんな複雑なロジックを導入するくらいなら別のアプローチを採用。
となってしまいそうな気がします。

ネットでユースケースについて調べると、
「複雑なsearch query」をASTで表現している例などもありました。

動的にqueryを組み立てられたり、
queryを使う側と、queryのロジックを疎結合に保てて良い。
みたいなメリットがあるとか。

Commandパターン

概要

1つ1つの処理をオブジェクトとして表現することで、
一連の処理を保存したり、あとで再現できたりして便利。

話の流れ

f:id:ishikota:20190504111655p:plain:w300

こんな上下左右4つの移動先を選びながら、
ゴールを目指すゲームを作った。

data class Maze(val row: Int, val column: Int, val map: List<String>)

/**
 * - - -
 * - X -
 * S X G
 */
val sampleMaze = Maze(
    row = 3,
    column = 3,
    map = listOf(
        "-", "-", "-",
        "-", "X", "-",
        "S", "X", "G"
    )
)

fun up(maze: Maze, current: Int): Int {
    val next = if (current - maze.column < 0) current else current - maze.column
    return if (maze.map[next] != "X") next else current
}

fun down(maze: Maze, current: Int): Int {
    val next = if (current + maze.column >= maze.map.size) current else current + maze.column
    return if (maze.map[next] != "X") next else current
}

fun right(maze: Maze, current: Int): Int {
    val next = if ((current + 1) % maze.column == 0) current else current + 1
    return if (maze.map[next] != "X") next else current
}

fun left(maze: Maze, current: Int): Int {
    val next = if (current % maze.column == 0) current else current - 1
    return if (maze.map[next] != "X") next else current
}

fun playMaze() {
    var current = sampleMaze.map.indexOf("S")  // current is 6
    current = up(sampleMaze, current)  // current will be 3
    current = up(sampleMaze, current)  // current will be 0
    current = right(sampleMaze, current)  // current will be 1
    current = right(sampleMaze, current)  // current will be 2
    current = down(sampleMaze, current)  // current will be 5
    current = down(sampleMaze, current)  // current will be 8
    sampleMaze.map[current]  // is "G"
}

この迷路に、プレイヤーの動きを記録して見せ合う「ゴースト機能」を実装したいと思います。

コード

/**
 * @param maze map object to play with
 * @param current current position in map
 */
abstract class MazeCommand(val maze: Maze, val current: Int) {
    /**
     * @return next next position after command is executed
     */
    abstract fun execute(): Int
}

class UpCommand(maze: Maze, current: Int) : MazeCommand(maze, current) {
    override fun execute(): Int {
        val next = if (current - maze.column < 0) current else current - maze.column
        return if (maze.map[next] != "X") next else current
    }
}

class DownCommand(maze: Maze, current: Int) : MazeCommand(maze, current) {
    override fun execute(): Int {
        val next = if (current + maze.column >= maze.map.size) current else current + maze.column
        return if (maze.map[next] != "X") next else current
    }
}

class RightCommand(maze: Maze, current: Int) : MazeCommand(maze, current) {
    override fun execute(): Int {
        val next = if ((current + 1) % maze.column == 0) current else current + 1
        return if (maze.map[next] != "X") next else current
    }
}

class LeftCommand(maze: Maze, current: Int) : MazeCommand(maze, current) {
    override fun execute(): Int {
        val next = if (current % maze.column == 0) current else current - 1
        return if (maze.map[next] != "X") next else current
    }
}

// プレイを記録
fun recordPlay() {
    val commandHistory: MutableList<MazeCommand> = mutableListOf()

    // helper to execute command and save it by one method call
    val performAndSave: (MazeCommand) -> Int = { command ->
        commandHistory.add(command)
        command.execute()
    }

    // play and record commands
    var current = sampleMaze.map.indexOf("S")  // current is 6
    current = performAndSave(UpCommand(sampleMaze, current))  // current will be 3
    current = performAndSave(UpCommand(sampleMaze, current))  // current will be 0
    current = performAndSave(RightCommand(sampleMaze, current)) // current will be 1
    current = performAndSave(RightCommand(sampleMaze, current)) // current will be 2
    current = performAndSave(DownCommand(sampleMaze, current))  // current will be 5
    current = performAndSave(DownCommand(sampleMaze, current))  // current will be 8
    sampleMaze.map[current]  // is "G"

    TODO("upload command history to server")
}

// ゴースト機能
fun restorePlay(commandHistory: List<MazeCommand>) {
    var current = sampleMaze.map.indexOf("S")  // current is 6
    for (command in commandHistory) {
        current = command.execute()
        Log.d("RestorePlay", "current=$current")
    }
    sampleMaze.map[current]  // is "G"
}

Pointは、playを保存できるように、
上下左右への移動をCommandインスタンスとして表現した点です。

実際にplayをするときは、
Commandを使ってゲームを進めていき、
使ったCommandを保存しておきます。
あとで、保存したCommandを実行すれば、playを復元することができます。

具体的な使い所

ユーザのアクションを記録しておいて、
あとで復元したい場合はこのパターンの出番かと思います。

アプリのユーザのアクションを全てCommandとして記録できれば、
ヘビーユーザの行動分析、エミュレートなど色々できそうな気もします。

上の例は、保存したCommandが、
何回実行しても同じ結果が得られるよう(冪等)に気をつけて作りました。
いざ使うとなると、保存するCommandにどの情報を持たせるのか。とか、
色々悩みpointがありそうだと思いました。

Proxyパターン

概要

あるクラスと同じinterfaceのProxyクラスを用意することで、
元のクラスを修正せず、同じinterfaceのまま、
機能の追加ができて便利

話の流れ

画像をダウンロードできる、こんなクラスを作った。

class ImageDownloader {
    fun downloadImage(url: String): Bitmap? = TODO("省略")
}

// How to use
fun main() {
    val imageDownloader = ImageDownloader()
    val image1 = imageDownloader.downloadImage("https://image1.png")
    val image2 = imageDownloader.downloadImage("https://image1.png")
    // download is executed twice
}

このクラスに、「urlをkeyに画像をキャッシュする」機能を追加したい。

キャッシュ機能はオプショナルな機能なため、
元のクラスとは切り離してこのロジックを実装したい。

コード

// 元のクラス(ImageDownloader)とProxy(CacheImageDownloader)を
// 同一視するためのinterface
interface ImageDownloadable {
    fun downloadImage(url: String): Bitmap?
}

// 元のクラス
class ImageDownloader: ImageDownloadable {
    override fun downloadImage(url: String): Bitmap? = TODO("省略")
}

// キャッシュ機能が実装されたProxyクラス
class CacheImageDownloader(val real: ImageDownloader): ImageDownloadable {

    // urlをkeyに画像をキャッシュするためのHashMap
    private val pool: MutableMap<String, Bitmap> = mutableMapOf()

    override fun downloadImage(url: String): Bitmap? {
        return if (pool.containsKey(url)) {
            pool[url]
        } else {
            val image = real.downloadImage(url)  // ベースとなる機能は元のクラスが担当(移譲)
            if (image != null) {
                pool[url] = image  // urlをkeyに画像をキャッシュ
            }
            image
        }
    }
}

// How to use
fun main() {
    val imageDownloader = CacheImageDownloader(ImageDownloader())
    val image1 = imageDownloader.downloadImage("https://image1.png")
    val image2 = imageDownloader.downloadImage("https://image1.png")
    // download is executed only once
}

Pointは、元のクラス(ImageDownloader)とProxy(CacheImageDownloader)が、
同じinterface(ImageDownloadable)を実装している点です。

このおかげで、使う側は元のクラスと同じ呼び出し方で、
追加機能にアクセスできています。

Gofのパターンでは、各クラスの役割を次のように呼んでいます。

  • Subject: 元のクラスとProxyに実装するinterface.上の例のImageDownloadable
  • RealSubject: 元のクラス.上の例のImageDownloader
  • Proxy: 上の例のCacheImageDownloader

使い所

いろんなユースケースが考えられそうです。

  • アクセス制限を行うProxy
  • 初期化に時間のかかるオブジェクトが特定の機能でしか使わないなら、その機能のアクセスまでオブジェクトの初期化を遅らせるProxy
  • キャッシュ機能を追加するProxy
  • logging機能を追加するProxy

(あれ、Decoratorパターンと何が違うんだっけ。。。)

Flyweightパターン

概要

動的に変わるpropertyを持たないオブジェクトなら、
1度作ったオブジェクトをいろんな所で共有することで、
メモリとか節約できるかも。

話の流れ

JavaのIntegerクラスでflyweightパターンが使われています。

Integerクラスとは、primitiveのintをオブジェクトとして扱えるようwrapしたクラスです。
初期化時に、wrapするintの値を受け取り、finalな変数に保存しておきます。
インスタンスは、wrapした整数値を別の型へ変換するmethodなどを提供しています。

public final class Integer ... {
    private final int value;
    public Integer(int value) {
        this.value = value;
    }

    public float floatValue() { ... }
    public String toString() { ... }
}

このIntegerクラスはfieldを1つしか持ちません。
それもfinalで宣言されているので、後から値が変わることもありません。

同じ引数で初期化したインスタンスは、
全く同じ機能を提供することになるので、
毎回インスタンスを作ってたらメモリがもったいないです。

そこで、(頻繁にインスタンスが作られそうな)-128から127の値に対応するインスタンスは、
あらかじめ作っておいて、そのキャッシュを共有する仕様になっています。 https://docs.oracle.com/javase/jp/8/docs/api/java/lang/Integer.html#valueOf-int-

コード

Integerクラスのインスタンスを作る時、
new Integer(127)でなくInteger.valueOf(127)のようにするのが勧められていますね。
これは、valueOfの方だと、上で説明したキャッシュロジックが有効になるためだったのです。

    public static Integer valueOf(int i) {
        // IntegerCache.low=-128, IntegerCache.high=127にdefault設定されてる
        if (i >= IntegerCache.low && i <= IntegerCache.high)
            return IntegerCache.cache[i + (-IntegerCache.low)];
        return new Integer(i);
    }

http://hg.openjdk.java.net/jdk8/jdk8/jdk/file/687fd7c7986d/src/share/classes/java/lang/Integer.java#l829

Gofで紹介してるFlyweightパターンは、次の3つの役割で構成されています。

Gofのパターンでは、Flyweightのinterfaceを作ってますが、今回のケースでは使っていません。

具体的な使い所

GUIの描画周りなど、パフォーマンスが重視される場面では、
自然と使っているパターンかもしれません。

気をつけるのは、
「共有するインスタンスに(変更可能な)状態を持たせないこと」
でしょうか。

インスタンスには不変(intrinsic)な値だけ持たせておいて、
可変(extrinsic)な値はメソッド呼び出し時に動的に渡します。

Stateパターン

概要

// 処理1は状態によって挙動が変わる
if (状態A) {
    // 処理1A
} else if (状態B) {
    // 処理1B
} else if (状態C) {
    // 処理1C
}

// 処理2は状態によって挙動が変わる
if (状態A) {
    // 処理2A
} else if (状態B) {
    // 処理2B
} else if (状態C) {
    // 処理2C
}

こんな条件分岐がクラス内にたくさん見つかったら、

interface State {
    fun 処理1()
    fun 処理2()
}

class 状態A: State {
    override fun 処理1() { 処理1A }
    override fun 処理2() { 処理2A }
}

class 状態B: State {
    override fun 処理1() { 処理1B }
    override fun 処理2() { 処理2B }
}

class 状態C: State {
    override fun 処理1() { 処理1C }
    override fun 処理2() { 処理2C }
}

var currentState = StateA()
currentState.処理1()  // 処理1A
currentState = StateC()
currentState.処理2()  // 処理2C

こんな風に状態をクラスで表現すると、条件分岐が消せていいかも。

話の流れ

スーパーに置くサイネージ端末のアプリを作っている。
2種類の動画(ex. 商品PR動画と広告動画)を交互に流し続ける。

流している動画の種類に応じて、以下のように挙動を変えたい。

layout 画面タップ時の挙動 ログ
商品PR動画 枠あり 商品詳細popupを表示 PR動画用のtableへログを送る
広告動画 枠なし(全画面表示) クーポンダウンロード用QRコード表示 広告動画用のtableへログを送る

コードを書いたらこんな感じに。

// 再生する動画を表すクラス。商品PR動画と広告動画の2種類。
sealed class Video(val id: Int, val videoUrl: String) {
    class ProductPRVideo(id: Int, videoUrl: String, val popupMessage: String): Video(id, videoUrl)
    class AdVideo(id: Int, videoUrl: String, val couponLink: String): Video(id, videoUrl)
}
interface VideoPlayer {
    fun playVideo(videoUrl: String, onVideoFinished: () -> Unit)
}

class VideoPlayerActivity : AppCompatActivity() {

    private val videoPlayer: VideoPlayer = // ...
    private val prVideo = Video.ProductPRVideo(id = 1, videoUrl = "https://...", popupMessage = "買ってね!!")
    private val adVideo = Video.AdVideo(id = 2, videoUrl = "https://...", couponLink = "https://...")

    private var currentVideo: Video = prVideo

    // Setup
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_video_player)

        // 「画面タップ時の挙動」は、再生してる動画の種類によって変える
        videoview.setOnClickListener {
            val current = currentVideo
            when(current) {
                is Video.ProductPRVideo -> {
                    showPopup(current)
                }
                is Video.AdVideo -> {
                    showCouponQR(current)
                }
            }
        }

        // 商品PR動画 -> 広告動画 -> 商品PR動画 -> ...とリピート
        playVideo(currentVideo)
    }

    private fun playVideo(video: Video) {
        currentVideo = video

        // layoutを変える
        when(currentVideo) {
            is Video.ProductPRVideo -> {
                videoview.showFrame()
            }
            is Video.AdVideo -> {
                videoview.hideFrame()
            }
        }

        videoPlayer.playVideo(video.videoUrl, onVideoFinished = {
            // ログを送る
            when(currentVideo) {
                is Video.ProductPRVideo -> {
                    sendPRVideoLog(videoId=currentVideo.id)
                }
                is Video.AdVideo -> {
                    sendAdVideoLog(videoId=currentVideo.id)
                }
            }

            // 動画の種類を切り替えて、次の動画再生を始める
            val nextVideo = if (video is Video.ProductPRVideo) adVideo else prVideo
            playVideo(nextVideo)
        })
    }

「画面タップ時の挙動」、「layoutを変える」、「ログを送る」それぞれの箇所で、
再生してる動画の種類による条件分岐(switch)を行っています。

条件分岐がたくさんあってコードが追いづらいですね。
今後、ここに機能を追加していくのも大変そうです。

そこでStateパターンを使ってみることにします。

コード

// 状態(動画の種類)によって挙動が変わる処理をinterfaceに切り出して、
// 状態毎に挙動を定義していく。
interface State {
    fun setupLayout(videoView: VideoView)
    fun onScreenTapped()
    fun sendLog(videoId: Int)
}

// 商品PR動画の挙動を定義
class ProductPRVideoState(val prVideo: Video.ProductPRVideo) : State {

    // 枠あり
    override fun setupLayout(videoView: VideoView) {
        videoView.showFrame()
    }

    // 商品詳細popupを表示
    override fun onScreenTapped() {
        showPopup(prVideo)
    }

    // PR動画用のtableへログを送る
    override fun sendLog(videoId: Int) {
        sendPRVideoLog(videoId)
    }

    private fun showPopup(video: Video.ProductPRVideo) { TODO("省略") }
    private fun sendPRVideoLog(videoId: Int) { TODO("省略") }
}

// 広告動画の挙動を定義
class AdVideoState(val adVideo: Video.AdVideo) : State {

    // 枠なし(全画面表示)
    override fun setupLayout(videoView: VideoView) {
        videoView.hideFrame()
    }

    // クーポンダウンロード用QRコード表示
    override fun onScreenTapped() {
        showCouponQR(adVideo.couponLink)
    }

    // 広告動画用のtableへログを送る
    override fun sendLog(videoId: Int) {
        sendAdVideoLog(videoId)
    }

    private fun showCouponQR(qrLink: String) { TODO("省略") }
    private fun sendAdVideoLog(videoId: Int) { TODO("省略") }
}

// 現在の状態を保持/状態遷移を担当するクラス
class Context(firstVideo: Video) {

    var currentVideo: Video = firstVideo
        private set
    private var currentState: State = buildState(firstVideo)

    // 新しいVideoをセットすることで,ContextのStateが切り替わり、
    // onScreenTappedなどの挙動(呼び出し先)が切り替わる。
    fun setCurrentVideo(video: Video) {
        currentVideo = video
        currentState = buildState(video)
    }

    fun onScreenTapped() {
        currentState.onScreenTapped()
    }

    fun setupLayout(videoView: VideoView) {
        currentState.setupLayout(videoView)
    }

    fun sendLog(videoId: Int) {
        currentState.sendLog(videoId)
    }

    private fun buildState(video: Video) = when (video) {
        is Video.ProductPRVideo -> ProductPRVideoState(video)
        is Video.AdVideo -> AdVideoState(video)
    }
}

class VideoPlayerActivity : AppCompatActivity() {

    private val videoPlayer: VideoPlayer = VideoPlayer()
    private val prVideo = Video.ProductPRVideo(id = 1, videoUrl = "https://...", popupMessage = "買ってね!!")
    private val adVideo = Video.AdVideo(id = 2, videoUrl = "https://...", couponLink = "https://...")

    private val context: Context = Context(firstVideo = prVideo)

    // Setup
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_video_player)

        // 画面タップされた時の挙動は、再生してる動画の種類によって変える
        videoview.setOnClickListener {
            context.onScreenTapped()
        }

        // 商品PR動画 -> 広告動画 -> 商品PR動画 -> ...とリピート
        playVideo(context.currentVideo)
    }

    private fun playVideo(context: Context) {
        // layoutを変える
        context.setupLayout(videoview)
        videoPlayer.playVideo(context.currentVideo.videoUrl, onVideoFinished = {
            // ログを送る
            context.sendLog(context.currentVideo.id)

            // 動画の種類を切り替えて、次の動画再生を始める
            val nextVideo = if (context.currentVideo is Video.ProductPRVideo) adVideo else prVideo
            context.setCurrentVideo(nextVideo)
            playVideo(context)
        })
    }
}

条件分岐を行っていた処理は全てContextクラスに依頼するようになりました。

Contextクラスは、内部で現在のStateを保持しているので、
現在のStateに応じた適切な処理を行ってくれます。

このおかげで、VideoPlayerActivityから条件分岐を消すことができました。
また、AdVideoStateを見れば、広告動画固有の挙動を一望できたりと、
以前よりもコードが整理されたように見えます。

(ちなみに、今回はContextクラスが状態遷移を担当しましたが、
状態遷移をどのクラスが担当するかは特に決められてないようです。)

具体的な使い所

if (状態A) {
    ...
} else if (状態B) {
    ...
} else if (状態C) {
    ...
}

こういう形の条件分岐がクラス内にたくさん見つかったら、
Stateパターンによって見通しをよくできるかもしれません。

上手く使えるとif文を消せて気持ちいいのですが、
いざ使ってみようとするとStateのinterfaceを上手くきれず悩んだりします。