LoginSignup
27
42

More than 1 year has passed since last update.

Swiftで覚えるデザインパターン

Last updated at Posted at 2021-07-13

はじめに

デザインパターンについて、下記の書籍を用いて勉強したのですが、普段はSwiftでのコーディングをするので実際にクラス図を書いたり実装したりして学ぼうと思いアウトプットしてみました。

Head Firstデザインパターン ―頭とからだで覚えるデザインパターンの基本


ボリュームがあるので複数回に分けて記事を投稿予定です。

  1. Swiftで覚えるデザインパターン ← 今ココ
    1. Strategy
    2. Observer
    3. Decorator
    4. Factory Method
    5. Abstract Factory
    6. Singleton
  2. Swiftで覚えるデザインパターンその2 2021/08/20公開
    1. Command
    2. Adaptor
    3. Facade
    4. Template Method
  3. Swiftで覚えるデザインパターンその32021/09/09公開
    1. Iterator
    2. Composite
    3. State
    4. Compound

Strategyパターン

概要

一連の振る舞いを一連のアルゴリズムとして定義し、それぞれをカプセル化して交換できる状態にする
こうすることで、使用する側に意識をさせることなく簡単に戦略(Strategy)を切り替えられる。

試しに作ったもの

画面を開くと自動的にポケモンたちに飛行アクションをさせるというものを作りました。
飛行アクションをする前に、各ポケモンに飛行タイプを定義させます。

今回は下記タイプを用意しました。
- 通常飛行
- 飛行できない
- 風船

クラス図

PokemonクラスはFlyBehavior(飛ぶ振る舞い)を持っている。
Pokemonクラスを継承したPikachuCharizard(リザードン)に実際にflyを実行させるためには、setFlyTypeで飛行タイプをセットしてそれによってどのアルゴリズムを使うかを決める。

飛行タイプを決めるのは呼ぶ側で、今回で言うとStrategyViewControllerが2体のポケモンを準備して飛ばそうとしています。

Strategy.png

実装

必要そうな部分を抜粋(以降のパターンの実装も同様です。)
実装は参考書籍のものに極力寄せています。

アルゴリズム
protocol FlyBehavior {
    /// 飛ぶ
    static func fly()
}

class FlyNormal: FlyBehavior {
    static func fly() {
        print("ふつーな飛行")
    }
}
// FlyNoWay, FlyBalloonも同様
Pokemon.swift
class Pokemon {
    enum FlyType {
        case normal
        case noFly
        case balloon
    }
    /// 飛行タイプ
    private var flyType: FlyType?

    /// 飛行タイプを設定
    /// - Parameter type: FlyType
    func setFlyType(with type: FlyType = .normal) {
        flyType = type
    }

    /// 飛行アクション
    func fly() {
        guard let type = flyType else {
            fatalError("飛行タイプが設定されていません")
        }
        switch type {
        case .normal:
            return FlyNormal.fly()
        case .noFly:
            return FlyNoWay.fly()
        case .balloon:
            return FlyBalloon.fly()
        }
    }
}
StrategyViewController.swift
class StrategyViewController: UIViewController {    
    let pikachu = Pikachu()
    let charizard = Charizard()

    override func viewDidLoad() {
        super.viewDidLoad()

        setup()
        fly()
    }

    /// それぞれのポケモンに飛行タイプを指定
    func setup() {
        pikachu.setFlyType(with: .noFly)
//        pikachu.setFlyType(with: .balloon)
        charizard.setFlyType()
    }

    /// 飛行
    func fly() { 
        pikachu.fly() // 「飛べないよぉ」(.noFlyではなく、.balloonにすると「ふわふわ飛ぶよ」みたいに変わる)
        charizard.fly() // 「ふつーな飛行」
    }
}

飛行タイプを変えるだけでflyの処理を簡単に切り替えられますね。
StrategyViewController.swiftsetup()内のコメントアウト部分

Observerパターン

概要

あるオブジェクトの状態が変わったことを、依存している全てのオブジェクトに自動的に通知して更新する。

値を保持するSubjectとそれを監視(購読)するObserverから成る。
SubjectとObjectの依存関係は1:Nである。

試しに作ったもの

今回は購読って響きからサブスクサービスのNetflix的なのを想定しました。
「New episode」を押したらランダムな作品の第n話が発行され、その時Subjectを購読しているユーザに発行した値を通知するような画面を作ってみました。

