📱

GoF デザインパターン 入門 ~入門するまで編~

に公開

GoF デザインパターンとは

GoF(Gang of Four)デザインパターンとは、エリック・ガンマ、リチャード・ヘルム、ラルフ・ジョンソン、ジョン・ブリシディーズの4人(Gang of Four)が著書「Design Patterns: Elements of Reusable Object-Oriented Software」で提唱したオブジェクト指向設計の23のデザインパターンです。

パターン

生成に関するパターン (Creational Patterns)

  • Singleton(シングルトン)パターン
  • Factory Method(ファクトリメソッド)パターン
  • Abstract Factory(抽象ファクトリ)パターン
  • Builder(ビルダー)パターン
  • Prototype(プロトタイプ)パターン

構造に関するパターン (Structural Patterns)

  • Adapter(アダプター)パターン
  • Bridge(ブリッジ)パターン
  • Composite(コンポジット)パターン
  • Decorator(デコレーター)パターン
  • Facade(ファサード)パターン
  • Flyweight(フライウェイト)パターン
  • Proxy(プロキシ)パターン

振る舞いに関するパターン (Behavioral Patterns)

  • Chain of Responsibility(責任連鎖)パターン
  • Command(コマンド)パターン
  • Interpreter(インタープリター)パターン
  • Iterator(イテレーター)パターン
  • Mediator(メディエーター)パターン
  • Memento(メメント)パターン
  • Observer(オブザーバー)パターン
  • State(ステート)パターン
  • Strategy(ストラテジー)パターン
  • Template Method(テンプレートメソッド)パターン
  • Visitor(ビジター)パターン

上記を理解するために必要なのが、オブジェクト指向プログラミングの基本、オブジェクト指向の設計原則(SOLID原則)

オブジェクト指向プログラミングの基本

カプセル化

関連するデータとデータを操作するメソッドをひとまとめにして内部の実装を隠蔽すること

class Person {
    private var _age: Int = 0
    val age = _age

    fun add(age: Int) {
        _age += age
    }
}

継承

既存のクラスの特性を新しいクラスに引き継ぎ、拡張できる仕組み

open class Animal {
    open fun makeSound() {
        println("Animal")
    }
}

class Dog: Animal() {
    open fun makeSound() {
        println("Dog")
    }
}

ポリモーフィズム

同じインターフェースを持つ異なるクラスのオブジェクトがそれぞれ異なる振る舞いを可能にする仕組み

interface PaymentMethod {
    fun processPayment(amount: Double): Boolean
}

class CreditCard : PaymentMethod {
    override fun processPayment(amount: Double): Boolean {
        println("クレジットカードで$amountを支払いました")
        return true
    }
}

class PayPal : PaymentMethod {
    override fun processPayment(amount: Double): Boolean {
        println("PayPalで$amountを支払いました")
        return true
    }
}

class BankTransfer : PaymentMethod {
    override fun processPayment(amount: Double): Boolean {
        println("銀行振込で$amountを支払いました")
        return true
    }
}

class PaymentProcessor {
    fun checkout(paymentMethod: PaymentMethod, amount: Double) {
        if (paymentMethod.processPayment(amount)) {
            println("支払い処理が完了しました")
        } else {
            println("支払い処理に失敗しました")
        }
    }
}

抽象化

複雑なシステムを単純化して必要な部分だけを表現する

interface ReportFormatter {
    fun format(data: List<Data>): String
}

class PdfFormatter : ReportFormatter {
    override fun format(data: List<Data>): String {
        // PDF形式にフォーマット
        return "PDF formatted data"
    }
}

class HtmlFormatter : ReportFormatter {
    override fun format(data: List<Data>): String {
        // HTML形式にフォーマット
        return "HTML formatted data"
    }
}

class CsvFormatter : ReportFormatter {
    override fun format(data: List<Data>): String {
        // CSV形式にフォーマット
        return "CSV formatted data"
    }
}

