/ Swift

【译】Swift Delegation

Delegation委托是Apple iOS开发中很常见的一种模式,不过在之前开发Hipo中更多算是照猫画虎,这次来一篇Apple iBooks中《The Swift Programming Language (
Swift 4.2 beta)》
中Protocol -- Delegation的译文。


委托(Delegation)是一种设计模式,能够让类或者结构体将自己一部分责任移交(或者称之为委托delegate)给另一个类型实例。该设计模式首先定义一个协议(protocol)描述被委托的责任,然后由受托者保证委托功能的实现。委托设计模式能够用来响应特定的动作或者从其他来源接受数据而不需要知道对方的类型。

下面例子为骰子积分榜类游戏定义了两个协议

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

protocol DiceGameDelegate: AnyObject {
    func gameDidStart(_ game: DiceGame)
    func game(_ game: DiceGame, didStartNewTurnWithDiceRoll: Int)
    func gameDidEnd(_ game: DiceGame)
}

DiceGame协议可以用在任何骰子类游戏。

DiceGameDelegate协议则可以用来追踪DiceGame的进度。为避免强引用的环形依赖,委托被声明为弱引用。更多弱引用的介绍可以查看Strong Reference Cycles Between Class Instances。作为一个只能用在类上的协议,下面例子中SnakesAndLadders类只能用弱引用来声明它的委托。 一个协议继承AnyObject之后就只能用在类上面了,更多信息可以查看Class-Only Protocols

下面版本的Snakes and Ladders游戏代码最初在Control Flow中介绍过。这个版本采用了DiceGame协议,使用Dic实例做它的骰子机,并且将它的进度通知给DiceGameDelegate

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)
    }
}

对于Snakes and Ladders游戏介绍,见Break

这个版本的游戏使用采用了DiceGame协议的SnakesAndLadders类来实现。它实现了一个可取值属性dice和一个方法play()来遵循协议(属性dice声明为常量,是因为它在初始化之后就不再更改了,并且协议中只规定它必须可取值)。

游戏的计分卡在类的init()中设置了。所有的游戏逻辑移到了协议的play方法中,并且使用了协议所规定的dice属性来提供骰子的值。

注意,delegate属性被定义为optional的DiceGameDelegate。是因为这个协议不是玩游戏所必须的。它是optional类型的,delegate属性自动设置初始值位nil。因此游戏的实例化使用者可以选择给它设置一个合适的委托。DiceGameDelegate协议是class-only的,你可以声明这个委托为weak引用类避免环形依赖。

DiceGameDelegate协议提供三个方法来跟踪游戏的进度。并且这三个方法已经在上述的play方法中集成了。它们会在新游戏开始,新一轮开始,或者游戏结束时候调用。

因为delegate属性可能是(optional)DiceGameDelegateplay()方法每次使用optional chaining来调用委托的方法。如果delegate属性是nil值,那么调用这些委托方法会优雅的失败而不会报错。如果delegate属性不是nil的值,当委托方法被调用的时候SnakesAndLadders的实例会被当作参数传进去。

下面例子DiceCgameTracker类实现了DiceGameDelegte协议。

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 lated for \(numberOfTurns) turns")
    }
}

DiceGameTracker实现了DiceGameDelegate所要求的三个方法。并且使用这三个方法来记录游戏进行了多少轮。它在游戏开始后重置numberOfTurns为0,每次新一轮开始后递增,并且一旦游戏结束就打印出游戏总共进行了多少轮。

上述gameDidStart(_:)的实现中,使用game参数来打印所玩游戏的介绍性信息。game的类型是DiceGame而不是SnakesandLadders,因此gameDidStart(_:)只能访问和使用DiceGame协议中的属性和方法。不过仍然能够使用类型推断(type casting)来判断实例的类。在这个例子中。它检查了如果gameSnakesandLadders的实例,就打印一些信息。

gameDidStart(_:)方法中还访问了game参数的dice属性。因为game实现了DiceGame协议,因此就保证了一定有dice属性。同样gameDidStart(_:)方法也能够访问dice的sides属性,而不用关心具体玩的那个游戏。

下面就是DiceGameTracker使用时候:

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”

下面代码的高亮截图,
Teampaper-Snap_2018-08-08-18-57-14@2x

Teampaper-Snap_2018-08-08-18-57-47@2x

Teampaper-Snap_2018-08-08-18-58-10@2x