21. 11. 07 Protocol, Delegation

Allie·2021년 11월 24일
0

TIL

목록 보기
11/14

Protocol

→ 메서드, 프로퍼티 그리고 특정 작업이나 기능의 부분이 적합한 다른 요구사항의 청사진을 정의한다. 프로토콜은 요구사항의 구현을 제공하기 위해 클래스, 구조체 또는 열거형에 의해 채택될 수 있다. 프로토콜의 요구사항에 충족하는 모든 타입은 프로토콜을 준수한다고 한다. 요구사항을 지정하는 것 외에도 그 일부를 구현하거나 준수하는 타입에 추가 기능을 구현하기 위해 프로토콜을 확장할 수 있다.

  • 프로토콜 기본 문법
    protocol SomeProtocol {
        // protocol definition goes here
    }
    
    // 이런식으로 어떤 타입이 프로토콜을 채택할 때, 타입의 이름 뒤에 명시한다.
    struct SomeStructure: FirstProtocol, AnotherProtocol {
        // structure definition goes here
    }

protocol을 사용하는 이유?

매개변수를 쓴다던지, 할당할 때 타입을 명시해주는데(ex. String) 어떤 스트링도 다 들어올 수 있다. 스트링이지만 해당 타입을 따르는 스트링만 들어올 수 있게하려고?

  • 협업을 할때에도 장점이 있다. 예를들면, A, B팀이 협업해 프로젝트를 할때, A팀이 구현하는 기능의 protocol을 만들어서 타입 규정을 해놓으면 관련된 기능을 개발하는 B팀은 A팀의 개발이 미완성인 상태라도, 그 프로토콜을 가져와서 완성이 되었다고 가정하고 개발을 할 수 있다.
  • 프로토콜을 이용해서 의존성을 줄일 수 있다

Error Type

  • Error protocol은 비어있는 프로토콜인데, 왜 비어있는걸 만들었을까? 왜 써야할까? → 에러타입이라는걸 컴파일러한테 알려주기 위해서. throws 함수를 쓸때 에러를 던지는데, 에러라는 걸 우리는 아는데, 프로토콜을 채택하지 않으면 컴파일러 입장에서는 그냥 열거형이다. 컴파일러랑 협업을 하는 느낌으로 이해하면될듯?..

Property Requirements

protocol SomeProtocol {
    var mustBeSettable: Int { get set } // 읽기쓰기 가능
    var doesNotNeedToBeSettable: Int { get } // 읽기 전용
}

struct SomeStructure: SomeProtocol {
	// 해당 프로토콜을 채택하는 과정. 콤마로 구분해서 여러개의 프로토콜을 채택할 수 있음.
}

class SomeClass: SomeSuperclass, FirstProtocol, AnotherProtocol {
    // 부모 클래스를 상속받는 경우에는, 클래스를 프로토콜보다 선행하여 적어야한다.
}

protocol AnotherProtocol {
    static var someTypeProperty: Int { get set }
		// 타입 프로퍼티는 항상 static 키워드를 붙여서 정의해야한다.
}
  • 프로토콜은 해당 프로토콜을 준수하는 어떤 타입에게 특정 이름과 타입을 가진 instance property 또는 type property를 요구할 수 있다. 프로토콜은 이 프로퍼티가 저장 프로퍼티인지, 연산 프로퍼티인지 명시하지 않고, 오직 프로퍼티의 이름과 타입만 요구한다. 또한, 프로토콜은 각 프로퍼티에 읽기만 가능한지, 읽기와 쓰기가 모두 가능한지 명시해야한다.

  • 프로퍼티 요구사항은 항상 변수 프로퍼티로 선언되어야 한다. gettable과 settable프로퍼티는 선언 다음에 { get set }을 쓰고, gettable프로퍼티는 { get }을 써준다.

  • 프로토콜 요구 프로퍼티의 get/set이란?

  • 저장 프로퍼티 관점 : get=외부에서 프로퍼티 값을 꺼내갈 수 있게 해라, set=외부에서 프로퍼티 값을 수정할 수 있게 해라
    (상수는 초기화 이후 수정 불가하니까 get set은 상수로 구현 못함)

  • 연산 프로퍼티 관점 : get=연산 프로퍼티의 getter (자신의 값을 남의 값을 통해 구함)를 구현해라, set=연산 프로퍼티의 setter (남의 값을 자신의 값을 통해 구함)를 구현해라

  • get set (읽기쓰기) : 상수(=불변 저장 프로퍼티)로 구현 불가, 읽기쓰기 변수로 구현함 (=가변 저장 프로퍼티 또는 getter/setter가 둘다 있는 가변 연산 프로퍼티)

  • get (읽기 전용) : 상수 가능, 읽기쓰기 변수 또는 읽기전용 변수로 구현함. 프로토콜에서 get 은 '읽기전용'보다는 "(외부기준) 읽기는 반드시 뚫어 놓고 쓰기는 그러던지 말던지 알아서 해라~"

