【デザインパターン・Swift】Strategyパターン


Strategyパターンとは?

Strategyつまりは戦略のことを指します。戦略の切り替えや追加などが簡単にできるデザインパターンのことをStrategyパターンと言います。
戦略とはソフトウェア開発においてはアルゴリズムのことを指し、アルゴリズムを独立させることでプログラムに柔軟な振る舞いを実現することがStarategyパターンの大きなメリットになります。
本記事ではSwiftを用いコードベースでStrategyパターンを使用しない場合、した場合を見比べ実装方法、有用性を確認していきます。

Strategyパターンを使用しない場合

以下のコードはジャンケンのプログラムです。
Playerはプレイングタイプにより、getHand()でHandを出して、ゲームをしています。
この時プレイングタイプにより条件分岐しHandを出していますが、この処理はPlayerクラスに書くものでしょうか?
現状のコードだとゲームの内容がジャンケンということもあり、非常にシンプルな処理ですが、ゲームの内容によってはHandを決める処理が非常に複雑になることも考えられます。
もしこのような処理が複雑なものの場合、Playerクラスが非常に煩雑なコードになってしまいます。
以上によりPlayerとPlaeyrに関する値を決める処理を同じPlayerに定義することで依存度が上がり、柔軟性のないコードになってしまいます。

Hand.swift

enum Hand: Int {
    case paper = 0
    case stone = 1
    case scissors = 2
}

Player.swift

class Player {
    var name: String
    var type: Int
    
    init(name: String, type: Int) {
        self.name = name
        self.type = type
    }
    
    public func getHand() -> Int {
        if type == 0 {
            return Hand.stone.rawValue
        } else if type == 1 {
            return Hand.paper.rawValue
        } else if type == 2 {
            return Hand.scissors.rawValue
        } else {
            return Hand.init(rawValue: [0, 1, 2].randomElement()!)!.rawValue
        }
    }
}

Game.swift

class Game {
    public func fight(player1: Player, player2: Player) -> String {
        if player1.getHand() == player2.getHand() {
            return "Draw"
        } else if (player1.getHand() + 1) % 3 == player2.getHand() {
            return "\(player1.name) Win"
        } else {
            return "\(player2.name) Win"
        }
    }
}

ViewController.swift

class ViewController: UIViewController {

    @IBOutlet weak var testLabel: UILabel!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        testLabel.text = Game().fight(player1: Player(name: "Taro", type: 4), player2: Player(name: "Hana", type: 4))
        testLabel.sizeToFit()
    }
}

Strategyパターンを使用する場合

以下Strategyパターンを使用して、変更のあったファイルだけ記述しています。
ハンドをきめる処理を別のクラス(RockStrategy、RandomStrategy.)に切り分けることによりPlayerクラスがすっきりしました。
今回クラス分けした処理は単純に列挙体Handの値を返すだけの処理ですが、ゲームの内容によっては複雑なアルゴリズムの末、結果を出す必要があることもあります。そういった場合、このように切り分けないとそのアルゴリズムをPlayerクラスに記述してしまうことになり、煩雑なコードなコードになってしまい柔軟性を失うでしょう。
そのため条件やアルゴリズムを切り分けることにより柔軟性を与えるStrategyパターンはプログラムを組む上で需要なデザインパターンだと言えるでしょう。

Strategy.swift

protocol Strategy {
    func getHand() -> Int
}

RockStrategy.swift

class RockStrategy: Strategy {
    func getHand() -> Int {
        return Hand.stone.rawValue
    }
}

RandomStrategy.swift

class RandomStrategy: Strategy {
    func getHand() -> Int {
        return Hand(rawValue: [0, 1, 2].randomElement()!)!.rawValue
    }
}

Player.swift

class Player {
    var name: String
    var strategy: Strategy
    
    init(name: String, strategy: Strategy) {
        self.name = name
        self.strategy = strategy
    }
    
    public func getHandFromStrategy() -> Int {
        return strategy.getHand()
    }
}

Game.swift

class Game {
    public func fight(player1: Player, player2: Player) -> String {
        if player1.getHandFromStrategy() == player2.getHandFromStrategy() {
            return "Draw"
        } else if (player1.getHandFromStrategy() + 1) % 3 == player2.getHandFromStrategy() {
            return "\(player1.name) Win"
        } else {
            return "\(player2.name) Win"
        }
    }
}

ViewController.swift

class ViewController: UIViewController {

    @IBOutlet weak var testLabel: UILabel!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        testLabel.text = Game().fight(player1: Player(name: "Taro", strategy: RockStrategy()), player2: Player(name: "Hana", strategy: RandomStrategy()))
        testLabel.sizeToFit()
    }
}

参考

https://www.techscore.com/tech/DesignPattern/Strategy
https://qiita.com/i-tanaka730/items/4d00c884b7ce1594f42a