GoFデザインパターンをSwiftyに書く


あけましておめでとうございます

年末年始休みで雪国のど田舎に帰省してゆっくりしてますが、コタツの中であまりに暇なので腰を据えて設計について考えるいい機会だなと思い、このテーマを選びました。

この記事で伝えたいこと

GoF(ゴフ)デザインパターンとは、the Gang of Fourと呼ばれる4人の開発者によって整理された、オブジェクト指向にもとづいた23個のプログラムのパターンのことです。
この記事では、その23個のパターンの中でもよく使いそうなものについてはサンプルコードをつけて紹介してます。その他のパターンは概要の説明にとどめてます。サンプルではできるだけ身近な例を使っているので、GoFデザインパターンにおける「オブジェクトの役割分担」と「プロトコルや構造体なども使ったSwiftらしい実装」のイメージをサクッとつかむ手助けになればこの記事は成功かなと思ってます。
ちなみに、概要説明で頻繁に出てくる「オブジェクト」という表現はSwiftのクラスや構造体などを指しています。

目次

振る舞いに関するパターン

Chain of Responsibility - 責任の連鎖

ある処理をオブジェクトに渡す際、処理の内容に応じて対応するオブジェクトを変えます。

Command - 命令

ある処理やその処理に伴って変化するパラメータなどをオブジェクトとしてまとめます。

Interpreter - 通訳

コードを解析し、その結果に応じて処理を行うオブジェクトをつくります。

Iterator - 反復子

配列などの集合体に対して、その要素1つ1つに順番に処理を行います。

Mediator - 調停者

複数のオブジェクト間の相互作用を調整するオブジェクトをつくります。

Memento - 思い出

ある時点でのインスタンスの状態を保存したり、復元したりするオブジェクトをつくります。

Observer - 観察者

監視される側の状態が変化したときに監視する側に通知するオブジェクトをつくります。

State - 状態

状態をオブジェクトとしてまとめ、その変化によって処理を切り替えます。
サンプルでは、Feelingsというenumが状態オブジェクトに当たり、stateの変化によってEngineerが振る舞いを変えます。

enum Feelings {
    case well, tired

    var isWell: Bool {
        return self == .well
    }

    func todo(for worker: Worker) -> String {
        switch self {
        case .well: return worker.task
        case .tired: return worker.relaxation
        }
    }
}

protocol Worker {
    var task: String { get set }
    var relaxation: String { get set }
    var wantsToDoTask: Bool { get }
    mutating func switchState(to state: Feelings)
    func doSomething()
}

struct Engineer: Worker {
    private var state = Feelings.well
    var task: String
    var relaxation: String
    var wantsToDoTask: Bool {
        return state.isWell
    }

    init(task: String, relaxation: String) {
        self.task = task
        self.relaxation = relaxation
    }

    mutating func switchState(to state: Feelings) {
        self.state = state
    }

    func doSomething() {
        print("\(state.todo(for: self))をする")
    }
}

var shintykt = Engineer(task: "プログラミング", relaxation: "サウナ浴")
shintykt.wantsToDoTask // true
shintykt.doSomething() // プログラミングをする
shintykt.switchState(to: .tired)
shintykt.wantsToDoTask // false
shintykt.doSomething() // サウナ浴をする

Strategy - 戦略

アルゴリズムをまとめたオブジェクトを複数用意し、その選択によって処理を切り替えます。
サンプルでは、iOSCareerAndroidCareerEngineerが選んで振る舞いを決めます。

protocol Career {
    func getSkill()
}

struct iOSCareer: Career {
    func getSkill() {
        print("iOS開発スキルを身につける")
    }
}

struct AndroidCareer: Career {
    func getSkill() {
        print("Android開発スキルを身につける")
    }
}

struct Engineer {
    private var career: Career

    init(career: Career) {
        self.career = career
    }

    func getSkill() {
        career.getSkill()
    }
}

let shintykt = Engineer(career: iOSCareer())
shintykt.getSkill() // iOS開発スキルを身につける

Template Method - テンプレートのメソッド