ObserverSampleApp.png

わかりやすいように、「New episode」を押したら「New Episode Released!」と表示されるようにしています。
ObserverはNetflixに登録(Register)していないと購読できず、逆に購読をやめる(Remove)と購読解除できます。

デバッグ画像は、下記流れの各状態で新しいエピソードを発行したときの結果です。

  1. 誰も購読していない
  2. Johnだけ購読を始めた
  3. Paulも購読を始めた
  4. Johnが購読解除した

ObservePrint.png

発行された値をちゃんと登録しているオブザーバーのみが検知できていると思います。

クラス図

SubjectObserverのオブジェクトは疎結合なので、互いにやりとりできますが中身を知っておくのが不要になるのがポイントのようです。

疎結合によって下記恩恵が得られます。

  • Subject, Observer共に、変更しても影響を及ぼさない
  • Subject, Observer共に再利用ができる
    • HuluSubjectを作ることも考えられる
    • Taroがオブザーバとして追加できることも考えられる
Observer.png

実装

Subject周り
protocol Subject: AnyObject {
    /// オブザーバーの登録
    func add(observer: Observer)
    /// オブザーバーの削除
    func remove(observer: Observer)
    /// 新しい値の発行
    func notify()
}

class NetfilxSubject: Subject {
    /// 購読者のリスト
    var observers: [Observer] = []
    /// 作品名
    private var title: String?
    /// 第何話
    private var episode: Int?

    func add(observer: Observer) {
        observers.append(observer)
    }

    func remove(observer: Observer) {
        guard let index = observers.firstIndex(where: {$0.name == observer.name}) else {
            return
        }
        observers.remove(at: index)
    }

    func notify() {
        guard
            let title = title,
            let episode = episode else {
            return
        }

        for observer in observers {
            observer.update(with: title, episode: episode)
        }
    }

    // 作品のデータを用意
    func setNewData(with title: String, episode: Int) {
        self.title = title
        self.episode = episode

        // オブザーバーに通知
        notify()
    }
}
Observer周り
protocol Observer: AnyObject {
    /// 名前
    var name: String {get}
    /// 値の更新
    /// - Parameter title: タイトル
    /// - Parameter episode: エピソード(話数)
    func update(with title: String, episode: Int)
}

// Paulも同様
class John: Observer {
    var name = "John"

    init(with subject: Subject) {
        subject.add(observer: self)
    }

    func update(with title: String, episode: Int) {
        print("John watched: \(title), episode: \(episode)")
    }
}
ObserverViewController
class ObserverViewController: UIViewController {
    private let subject = NetfilxSubject()
    private var john: John?
    private var paul: Paul?
    /// 作品集
    private let titleMock = ["全裸監督", "ストレンジャー・シングス", "愛の不時着", "梨泰院クラス"]

    // New Episodeを押したとき
    @IBAction func didTapUpdateEpisode(_ sender: Any) {
        print("New Episode Released!")

        // 今回は適当にエピソードを発行する
        let title = titleMock.randomElement() ?? "No Data"
        let episode = Int.random(in: 1..<10)
        subject.setNewData(with: title, episode: episode)
    }

    // John側のボタンを押したとき(Paulでも同様)
    @IBAction func didTapJohnRegister(_ sender: Any) {
        john = John(with: subject)
    }

    @IBAction func didTapJohnRemove(_ sender: Any) {
        guard let observer = john else {
            return
        }
        subject.remove(observer: observer)
    }
}

Decoratorパターン

概要

既存のオブジェクトに付加的な責務を動的に付与することで機能を拡張する。
単純な継承を行うよりも比較的柔軟な機能拡張を行える。

Decoratoration(装飾する)からも想像できるように、オブジェクトの機能を総称していくパターンのようですね。

試しに作ったもの

ラーメンをベースに、トッピングを選んで会計をする画面を作りました。

ベースのラーメンが600円、煮卵80円、海苔50円、チャーシュー100円で作っています。

DecoratorSampleApp.png

画像のように、煮卵、チャーシューを選択すると、600+80+100の780円になります。
会計ボタンを押した結果が以下です。
DecoratePrint.png

クラス図