// MARK: gettable 읽기전용
protocol FullyNamed {
    var fullName: String { get }
}

// 저장 프로퍼티
struct Person: FullyNamed {
    var name: String
}

var allie = Person(fullName: "Allie Park")
allie.fullName = "Sally Kim"

// 연산 프로퍼티
struct Person: FullyNamed {
    var name: String
var fullName: String {
      return name
    }
}
var allie = Person(name: "Allie Park")
allie.fullName = "Sally Kim" // 읽기전용이라 오류!!

struct Person: FullyNamed {
    var name: String
var fullName: String {
        get {
            return name
        }
        set {
            name = newValue
        }
    }
}
var allie = Person(name: "Allie Park")
allie.fullName = "Sally Kim" // settable이라 ok!!

// MARK: 읽기 및 쓰기 전용
protocol fullyNamed {
    var fullName: String { get set }
}

struct Person: fullyNamed {
    var name: String
    var fullName: String {
        return name
    }
}
// protocol 이 읽기 및 쓰기전용을 요구했는데 set이 충족이 안되어 오류발생.

Method Requirements

  • 프로토콜은 특정 인스턴스 메소드 및 타입 메소드가 타입을 준수하여 구현되도록 요구할 수 있다.
  • 메서드는 일반 인스턴스 및 타입 메서드와 완전히 동일하지만, 중괄호와 메서드 본문없이 프로토콜 정의의 일부로 작성된다. 가변 파라미터는 일반 메서드와 동일한 규칙에 따라 허용된다. 그러나 프로토콜 정의 내의 메서드 매개변수에는 기본값을 지정할 수 없다.
  • 아래와같이 정의만 프로토콜에서 하고 해당 프로토콜을 채택하는 타입에서 이 메서드를 구현해야한다!!
protocol SomeProtocol {
    func someMethod()
    func anotherMethod(name: String, age: Int) -> Int
    func protocolMethod() -> String
}
  • 타입 프로퍼티를 프로토콜에 정의한 경우, 타입 메서드 요구사항에 항상 static 키워드를 붙여야 한다. 클래스에서 구현할 때, 타입 메서드 요구사항 앞에 항상 class 또는 static 키워드가 붙는 경우에도 마찬가지!
protocol SomeProtocol {
    static func someTypeMethod()
    static func anotherTypeMethod()
}

class Allie: SomeProtocol {
    static func someTypeMethod() {
    }
    static func anotherTypeMethod() {
    }
}
class Child: Allie {
    override static func anotherTypeMethod() {
    }  // 서브클래스에서 재정의 가능!@
}
// MARK: 단일(single) 인스턴스 메소드를 요구하는 프로토콜 예제
protocol RandomNumberGenerator {
    func random() -> Double
}

class LinearCongruentialGenerator: RandomNumberGenerator {
    var lastRandom = 42.0 // 기본값을 할당해서 init 필요없음
    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
    }
}
  • 인스턴스에 속한 메서드를 수정 또는 변경할 때, 값타입인 구조체와 열거형 같은 경우에는 해당 메서드 앞에 mutating 키워드를 붙여 해당 메서드가 속한 인스턴스와 해당 인스턴스의 모든 프로퍼티를 수정할 수 있음을 나타내준다. 만약 프로토콜을 채택하는 곳이 열거형이나 구조체인데, 프로퍼티 변경이 필요하다면!! 애초에 프로토콜에서 mutating으로 메서드를 만들라고 요구하면 된다! 클래스라면 키워드 빼고 작성 가능!!