class ReportGenerator {
    fun generateReport(data: List<Data>, formatter: ReportFormatter): String {
        return formatter.format(data)
    }
}

上記のように4つの原則カプセル化、継承、ポリモーフィズム、抽象化は独立したものではなく、密接に関連し合い、相互に補完する概念

オブジェクト指向の設計原則(SOLID原則)

SOLID原則は拡張性、保守性、可読性を高めコードの品質を向上させるための設計思想
以下が正式名称
S → Single Resposibility Priciple 単一責任の原則
O → Open Closed Priciple オープン・クローズドの原則
L → Liscov substitution Priciple リスコフ置換の原則
I → Interface Segregation Priciple インターフェース分離の原則
D → Dependency inversion Principle 依存関係逆転の原則

Single Resposibility Priciple 単一責任の原則

単一責任の原則は役割を一つだけ与えましょうというルール
だからといって、1つの振る舞いだけをするべき、同じロジックを1つにまとめようという話ではない

// 悪い例:複数の責任を持つクラス
class User(val name: String, val email: String) {
    // ユーザー情報の管理
    fun validateEmail(): Boolean {
        return email.contains("@")
    }
    // データベース操作の責任
    fun saveToDatabase() {
        println("Saving user $name to database...")
        // データベースへの保存ロジック
    }
    // メール送信の責任
    fun sendWelcomeEmail() {
        println("Sending welcome email to $email")
        // メール送信ロジック
    }
}

// 良い例:各クラスが単一の責任を持つ

// ユーザー情報の管理のみを担当するdata class
data class User(val name: String, val email: String)

// メール検証を担当
class EmailValidator {
    fun isValid(email: String): Boolean {
        return email.contains("@")
    }
}

// データベース操作を担当
class UserRepository {
    fun save(user: User) {
        println("Saving user ${user.name} to database...")
        // データベースへの保存ロジック
    }
}

// メール送信を担当
class EmailService {
    fun sendWelcomeEmail(user: User) {
        println("Sending welcome email to ${user.email}")
        // メール送信ロジック
    }
}

Open Closed Priciple オープン・クローズドの原則

オープン・クローズドの原則は拡張に対して開かれていて、変更に対して閉じていなければならないという原則
既存コードを変更せず新しい機能を追加できることが理想
悪い例

// 形状の種類を表す列挙型
enum class ShapeType {
    CIRCLE,
    RECTANGLE,
    TRIANGLE
}

// 形状クラス
data class Shape(
    val type: ShapeType,
    val radius: Double = 0.0,
    val width: Double = 0.0,
    val height: Double = 0.0,
    val base: Double = 0.0
)

// 面積を計算するクラス
class AreaCalculator {
    fun calculateArea(shape: Shape): Double {
        return when (shape.type) {
            ShapeType.CIRCLE -> Math.PI * shape.radius * shape.radius
            ShapeType.RECTANGLE -> shape.width * shape.height
            ShapeType.TRIANGLE -> 0.5 * shape.base * shape.height
        }
    }
}

これだとShapeTypeを増やした後AreaCalculatorのロジックの変更をしないといけなくなる

良い例

// 形状の抽象インターフェース
interface Shape {
    fun calculateArea(): Double
}

// 円の実装
data class Circle(val radius: Double) : Shape {
    override fun calculateArea(): Double {
        return Math.PI * radius * radius
    }
}

// 長方形の実装
data class Rectangle(val width: Double, val height: Double) : Shape {
    override fun calculateArea(): Double {
        return width * height
    }
}

// 三角形の実装
data class Triangle(val base: Double, val height: Double) : Shape {
    override fun calculateArea(): Double {
        return 0.5 * base * height
    }
}

// 面積を計算するクラス
class AreaCalculator {
    fun calculateArea(shape: Shape): Double {
        return shape.calculateArea()
    }
}

これだと新しいShapeを継承したクラスを追加するだけでいい、ロジックの変更はない