テンプレのメソッドを用意し、各オブジェクトで共有します。
サンプルでは、プロトコルのcontribute()で共有すべき処理の流れを実装しておいて、write()post()で具体的に何をするかはプロトコルの実装先に任せてます。

protocol Contributor {
    func write()
    func post()
    func contribute()
}

extension Contributor {
    func contribute() {
        (0 ..< 3).forEach { _ in write() }
        post()
    }
}

struct QiitaContributor: Contributor {
    func write() {
        print("Qiitaの記事を練る")
    }

    func post() {
        print("Qiitaに投稿する")
    }
}

let shintykt = QiitaContributor()
shintykt.contribute()
/* Qiitaの記事を練る
   Qiitaの記事を練る
   Qiitaの記事を練る
   Qiitaに投稿する */

Visitor - 訪問者

データ構造と処理を分け、処理を担うオブジェクトが複数のデータ構造に対して順々に処理を行います。
サンプルでは、処理を担うProgrammerとデータ構造を表すSwiftPHPDartで分かれてます。

// 処理する側
protocol ProgrammingLanguageVisitor {
    func study(_ language: Swift)
    func study(_ language: PHP)
    func study(_ language: Dart)
}

class Programmer: ProgrammingLanguageVisitor {
    var currentLanguage: String?

    func study(_ language: Swift) {
        currentLanguage = "Swift"
    }

    func study(_ language: PHP) {
        currentLanguage = "PHP"
    }

    func study(_ language: Dart) {
        currentLanguage = "Dart"
    }
}

// 処理される側
protocol ProgrammingLanguage {
    func accept(_ programmer: ProgrammingLanguageVisitor)
}

class Swift: ProgrammingLanguage {
    func accept(_ programmer: ProgrammingLanguageVisitor) {
        programmer.study(self)
    }
}

class PHP: ProgrammingLanguage {
    func accept(_ programmer: ProgrammingLanguageVisitor) {
        programmer.study(self)
    }
}

class Dart: ProgrammingLanguage {
    func accept(_ programmer: ProgrammingLanguageVisitor) {
        programmer.study(self)
    }
}

let shintykt = Programmer()
let languages = [Swift(), PHP(), Dart()].map { (language: ProgrammingLanguage) -> String in
    language.accept(shintykt)
    return shintykt.currentLanguage ?? ""
} // ["Swift", "PHP", "Dart"]

生成に関するパターン

Abstract Factory - 抽象的な製造所

あるインスタンスを生成するための処理やそれに関連するインスタンスの生成について定めたオブジェクトをつくります。

Builder - 建造者

ある複雑な構造をもったインスタンスを段階的に生成するための処理を定めたオブジェクトをつくります。

Factory Method - 製造所のメソッド

インスタンスの生成方法を定めたテンプレのメソッドを用意し、各オブジェクトで共有します。
サンプルでは、ImoniFanプロトコルでインスタンスの生成方法を実装しておいて、PersonFromYamagataではじめてインスタンスの具体型を決めています。
ちなみに芋煮というのは里芋を使った東北の郷土料理のことで、牛肉×しょうゆ味の山形芋煮派と豚肉×みそ味の宮城芋煮派で分かれてます。

// 生成される側
protocol Imoni {
    var ingredient: String { get }
    var taste: String { get }
}

struct YamagataImoni: Imoni {
    var ingredient: String {
        return "牛肉"
    }

    var taste: String {
        return "しょう油味"
    }
}

struct MiyagiImoni: Imoni {
    var ingredient: String {
        return "豚肉"
    }

    var taste: String {
        return "みそ味"
    }
}

// 生成する側
enum Prefecture {
    case Yamagata, Miyagi
}

protocol ImoniFan {
    func makeImoni(from prefecture: Prefecture) -> Imoni
    func eatImoni()
}

extension ImoniFan {
    func makeImoni(from prefecture: Prefecture) -> Imoni {
        switch prefecture {
        case .Yamagata: return YamagataImoni()
        case .Miyagi: return MiyagiImoni()
        }
    }
}

