주니곰의 괴발노트
Swift - Protocol(타입, 델리게이트) (3/4) 본문
타입으로서의 프로토콜
- 프로토콜 자체는 어떤 기능도 구현하지 않지만, 프로토콜을 코드에서 완전한 타입으로 사용가능
- 타입으로 프로토콜을 사용하는 것은 "T가 프로토콜을 준수하는 타입 T가 존재한다"라는 구절에서 비롯된 존재 타입 (existential type)
- 다른 타입 (Int, String, 그리고 Double 등)이 허용되는 여러 위치에서 프로토콜을 사용가능
- 프로토콜은 타입이므로 이름을 대문자로 시작
protocol RandomNumberGenerator {
func random() -> Double
}
class LinearCongruentialGenerator: RandomNumberGenerator {
var lastRandom = 42.0
let m = 139968.0
let a = 3877.0
let c = 29573.0
func random() -> Double {
lastRandom = ((lastRandom * a + c)
.truncatingRemainder(dividingBy:m))
return lastRandom / m
}
}
class Dice {
let sides: Int
let generator: RandomNumberGenerator
init(sides: Int, generator: RandomNumberGenerator) {
self.sides = sides
self.generator = generator
}
func roll() -> Int {
return Int(generator.random() * Double(sides)) + 1
}
}
var d6 = Dice(sides: 6, generator: LinearCongruentialGenerator())
for _ in 1...5 {
print("Random dice roll is \(d6.roll())")
}
// Random dice roll is 3
// Random dice roll is 5
// Random dice roll is 4
// Random dice roll is 5
// Random dice roll is 4
- 이 예제는 보드 게임에서 사용하기 위한 n 면의 주사위를 나타내는 Dice 라는 새로운 클래스를 정의
- generator 프로퍼티는 RandomNumberGenerator 타입
- 따라서 RandomNumberGenerator 프로토콜을 채택하는 모든 타입에 인스턴스로 설정가능
- 인스턴스가 RandomNumberGenerator 프로토콜을 채택해야 된다는 것을 제외, 이 프로퍼티에 할당하는 인스턴스에 다른 것은 불필요
- 타입은 RandomNumberGenerator 이므로 Dice 클래스 내의 코드는 이 프로토콜을 준수하는 모든 생성기에 적용하는 방식으로만 generator 와 상호작용 가능
- 생성기의 기본 타입으로 정의된 메서드 또는 프로퍼티는 사용불가하나, 상위 클래스에서 하위 클래스로 다운 캐스트 할 수 있는 방법과 동일하게 프로토콜 타입에서 기본 타입으로 다운 캐스트 가능
- Dice 는 초기화 상태를 설정하기 위해 초기화 구문을 가지고 있는데, 새로운 Dice 인스턴스로 초기화 할 때 파라미터로 준수하는 타입의 값을 전달가능
- Dice 는 1과 주사위의 면의 숫자 사이의 정수 값을 반환하는 roll 인스턴스 메서드를 제공
- 이 메서드는 0.0 과 1.0 사이의 새로운 난수를 생성하는 생성기의 random() 메서드를 호출하고 올바른 범위 내의 주사위 굴림값을 생성하기 위해 난수를 사용
- generator 는 RandomNumberGenerator 를 채택하기 때문에 random() 메서드를 호출가능
위임(Delegation)
- 위임 (Delegation)은 클래스 또는 구조체가 책임의 일부를 다른 타입의 인스턴스에 넘겨주거나 위임할 수 있도록 하는 디자인 패턴
- 이 디자인 패턴은 위임된 기능을 제공하기 위해 준수하는 타입 (대리자라고 함)이 보장되도록 위임된 책임을 캡슐화하는 프로토콜을 정의하여 구현
- 위임은 특정 작업에 응답하거나 해당 소스의 기본 타입을 알 필요 없이 외부 소스에서 데이터를 검색하는데 사용
protocol DiceGame {
var dice: Dice { get }
func play()
}
protocol DiceGameDelegate: AnyObject {
func gameDidStart(_ game: DiceGame)
func game(_ game: DiceGame, didStartNewTurnWithDiceRoll diceRoll: Int)
func gameDidEnd(_ game: DiceGame)
}
- DiceGame 프로토콜은 주사위를 포함하는 모든 게임에 의해 채택될 수 있는 프로토콜
- DiceGameDelegate 프로토콜은 DiceGame 의 진행사항을 추적하기 위해 채택될 수 있음
- 클래스 전용 프로토콜 (Class-Only Protocols)은 AnyObject의 상속으로 표시
class SnakesAndLadders: DiceGame {
let finalSquare = 25
let dice = Dice(sides: 6, generator: LinearCongruentialGenerator())
var square = 0
var board: [Int]
init() {
board = Array(repeating: 0, count: finalSquare + 1)
board[03] = +08; board[06] = +11; board[09] = +09; board[10] = +02
board[14] = -10; board[19] = -11; board[22] = -02; board[24] = -08
}
weak var delegate: DiceGameDelegate?
func play() {
square = 0
delegate?.gameDidStart(self)
gameLoop: while square != finalSquare {
let diceRoll = dice.roll()
delegate?.game(self, didStartNewTurnWithDiceRoll: diceRoll)
switch square + diceRoll {
case finalSquare:
break gameLoop
case let newSquare where newSquare > finalSquare:
continue gameLoop
default:
square += diceRoll
square += board[square]
}
}
delegate?.gameDidEnd(self)
}
}
- 이 게임은 DiceGame 프로토콜을 채택하는 SnakesAndLadders 라는 클래스로 래핑
- 프로토콜을 준수하기 위해 gettable dice 프로퍼티와 play() 메서드를 제공 (초기화 후에 변경할 필요가 없고 프로토콜은 오직 gettable만 요구하므로 dice 프로퍼티는 상수 프로퍼티로 선언)
- Snakes and Ladders 게임보드 설정은 클래스의 init() 초기화 구문 내에서 발생
- 모든 게임 로직은 주사위 굴림값을 제공하기 위해 프로토콜의 요구된 dice 프로퍼티를 사용하는 프로토콜의 play 메서드에서 이동
- 위임자는 게임 플레이 하기위해 요구되지 않으므로 delegate 프로퍼티는 옵셔널 DiceGameDelegate 로 정의
- 옵셔널 타입이므로 delegate 프로퍼티는 자동으로 초기값을 nil 로 설정
- DiceGameDelegate 프로토콜은 클래스 전용 이므로 참조 사이클을 막기위해 weak 로 위임자를 선언가능
- DiceGameDelegate 는 게임의 진행사항을 추적하기 위해 3개의 메서드를 제공하는데, 이 3개의 메서드는 위의 play() 메서드 내의 게임 로직으로 통합되었고, 새로운 게임이 시작되고 새로운 턴이 시작되거나 게임이 종료될 때 호출
- delegate 프로퍼티는 옵셔널 DiceGameDelegate 이므로 play() 메서드는 위임자에서 메서드를 호출할 때마다 옵셔널 체이닝을 사용
- delegate 프로퍼티가 nil 이면 이 위임자 호출은 정상적으로 에러없이 실패
- delegate 프로퍼티가 nil 이 아니면 이 위임자 메서드가 호출되고 파라미터로 SnakesAndLadders 인스턴스는 전달
class DiceGameTracker: DiceGameDelegate {
var numberOfTurns = 0
func gameDidStart(_ game: DiceGame) {
numberOfTurns = 0
if game is SnakesAndLadders {
print("Started a new game of Snakes and Ladders")
}
print("The game is using a \(game.dice.sides)-sided dice")
}
func game(_ game: DiceGame, didStartNewTurnWithDiceRoll diceRoll: Int) {
numberOfTurns += 1
print("Rolled a \(diceRoll)")
}
func gameDidEnd(_ game: DiceGame) {
print("The game lasted for \(numberOfTurns) turns")
}
}
let tracker = DiceGameTracker()
let game = SnakesAndLadders()
game.delegate = tracker
game.play()
// Started a new game of Snakes and Ladders
// The game is using a 6-sided dice
// Rolled a 3
// Rolled a 5
// Rolled a 4
// Rolled a 5
// The game lasted for 4 turns
- DiceGameTracker는 DiceGameDelegate에 의해 요구된 3개의 메서드 모두 구현하고, 이 메서드를 사용하여 게임의 턴 수를 추적
- 게임이 시작될 때 numberOfTurns 프로퍼티를 0으로 재설정하고 새로운 턴이 시작될 때마다 증가하고 게임이 종료될 때 턴의 총 수를 출력
- 위에서 보이는 gameDidStart(_:) 의 구현은 곧 플레이 할 게임에 대한 일부 소개 정보를 출력하기 위해 game 파라미터를 사용
- game 파라미터는 SnakesAndLadders 가 아닌 DiceGame 의 타입을 가지므로 gameDidStart(_:)는 DiceGame 프로토콜의 부분으로 구현된 메서드와 프로퍼티로만 접근하고 사용가능하나, 이 메서드는 여전히 기본 인스턴스의 타입을 조회하기 위해 타입 캐스팅을 사용할 수 있음
- gameDidStart(_:) 메서드는 전달된 game 파라미터의 dice 프로퍼티에 접근
- game 은 DiceGame 프로토콜을 준수하므로 dice 프로퍼티가 보장되므로 gameDidStart(_:) 메서드는 어떤 종류의 게임을 플레이 하든 주사위의 sides 프로퍼티에 접근하고 출력가능
자료출처
https://docs.swift.org/swift-book/LanguageGuide/Protocols.html
Comments