Liscov substitution Priciple リスコフ置換の原則

リスコフの置換原則はサブタイプはそのスーパータイプの代わりに使用できなければならないという原則
つまり親クラスで可能な操作は小クラスでも同様に動作する必要があるということ

  • サブタイプにしかないメソッド
  • どのメソッドもオーバライドしていない
  • 事前条件の強化
    • 呼び出し側が満たすべき条件
      • サブクラスが親クラスよりもより厳しい制約を課すこと
        • 例)親は1~100の数字を許容する、子は1~50の数字を許容する
        • 親クラスを使用するコードは親クラスの事前条件だけを知っているので、予期せぬエラーにつながる
        • 親クラスなら処理できるのに子クラスでは処理できないのはNG
  • 事後条件の弱化
    • メソッド実行後に「保証される結果や状態」
      • サブクラスが親クラスよりも「弱い保証」しか提供しないこと
        • 例)親はメールアドレスを受け取り、有効でない場合例外を返す、子はメールアドレスを受け取り、有効でない場合falseを返す
        • 無効なメールアドレスでもユーザー登録が成功してしまうかもしれない

この特徴が見られる場合はリスコフ置換の原則に違反していると言える

悪い例

// 親クラス
open class Bird {
    open fun fly(): String {
        return "鳥が飛んでいます"
    }
}

// サブクラス(問題あり)
class Penguin : Bird() {
    override fun fly(): String {
        return "ペンギンは飛べません!" // 予想外の動作
    }
}

// クライアントコード
fun makeBirdFlyHighAndFar(bird: Bird) {
    val flyingStatus = bird.fly()
    // 鳥が飛ぶことを前提としたコード
    println("$flyingStatus → 高く、遠くへ!")
}

良い例

// 基本的な動物インターフェース
interface Animal {
    fun move(): String  // 全ての動物は何らかの方法で移動できる
}

// 飛ぶ能力を持つインターフェース
interface Flyable {
    fun fly(): String  // 飛ぶ能力
}

// 泳ぐ能力を持つインターフェース
interface Swimable {
    fun swim(): String  // 泳ぐ能力
}

// 基本的な鳥クラス(全ての鳥に共通する特性)
open class Bird : Animal {
    override fun move(): String {
        return "鳥が移動しています"
    }
}

// 飛べる鳥のクラス
class FlyingBird : Bird(), Flyable {
    override fun move(): String {
        return fly()  // 飛べる鳥は飛ぶことで移動する
    }
    
    override fun fly(): String {
        return "鳥が飛んでいます"
    }
}

// 泳げる鳥のクラス
class SwimmingBird : Bird(), Swimable {
    override fun move(): String {
        return "鳥が移動しています(主に水中で)"
    }
    
    override fun swim(): String {
        return "鳥が泳いでいます"
    }
}

// ペンギンのクラス - 泳げるが飛べない鳥
class Penguin : Bird(), Swimable {
    override fun move(): String {
        return "ペンギンが歩いたり泳いだりしています"
    }
    
    override fun swim(): String {
        return "ペンギンが上手に泳いでいます"
    }
}

Interface Segregation Priciple インターフェース分離の原則

インターフェース分離の原則はクライアントは自分が使用しないインターフェースに依存すべきではないという原則
悪い例

// 大きすぎるインターフェース
interface Worker {
    fun work()
    fun eat()
    fun sleep()
}

// 実装クラスは使わないメソッドも実装する必要がある
class Robot : Worker {
    override fun work() {
        println("ロボットが作業しています")
    }
    
    override fun eat() {
        // ロボットは食べないので空実装
        println("ロボットは食べられません")
    }
    
    override fun sleep() {
        // ロボットは眠らないので空実装
        println("ロボットは眠りません")
    }
}

class Human : Worker {
    override fun work() {
        println("人間が作業しています")
    }
    
    override fun eat() {
        println("人間が食事をしています")
    }
    