struct PersonFromYamagata: ImoniFan {
    func eatImoni() {
        let imoni = makeImoni(from: .Yamagata)
        print("\(imoni.ingredient)の入った\(imoni.taste)の芋煮を食べる")
    }
}

let shintykt = PersonFromYamagata()
shintykt.eatImoni() // 牛肉の入ったしょう油味の芋煮を食べる

Prototype - 原型

ひな形のインスタンスをコピーして類似したインスタンスをつくります。

Singleton - 唯一の存在

インスタンスが1個しか生成されないことを保証します。
サンプルでは、init()のアクセスレベルをprivateにすることでインスタンス生成を防いでいます。checker.hasSameInstanceでインスタンスが唯一であることがわかります。

class Singleton {
    static let shared = Singleton()

    private init() {
        print("インスタンスが生成された")
    }
}

class SingletonChecker {
    private let firstInstance = Singleton.shared
    private let secondInstance = Singleton.shared
    var hasSameInstance: Bool {
        return firstInstance === secondInstance
    }
}

let checker = SingletonChecker() // インスタンスが生成された
checker.hasSameInstance // true

構造に関するパターン

Adapter - 適合させるもの

データ構造を扱いやすいようにラップして提供するオブジェクトをつくります。

Bridge - 橋渡し

機能を追加するためのオブジェクトと実装を追加するためのオブジェクトを分け、両者を組み合わせて使います。

Composite - 混合物

格納する側と格納される側を同じものとみなして再帰的な構造をつくります。
サンプルでは、MemberTeamを同じOrganizationEntryとみなしています。Member側ではOrganizationEntryを追加することができないので、add(_ entry: OrganizationEntry)でエラーにしたりします。

protocol OrganizationEntry {
    var name: String { get }
    var role: String { get }
    mutating func add(_ entry: OrganizationEntry)
}

struct Member: OrganizationEntry {
    let name: String
    var role: String

    init(name: String, role: String) {
        self.name = name
        self.role = role
    }

    func add(_ entry: OrganizationEntry) {
        print("メンバーはエントリ追加不可")
    }
}

struct Team: OrganizationEntry {
    let name: String
    var role: String

    private var entryList = [OrganizationEntry]()

    init(name: String, role: String) {
        self.name = name
        self.role = role
    }

    mutating func add(_ entry: OrganizationEntry) {
        entryList.append(entry)
    }

    func showList() {
        entryList.forEach { print($0.name) }
    }
}

var iOSTeam = Team(name: "iOSチーム", role: "iOSアプリ開発・保守・運用")
let shintykt = Member(name: "shintykt", role: "iOSエンジニア")
let developmentTeam = Team(name: "開発チーム", role: "アプリ開発")
iOSTeam.add(shintykt)
iOSTeam.add(developmentTeam)
iOSTeam.showList()
/* shintykt
   開発チーム */

Decorator - 装飾者

あるインスタンスに対して補足的な機能を追加していくオブジェクトをつくります。

Facade - 表面

複雑な処理をまとめてシンプルなAPIを提供するオブジェクトをつくります。
サンプルでは、EditorCompilerDebuggerの処理をIDEintegrate()でまとめてます。

struct IDE {
    let editor = Editor()
    let compiler = Compiler()
    let debugger = Debugger()

    func integrate() {
        editor.produceSourceCode()
        compiler.compile()
        debugger.debug()
    }
}

struct Editor {
    func produceSourceCode() {
        print("ソースコードをつくる")
    }
}

struct Compiler {
    func compile() {
        print("コンパイルする")
    }
}

struct Debugger {
    func debug() {
        print("デバッグする")
    }
}

let xcode = IDE()
xcode.integrate()
/* ソースコードをつくる
   コンパイルする
   デバッグする */

Flyweight - フライ級

インスタンスを効率的に再利用することでメモリの使用量を減らします。

Proxy - 代理人

あるインスタンスが行う処理の中でそのインスタンスが行う必要がないものを代理で行うラッパーをつくります。

以上