はじめに
デザインパターンについて、下記の書籍を用いて勉強したのですが、普段はSwiftでのコーディングをするので実際にクラス図を書いたり実装したりして学ぼうと思いアウトプットしてみました。
Head Firstデザインパターン ―頭とからだで覚えるデザインパターンの基本
ボリュームがあるので複数回に分けて記事を投稿予定です。
-
Swiftで覚えるデザインパターン ← 今ココ
- Strategy
- Observer
- Decorator
- Factory Method
- Abstract Factory
- Singleton
-
Swiftで覚えるデザインパターンその2
2021/08/20公開
- Command
- Adaptor
- Facade
- Template Method
-
Swiftで覚えるデザインパターンその3
2021/09/09公開
- Iterator
- Composite
- State
- Compound
Strategyパターン
概要
一連の振る舞いを一連のアルゴリズムとして定義し、それぞれをカプセル化して交換できる状態にする。
こうすることで、使用する側に意識をさせることなく簡単に戦略(Strategy)を切り替えられる。
試しに作ったもの
画面を開くと自動的にポケモンたちに飛行アクションをさせるというものを作りました。
飛行アクションをする前に、各ポケモンに飛行タイプを定義させます。
今回は下記タイプを用意しました。
- 通常飛行
- 飛行できない
- 風船
クラス図
Pokemon
クラスはFlyBehavior
(飛ぶ振る舞い)を持っている。
Pokemonクラスを継承したPikachu
やCharizard(リザードン)
に実際にfly
を実行させるためには、setFlyType
で飛行タイプをセットしてそれによってどのアルゴリズムを使うかを決める。
飛行タイプを決めるのは呼ぶ側で、今回で言うとStrategyViewController
が2体のポケモンを準備して飛ばそうとしています。
実装
必要そうな部分を抜粋(以降のパターンの実装も同様です。)
実装は参考書籍のものに極力寄せています。
protocol FlyBehavior {
/// 飛ぶ
static func fly()
}
class FlyNormal: FlyBehavior {
static func fly() {
print("ふつーな飛行")
}
}
// FlyNoWay, FlyBalloonも同様
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()
}
}
}
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.swift
のsetup()
内のコメントアウト部分
Observerパターン
概要
あるオブジェクトの状態が変わったことを、依存している全てのオブジェクトに自動的に通知して更新する。
値を保持するSubject
とそれを監視(購読)するObserver
から成る。
SubjectとObjectの依存関係は1:N
である。
試しに作ったもの
今回は購読って響きからサブスクサービスのNetflix的なのを想定しました。
「New episode」を押したらランダムな作品の第n話が発行され、その時Subjectを購読しているユーザに発行した値を通知するような画面を作ってみました。
わかりやすいように、「New episode」を押したら「New Episode Released!」と表示されるようにしています。
ObserverはNetflixに登録(Register)していないと購読できず、逆に購読をやめる(Remove)と購読解除できます。
デバッグ画像は、下記流れの各状態で新しいエピソードを発行したときの結果です。
- 誰も購読していない
- Johnだけ購読を始めた
- Paulも購読を始めた
- Johnが購読解除した
発行された値をちゃんと登録しているオブザーバーのみが検知できていると思います。
クラス図
Subject
とObserver
のオブジェクトは疎結合なので、互いにやりとりできますが中身を知っておくのが不要になるのがポイントのようです。
疎結合によって下記恩恵が得られます。
- Subject, Observer共に、変更しても影響を及ぼさない
- Subject, Observer共に再利用ができる
-
HuluSubject
を作ることも考えられる -
Taro
がオブザーバとして追加できることも考えられる
-
実装
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()
}
}
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)")
}
}
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円で作っています。
画像のように、煮卵、チャーシューを選択すると、600+80+100の780円になります。
会計ボタンを押した結果が以下です。
クラス図
Menu
が俗にComponent
と呼ばれるベースとなるクラスになる。
Menuの具象クラスとしてRamen
クラスがある。
Decorator
がラーメンの装飾をするためのトッピングを用意するクラスで、SeasonedEgg(煮卵)
、Seaweed(海苔)
、RoasePork(チャーシュー)
を今回用意しています。
これらのクラスがMenuの状態を拡張することで品目や料金を更新することができます。
DecoratorはMenu(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
}
}
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
}
}
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の場合はプロトコル)を定義し、サブクラスにどのクラスをインスタンス化させるかを決めさせる。
これによって、インスタンス化させる処理をカプセル化させられる。
試しに作ったもの
好きなボタンを押すとそのお好み焼きを作成するような画面を作成しました。
スイッチの向きによって広島風か関西風かを選べます。
スイッチが右にある場合(Onの場合)、関西風〜〜お好み焼きが出来上がります。
Offにすると、広島風になります。
クラス図
Factory MethodパターンではOkonomiyakiがProduct
、OkonomiyakiStoreがCreator
の役割を担っている。
Creator
がオブジェクト作成のためのメソッド(ファクトリメソッド)を定義し、そのサブクラスがファクトリメソッドを実装してオブジェクトを作成する。
Product
は、Productを実装したオブジェクト(それぞれの具体的なお好み焼き)をCreator
が呼び出すためのインターフェース(プロトコル)である。
prepare()やmix()等の処理は基本的には変わらないので、protocol extensionで実装して共通処理として扱う。
実装
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()
}
}
}
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 = "ミックスお好み焼き〜関西風〜"
}
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パターンでは食材の種類が異なり、作り方は共通なものとして実装するようなパターンにします。
実装
Sauce
とその実装しているクラスは定義しただけなので省略します。
他の食材を定義する場合も同様にできます。
呼び出すViewControllerも基本的には変わりません。
/// 食材取得用Factory
protocol OkonomiyakiIngredientFactory: AnyObject {
/// お好みソースを取得
func getSauce() -> Sauce
// 他の食材を取得したい場合は別途定義
}
/// 関西風お好み焼き食材Factory(広島風も同様)
final class KansaiOkonomiyakiIngredientFactory: OkonomiyakiIngredientFactory {
func getSauce() -> Sauce {
return KansaiSauce()
}
/*
他の食材も同様に実装
*/
}
// 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
}
}
/// お好み焼き
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円貯金をして、ちゃんとデータが引き継がれて値が更新されるようになっています。
俺視点貯金 | 私視点貯金 |
---|---|
クラス図
これまでで一番シンプルな図になりました。
それぞれの画面からシングルトンであるPiggyBank
を参照するような形です。
実装
class PiggyBank {
static let shared = PiggyBank()
private var price = 0
private init() {}
func updatePrice() {
self.price += 500
}
func getPrice() -> Int {
return price
}
}
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パターン
Decoratorパターン
Factory Method & Abstract Factoryパターン
Abstract Factory パターン - デザインパターン入門 - IT専科
Singletonパターン
Swift におけるシングルトン・staticメソッドとの付き合い方