Menuが俗にComponentと呼ばれるベースとなるクラスになる。
Menuの具象クラスとしてRamenクラスがある。

Decoratorがラーメンの装飾をするためのトッピングを用意するクラスで、SeasonedEgg(煮卵)Seaweed(海苔)RoasePork(チャーシュー)を今回用意しています。
これらのクラスがMenuの状態を拡張することで品目や料金を更新することができます。

DecoratorはMenu(Component)と一緒にオブジェクトを構成するということで、ラーメンとトッピングの型を一致させるという従来の継承の使い方をして、かつ同じ振る舞いを取得するようにしています。(コンポジション/集約)
これにより、ラーメンとトッピングの組み合わせを柔軟に設定することができるようになります。

Decorator.png

実装

Component周り
class Menu {
    /// 品目
    var description: String = "メニュー"

    /// 品目を取得する
    /// - Returns: String
    func getDescription() -> String {
        return description
    }

    /// 料金を取得する
    func cost() -> Int {
        fatalError("注文するものが選択されていません")
    }
}

class Ramen: Menu {
    override init() {
        super.init()

        description = "ラーメン"
    }

    // ラーメン代600円
    override func cost() -> Int {
        return 600
    }
}
Decorator周り
class Decorator: Menu {
    /// メニュー
    var menu: Menu

    init(_ menu: Menu) {
        self.menu = menu
    }
}

/// 煮卵(海苔、チャーシューも同様)
class SeasonedEgg: Decorator {
    override func getDescription() -> String {
        // 品目に煮卵を追加
        return menu.getDescription() + "、煮卵"
    }

    override func cost() -> Int {
        return menu.cost() + 80
    }
}
DecoratorViewController
class DecoratorViewController: UIViewController {
    // 省略

    // トッピングに選んだものだけ計算
    func caclulate() {
        var menu: Menu = Ramen()
        if seasonedEggSwitch.isOn {
            menu = SeasonedEgg(menu)
        }

        if seaweedSwitch.isOn {
            menu = Seaweed(menu)
        }

        if roastPorkSwitch.isOn {
            menu = RoastPork(menu)
        }
        print(menu.getDescription() + "で合計は\(menu.cost())円になります。")
    }
}

Factory Methodパターン

概要

オブジェクト作成のためのインターフェース(Swiftの場合はプロトコル)を定義し、サブクラスにどのクラスをインスタンス化させるかを決めさせる

これによって、インスタンス化させる処理をカプセル化させられる。

試しに作ったもの

好きなボタンを押すとそのお好み焼きを作成するような画面を作成しました。
スイッチの向きによって広島風か関西風かを選べます。

FactorySampleApp.png

スイッチが右にある場合(Onの場合)、関西風〜〜お好み焼きが出来上がります。
Offにすると、広島風になります。
FactoryePrint.png FactoryePrint2.png

クラス図

Factory MethodパターンではOkonomiyakiProductOkonomiyakiStoreCreatorの役割を担っている。

Creatorがオブジェクト作成のためのメソッド(ファクトリメソッド)を定義し、そのサブクラスがファクトリメソッドを実装してオブジェクトを作成する。

Productは、Productを実装したオブジェクト(それぞれの具体的なお好み焼き)をCreatorが呼び出すためのインターフェース(プロトコル)である。
prepare()やmix()等の処理は基本的には変わらないので、protocol extensionで実装して共通処理として扱う。

FactoryMethod.png

実装

Creator周り
protocol OkonomiyakiStore {
    /// お好み焼きを作成
    func create(type: Topping) -> Okonomiyaki
}

extension OkonomiyakiStore {
    /// 注文されたお好み焼きを焼き上げる
    func order(type: Topping) -> Okonomiyaki {
        let okonomiyaki = create(type: type)

        okonomiyaki.prepare()
        okonomiyaki.mix()
        okonomiyaki.bake()

        return okonomiyaki
    }
}

/// 関西風お好み焼き専門店(広島風も同様)
class KansaiOkonimiyakiStore: OkonomiyakiStore {
    func create(type: Topping) -> Okonomiyaki {
        switch type {
        case .mix:
            return KansaiMixedOkonomiyaki()
        case .cheese:
            return KansaiCheeseOkonomiyaki()
        case .mentaiko:
            return KansaiMentaikoOkonomiyaki()
        }
    }
}
Priduct周り
protocol Okonomiyaki {
    /// 品目
    var name: String { get set }
}