Initializer Requirements

  • 프로토콜은 해당 프로토콜을 준수하려는 타입에게 특정한 이니셜라이저를 구현하도록 요구할 수 있다.
  • 해당 프로토콜을 준수하는 클래스에서 이니셜라이저 요구사항을 구현할 수 있는데, required라는 modifier를 표시해주어야함!! 구조체에서는 필요없음.
  • required를 init앞에 써줘야하는 이유는, required를 사용하면 해당 프로토콜을 준수하는 클래스의 모든 하위 클래스들 역시 이니셜라이저 요구사항을 구현하는 것을 보장 받을 수 있기 때문이다. 만약 final 클래스이면 서브클래스화 할 수 없기 때문에 안써줘두 된다. (final은 override 재정의 막아주는 애)
protocol SomeProtocol {
    init(someParameter: Int)
} // 요렇게~

class SomeClass: SomeProtocol {
    required init(someParameter: Int) {
        // initializer implementation goes here
    }
}

// MARK: 서브클래스가 슈퍼클래스의 init을 재정의하고, 프로토콜에서 요구하는 init과 일치하는 경우
protocol SomeProtocol {
    init()
}

class SomeSuperClass {
    init() {
        // initializer implementation goes here
    }
}

class SomeSubClass: SomeSuperClass, SomeProtocol {
    // "required" from SomeProtocol conformance; "override" from SomeSuperClass
    required override init() {
        // initializer implementation goes here
    }
}

Protocols as Types

  • 프로토콜은 기능을 구현하지는 않지만, 타입이 때문에 완전한 형태로 코드에서 사용된다.
  • 프로토콜은 다른 타입이 허용되는 여러 곳에서 다음과 같은 프로토콜을 사용할 수 있다.
    • 함수, 메서드 또는 이니셜라이저에서의 매개변수 타입 또는 리턴타입
    • 상수, 변수 또는 프로퍼티로서의 타입
    • 배열, 딕셔너리 또는 다른 컨테이너 항목으로서의 타입
  • 아래 Dice라는 클래스에서 RandomNumberGenerator라는 프로토콜을 채택하지는 않았지만, generator라는 저장프로퍼티의 타입으로 지정해줬다. generators는 RandomNumberGenerator타입이기 때문에, random()이라는 메서드는 구현해줘야 할 의무는 없고, 그냥 random()에 접근할 수 있게 되는것이다.
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
    }
}

Delegation

→ Delegation은 클래스나 구조체가 책임을 일부 다른 타입의 인스턴스로 넘길 수 있게 하는 디자인 패턴이다.

protocol DiceGame {
    var dice: Dice { get }
    func play()
}

/// DiceGame의 책임을 DiceGameDelegate를 따르는 인스턴스에 위임
protocol DiceGameDelegate: AnyObject { // AnyObject로 선언하면 클래스만 이 프로토콜을 채택할 수 있는 속성이 된다.
    func gameDidStart(_ game: DiceGame)
    func game(_ game: DiceGame, didStartNewTurnWithDiceRoll diceRoll: Int)
    func gameDidEnd(_ game: DiceGame)
}

class SnakesAndLadders: DiceGame { // 프로토콜의 조건에 맞추기위해 dice라는 프로퍼티는 gettable하게 구현되어 있고, play()메소드가 구현되어 있다.
    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() { // 바로 위에 정의한 delegate가 DiceGameDelegation 옵셔널타입이므로 play() 메소드는 delegate의 메소드를 호출할때마다 옵셔널 체이닝을 한다.
        square = 0
        /// DiceGameDelegation의 게임 진행상황을 tracking하는 메소드(게임 시작)
        delegate?.gameDidStart(self)
        gameLoop: while square != finalSquare {
            let diceRoll = dice.roll()
            /// DiceGameDelegation의 게임 진행상황을 tracking하는 메소드(게임 진행)
            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]
            }
        }
        /// DiceGameDelegation의 게임 진행상황을 tracking하는 메소드(게임 종료)
        delegate?.gameDidEnd(self)
    }
}

// DiceGameDelegate 프로토콜을 채택한 DiceGameTracker클래스
class DiceGameTracker: DiceGameDelegate { // 프로토콜의 조건에 맞추기위해 3개의 메소드가 구현되어 있다.
    var numberOfTurns = 0
    func gameDidStart(_ game: DiceGame) {
        numberOfTurns = 0
        if game is SnakesAndLadders { // 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")
    }
}
  • 프로토콜에 AnyObject를 추가하면 클래스에서만 해당 프로토콜을 채택할 수 있다.(Class-Only Protocol이 된다.)
profile
게발자🦀 되는 중.. 궁김하다.. 궁김해..

0개의 댓글