    override fun sleep() {
        println("人間が眠っています")
    }
}

Robotクラスにはeat()とsleep()は不要なのに実装が強制されてしまっている

良い例

// インターフェースを分割する
interface Workable {
    fun work()
}

interface Eatable {
    fun eat()
}

interface Sleepable {
    fun sleep()
}

// ロボットは作業だけできる
class Robot : Workable {
    override fun work() {
        println("ロボットが作業しています")
    }
}

// 人間はすべての機能が必要
class Human : Workable, Eatable, Sleepable {
    override fun work() {
        println("人間が作業しています")
    }
    
    override fun eat() {
        println("人間が食事をしています")
    }
    
    override fun sleep() {
        println("人間が眠っています")
    }
}

D → Dependency inversion Principle 依存関係逆転の原則

依存関係逆転の原則は2つの概念から成る

  • 上位モジュールは下位モジュールに依存すべきではない、両方とも抽象(インターフェース)に依存すべき
  • 抽象は詳細に依存すべきでない、詳細が抽象に依存すべき

悪い例

// 下位レベルのクラス
class MySQLDatabase {
    fun connect() {
        println("MySQLデータベースに接続しました")
    }
    
    fun executeQuery(query: String): List<String> {
        println("クエリを実行: $query")
        return listOf("結果1", "結果2")
    }
}

// 上位レベルのクラス - 具体的な実装に直接依存している
class UserService {
    private val database = MySQLDatabase()
    
    fun getUsers(): List<String> {
        database.connect()
        return database.executeQuery("SELECT * FROM users")
    }
}

この例ではUserServiceがMySQLDatabaseに直接依存してしまっている。
これにより

  • データベースを変更(例:PostgreSQLに変更)する場合、UserServiceも変更する必要がある
  • テストが困難(実際のMySQLデータベースが必要になる)
    再利用性が低下する

良い例

// 抽象(インターフェース)
interface Database {
    fun connect()
    fun executeQuery(query: String): List<String>
}

// 下位レベルのクラス - インターフェースを実装
class MySQLDatabase : Database {
    override fun connect() {
        println("MySQLデータベースに接続しました")
    }
    
    override fun executeQuery(query: String): List<String> {
        println("MySQLでクエリを実行: $query")
        return listOf("結果1", "結果2")
    }
}

class PostgreSQLDatabase : Database {
    override fun connect() {
        println("PostgreSQLデータベースに接続しました")
    }
    
    override fun executeQuery(query: String): List<String> {
        println("PostgreSQLでクエリを実行: $query")
        return listOf("結果1", "結果2")
    }
}

// 上位レベルのクラス - 抽象に依存
class UserService(private val database: Database) {
    fun getUsers(): List<String> {
        database.connect()
        return database.executeQuery("SELECT * FROM users")
    }
}
  • UserServiceは具体的な実装ではなく抽象インターフェースDatabaseに依存
  • 依存性は外部から注入される(依存性注入)
  • データベースの実装を変更してもUserServiceのコードは変更不要
  • テストが容易になる(モックやスタブが使用可能)

依存性の注入について

コンポーネント間の依存関係を外部から提供すること。
これによりコンポーネントの結合度を下げ、テスト容易性や再利用性を高める。
単純な例

// インターフェースを定義
interface UserRepository {
    fun findById(id: Int): User?
}

// 実装クラス
class UserRepositoryImpl : UserRepository {
    override fun findById(id: Int): User? {
        // データベースからユーザーを検索する実装
        return User(id, "User $id")
    }
}

// 依存性をコンストラクタから注入
class UserService(private val userRepository: UserRepository) {
    fun getUser(id: Int): User? {
        return userRepository.findById(id)
    }
}

// 使用例
fun main() {
    val repository = UserRepositoryImpl()
    val userService = UserService(repository)
    val user = userService.getUser(1)
    println(user)
}

しかし通常のAndroid開発ではKoinやHiltといったDIフレームワークが使用される

Discussion