extension Okonomiyaki {
    /// mixやbakeも同様の処理
    func prepare() {
        print("下処理")
    }

    func getName() -> String {
        return name
    }
}

/// トッピングの種類
enum Topping {
    case mix
    case cheese
    case mentaiko
}

/// ミックスお好み焼き〜関西風〜(他のお好み焼きも同様に作成する)
class KansaiMixedOkonomiyaki: Okonomiyaki {
    var name: String = "ミックスお好み焼き〜関西風〜"
}
FactoryViewController.swift
class FactoryViewController: UIViewController {
    /// お好み焼き店舗
    var okonomiyakiStore: OkonomiyakiStore?
    /// お好み焼きスイッチ
    @IBOutlet private weak var okonomiyakiStyleSwitch: UISwitch!

    override func viewDidLoad() {
        super.viewDidLoad()

        okonomiyakiStore = KansaiOkonimiyakiStore()
    }

    @IBAction func didSwitchOkonomiyakiStyle(_ sender: Any) {
        okonomiyakiStore = okonomiyakiStyleSwitch.isOn ? KansaiOkonimiyakiStore() : HiroshimaOkonomiyakiStore()
    }

    // チーズや明太子のボタンも同様
    @IBAction func didTapMixedButton(_ sender: Any) {
        order(with: .mix)
    }

    /// お好み焼きを注文
    func order(with topping: Topping) {
        guard let store = okonomiyakiStore else {
            return
        }
        let okonomiyaki = store.order(type: topping)
        print(okonomiyaki.getName() + "の完成です!")
    }
}

Abstract Factoryパターン

概要

具象クラス(オブジェクト)を指定せずに一連の関連オブジェクトや依存オブジェクトを作成するためのインターフェースを提供する。

クライアントが実際に作成される具体的な製品を知ることなく一連の製品を作成できる。

※作ったものは、Factory Methodパターンと同じものです。

クラス図

赤い部分がFactoryMethodパターンからの差分です。

AbstractFactoryが一連の食材オブジェクトSauceを作成する役割を担い、具体的な処理は具象ファクトリで実装して食材を獲得します。
お好み焼きを作る際、FactoryMethodパターンでは地域別にクラスを作成していました。
AbstractFactoryパターンでは食材の種類が異なり、作り方は共通なものとして実装するようなパターンにします。

AbstractFactory.png

実装

Sauceとその実装しているクラスは定義しただけなので省略します。
他の食材を定義する場合も同様にできます。

呼び出すViewControllerも基本的には変わりません。

IngredientFactory周り
/// 食材取得用Factory
protocol OkonomiyakiIngredientFactory: AnyObject {
    /// お好みソースを取得
    func getSauce() -> Sauce

    // 他の食材を取得したい場合は別途定義
}

/// 関西風お好み焼き食材Factory(広島風も同様)
final class KansaiOkonomiyakiIngredientFactory: OkonomiyakiIngredientFactory {
    func getSauce() -> Sauce {
        return KansaiSauce()
    }

    /*
     他の食材も同様に実装
     */
}
Creator周り
// protocolは基本的に変わりません

/// 関西風お好み焼き専門店(広島風も同様)
class KansaiOkonimiyakiStore: OkonomiyakiStore {
    private let factory = KansaiOkonomiyakiIngredientFactory()

    func create(type: Topping) -> Okonomiyaki {
        var okonomiyaki: Okonomiyaki
        switch type {
        case .mix:
            okonomiyaki = MixedOkonomiyaki(factory: factory)
        case .cheese:
            okonomiyaki = CheeseOkonomiyaki(factory: factory)
        case .mentaiko:
            okonomiyaki = MentaikoOkonomiyaki(factory: factory)
        }

        okonomiyaki.name += "〜関西風〜"

        return okonomiyaki
    }
}
Product周り
/// お好み焼き
protocol Okonomiyaki {
    /// 品目
    var name: String { get set }

    // 以下、Abstract Factory用
    /// ソース
    var sauce: Sauce? { get set }
    /// 食材を取得する
    func getFoodstuff()
}

extension Okonomiyaki {
    // 省略
}

