📱

GoF デザインパターン 入門 ~生成に関するパターン (Creational Patterns)編~

に公開

Singleton(シングルトン)パターン

シングルトンパターンは、クラスのインスタンスが1つだけ存在することを保証するデザインパターン
Kotlinだとシンプルに記述できる

object DatabaseConnection {
    init {
        println("Database connection initialized")
    }
    
    fun connect() = println("Connected to database")
}

あるいは

class MyClass {
    companion object {
        // ここに定義されたプロパティとメソッドは、クラス名で直接アクセスできる
        val commonValue = 10
        fun commonFunction() = println("This is a common function")
    }
    
    // インスタンスメソッド
    fun instanceMethod() = println("This is an instance method")
}

objectの場合は以下の使用用途

  • アプリケーション全体で1つだけ必要なサービス
  • ユーティリティ関数の集まり
  • グローバルな状態管理(設定、キャッシュなど)

companion objectの場合は以下の仕様用途

  • クラスのインスタンス生成を制御したい時(次項のFactory Method(ファクトリメソッド)パターン)
  • クラス関連の定数を定義したいとき
  • クラス全体に関連するユーティリティ機能を提供したいとき

Factory Method(ファクトリメソッド)パターン

オブジェクト生成のロジックを専用のメソッドに移譲するデザインパターン
ドメインモデルの整合性を保護するための生成制御や本番/開発/テスト環境で異なる実装を使い分けるなどいったケースで使用できる

class User private constructor(
    val id: String,
    val email: String
) {
    companion object {
        fun of(id: String, email: String): User {
            return User(id, email)
        }
    }
}
interface ApiClient {
    fun fetchData(endpoint: String): String
}

class RealApiClient : ApiClient {
    override fun fetchData(endpoint: String): String {
        // 実際のHTTPリクエストを行う
        return "実際のAPIからのレスポンス"
    }
}

class MockApiClient : ApiClient {
    override fun fetchData(endpoint: String): String {
        // テスト用のモックデータを返す
        return """{"id": 1, "name": "テストデータ"}"""
    }
}

class ApiClientFactory {
    fun createApiClient(isTestMode: Boolean): ApiClient {
        return if (isTestMode) MockApiClient() else RealApiClient()
    }
}

Abstract Factory(抽象ファクトリ)パターン

関連する一連のオブジェクトファミリーをその具体的なクラスに依存せず生成するためのデザインパターン、同じテーマの複数の製品を一貫して作成する場合に使用する。
以下のようにファクトリを抽象にすることで工場の工場を作り、テーマ毎の生成が可能になる
抽象製品
具象製品
抽象ファクトリ
具象ファクトリ

// 抽象製品
interface DataStorage {
    suspend fun saveData(key: String, data: Any)
    suspend fun <T> getData(key: String, clazz: Class<T>): T?
}

interface CacheStorage {
    fun cacheImage(url: String, bitmap: Bitmap)
    fun getCachedImage(url: String): Bitmap?
}

// 抽象ファクトリ
interface StorageFactory {
    fun createDataStorage(context: Context): DataStorage
    fun createCacheStorage(context: Context): CacheStorage
}

// 具象ファクトリ
class LocalStorageFactory : StorageFactory {
    override fun createDataStorage(context: Context) = 
        SharedPrefsStorage(context)
    
    override fun createCacheStorage(context: Context) = 
        InMemoryImageCache()
}

class CloudStorageFactory : StorageFactory {
    override fun createDataStorage(context: Context) = 
        FirebaseStorage()
    
    override fun createCacheStorage(context: Context) = 
        CloudImageCache()
}

Builder(ビルダー)パターン

このパターンはパラメータが多く、一部がオプションなどして複雑なもの、パラメータに対して検証ロジックがある場合などで使われる

data class Person(
    val firstName: String = "",
    val lastName: String = "",
    val age: Int? = null,
    val phone: String? = null,
    val address: String? = null,
    val email: String? = null
)

class PersonBuilder {
    private var person = Person()
    
    fun firstName(firstName: String) = apply { 
        person = person.copy(firstName = firstName) 
    }
    
    fun lastName(lastName: String) = apply { 
        person = person.copy(lastName = lastName) 
    }
    
    fun age(age: Int) = apply { 
        person = person.copy(age = age) 
    }
    
    fun phone(phone: String) = apply { 
        person = person.copy(phone = phone) 
    }
    
    fun address(address: String) = apply { 
        person = person.copy(address = address) 
    }
    
    fun email(email: String) = apply { 
        person = person.copy(email = email) 
    }
    
    fun build(): Person {
        require(person.firstName.isNotBlank()) { "First name is required" }
        require(person.lastName.isNotBlank()) { "Last name is required" }
        return person
    }
}

fun main() {
    val person = PersonBuilder()
        .firstName("John")
        .lastName("Doe")
        .age(30)
        .phone("123-456-7890")
        .address("123 Main St")
        .email("john.doe@example.com")
        .build()
    
    println(person)
}

Prototype(プロトタイプ)パターン

プロトタイプパターンは既存のオブジェクトをコピーして新しいオブジェクトを作成するデザインパターン

data class Document(
    var title: String,
    var content: String,
    val author: String,
    val metadata: MutableMap<String, Any> = mutableMapOf()
) : Prototype {
    
    // 深いコピーを行うクローンメソッド
    override fun clone(): Document {
        // データクラスなので、copyメソッドが自動生成される
        // ただし、MutableMapは参照がコピーされるので、内容を手動でコピーする
        val clonedDocument = this.copy()
        clonedDocument.metadata = metadata.toMutableMap()
        return clonedDocument
    }
}

fun main() {
    // 元となるドキュメントを作成
    val originalDocument = Document(
        title = "プロトタイプパターン",
        content = "これはプロトタイプパターンの説明です。",
        author = "山田太郎",
        metadata = mutableMapOf("created" to "2023-10-20", "version" to 1)
    )
    
    // ドキュメントをクローン
    val clonedDocument = originalDocument.clone()
    
    // クローンを修正
    clonedDocument.title = "改訂:プロトタイプパターン"
    clonedDocument.content = "これはプロトタイプパターンの改訂版説明です。"
    clonedDocument.metadata["version"] = 2
    
    // 結果を表示
    println("元のドキュメント: $originalDocument")
    println("クローンされたドキュメント: $clonedDocument")
}

Kotlinの場合は基本的にdata classにデフォルトであるcopyを使えば良さそう

Discussion