// 他のお好み焼きも同様
class MixedOkonomiyaki: Okonomiyaki2 {
    var name: String = "ミックスお好み焼き"
    var sauce: Sauce?
    private var factory: OkonomiyakiIngredientFactory

    init(factory: OkonomiyakiIngredientFactory) {
        self.factory = factory
    }

    func getFoodstuff() {
        sauce = factory.getSauce()
    }
}

Singletonパターン

概要

クラスがインスタンスを一つしか持たないことを保証し、そのインスタンスに対するグローバルポイントを提供する。

インスタンスを複数作ると値の不整合が起きてしまう場合があるので、一貫して共通のインスタンスのデータを扱いたい時に使うのが良さそうです。

Swiftでの開発ではUIApplication.shared.hogehogeのような使われ方をしている部分を見ることもあるかと思いますが、これはシングルトンを活用しているようです。

試しに作ったもの

貯金箱をシェアするような画面をそれぞれ作りました。
俺でも私でもボタンを押せば500円貯金をして、ちゃんとデータが引き継がれて値が更新されるようになっています。

俺視点貯金 私視点貯金
Singleton.png Singleton2.png

画面を跨いでも値が取れていますね。
SingletonPrint.png

クラス図

これまでで一番シンプルな図になりました。
それぞれの画面からシングルトンであるPiggyBankを参照するような形です。

Singleton.png

実装

PiggyBank.swift
class PiggyBank {
    static let shared = PiggyBank()
    private var price = 0

    private init() {}

    func updatePrice() {
        self.price += 500
    }

    func getPrice() -> Int {
        return price
    }
}
SingletonViewController.swift
class SingletonViewController: UIViewController {
    private let bank = PiggyBank.shared

    @IBAction private func didTapSavingButton(_ sender: Any) {
        bank.updatePrice()
        print("俺の貯金箱からの貯金で、累計:\(bank.getPrice())円になりました。")
    }
}

// 同様にSingleton2ViewControllerも実装

おさらい

パターン 特徴
Strategy 振る舞い(アルゴリズム)をクラスに分けてカプセル化し、柔軟に切り替えられるようにする
Observer オブジェクトの状態を監視し、状態が変わったらそれを監視している全てのオブジェクトに通知が飛んで更新される
Decorator 既存クラスを継承せずに、拡張する形で付加的な機能を定義していく
Factory Method オブジェクト作成を抽象化する。オブジェクト作成のためのインターフェース(プロトコル)を定義し、実際にどのクラスをインスタンス化するかを決めるのはサブクラスが行う
Abstract Factory 関連するオブジェクト群をまとめて生成する手順を抽象化する。これによりクライアントがインスタンスの情報を知る必要なオブジェクトを作成できる 
Singleton クラスがインスタンスを一つしか持たないことを保証し、グローバルなアクセスポイントを提供する

今回は6つのデザインパターンについて学びました。
実際に作ったアプリ(クラス図画像付き)のGitHubのリポジトリを共有しますので、具体的な実装をみたい方がいらっしゃったらご覧ください。

(2021/08/19更新 追加で勉強した分ができたので、リポジトリ内のディレクトリの階層を変更しました。この記事の実装は全てPatterns1にあります。)

参考にした書籍はまだ折り返していないかな?ってくらいなので引き続きアウトプットしていきます。

まだどういう特性があるのかをとりあえず知れたって感じなので、具体的に使う場面があるならどういうとこかはこれから考えていきたいです。

クラス図や実装は急ピッチで作った部分もあるので、アドバイスやご指摘があればコメント欄にお願いいたします。
お手柔らかにお願いいたします🥺

参照

共通
Head Firstデザインパターン ―頭とからだで覚えるデザインパターンの基本

Strategyパターン
Swiftで学ぶデザインパターン11 (Strategyパターン) - しめ鯖日記

Observerパターン

  1. 【Swift】Observerパターン入門 | 2速で歩くヒト
  2. RxSwift をやる前にちゃんと Observer パターンを学ぶ

Decoratorパターン

  1. Swiftで学ぶデザインパターン2 (Decorator パターン) - しめ鯖日記
  2. [iOS 8] Swiftでデザインパターン No.7 Decorator

Factory Method & Abstract Factoryパターン
Abstract Factory パターン - デザインパターン入門 - IT専科

Singletonパターン
Swift におけるシングルトン・staticメソッドとの付き合い方

27
42
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
27
42