kraften i delegat designmönster
29 maj 2019
förra veckan före WWDC och alla så glada över nya funktioner som vi kommer att ha bara om några dagar. Låt oss dock hålla inlägg relaterade till WWDC för nästa vecka. Den här veckan ska vi prata om min favoritdesignmönsterdelegat. Delegat är det mest enkla och kraftfulla mönstret.
i programvaruteknik är delegeringsmönstret ett objektorienterat designmönster som gör att objektkomposition kan uppnå samma kodåteranvändning som ett arv. I delegering hanterar ett objekt en begäran genom att delegera till ett andra objekt (delegaten). En delegat är ett hjälpobjekt, men med det ursprungliga sammanhanget.
protokoll
vi använder delegat mönster varje dag, och iOS SDK använder det på många ställen. Till exempel delegerar UITableView till UITableViewDataSource som fyller tabellen med celler, det delegerar också cellval och andra åtgärder till UITableViewDelegate. Ett annat utmärkt exempel på delegatmönster är FlowController eller samordnare. ViewControllers delegerar navigationslogik till koordinator. Jag har separerat inlägg om att extrahera navigationslogik i FlowControllers.
Låt oss dyka in i kodexempel. Antag att du arbetar på ett spel. Du extraherade spellogik i separerat klassspel, och du vill delegera speltillståndsändringar till UIViewController som gör det här spelet.
protocol GameDelegate: AnyObject { func stateChanged(from oldState: Game.State, to newState: Game.State)}class Game { private var state: State = .notStarted { didSet { delegate?.stateChanged(from: oldValue, to: state) } } weak var delegate: GameDelegate? private(set) var value: Int = 0 func start() { state = .started } func generateNextValue() { value = Int.random(in: 0..<1000) state = generateState(using: value) }}extension Game { enum State { case notStarted case started case right case win case lost }}
här är källkoden för ett enkelt spel som genererar slumpmässiga värden. Spelmotorn genererar tillstånd baserat på slumpmässiga värden. Varje statlig förändring kallar delegat för att passera gamla och nya stater. Vi definierar vårt delegatprotokoll utökat från AnyObject, det betyder att den enda klassinstansen kan acceptera den. Jag använder också svagt nyckelord för att definiera variabel holding delegat. Det behövs för att bryta behålla cykeln mellan delegat och spel klass. Låt oss ta en titt på GameViewController nu.
class GameViewController: UIViewController { private let game: Game init(game: Game) { self.game = game super.init(nibName: nil, bundle: nil) } @IBAction func play() { game.start() } @IBAction func next() { game.generateNextValue() } override func viewDidLoad() { super.viewDidLoad() game.delegate = self }}extension GameViewController: GameDelegate { func render(_ state: Game.State) { switch state { case .lost: renderLost() case .right: renderRight() case .win: renderWin() case .started: renderStart() case .notStarted: renderNotStarted() } } func stateChanged(from oldState: Game.State, to newState: Game.State) { render(newState) }}
Här har vi en GameViewController-klass som matar spel med användaråtgärder och gör tillståndsändringar. GameViewController överensstämmer med GameDelegate och implementerar alla nödvändiga rendering i förlängning. Som ett resultat har vi en sammansatt kodbas med hjälp av delegera designmönster.
stängningar
ibland när du bara har en metod i delegaten kan du ersätta den med stängning. Tanken är densamma, men nu ringer du Stängningen och skickar staten istället för att ringa metoden enligt protokoll. Låt oss ta en titt på exemplet med stängning.
class Game { typealias StateHandler = (State) -> Void var handler: StateHandler? private var state: State = .notStarted { didSet { handler?(state) } }}class GameViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() game.handler = { state in self?.render(state) } }}
som du kan se passerar vi Stängningen till spelklassinstansen som hanterar tillståndsändringar. Vi använder svaga för att bryta behållningscykeln under stängningens kontextfångst. Ett annat alternativ här kan vara en användning av det faktum att någon Swift-funktion är en stängning. Så istället för att skapa Separerad stängning kan vi skicka funktionsnamnet. Var dock försiktig med den här metoden skapar behåll cirkel. Här är ett exempel på hur vi kan göra det.
class GameViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() game.handler = render }}extension GameViewController { func render(_ state: Game.State) { switch state { case .lost: renderLost() case .right: renderRight() case .win: renderWin() case .started: renderStart() case .notStarted: renderNotStarted() } }}
slutsats
idag diskuterade vi det mest kraftfulla och enkla designmönstret i iOS-utveckling. Jag tycker om hur enkelt det är och hur användbart det kan vara att komponera bitar för att göra kodbas frikopplad. Följ mig gärna på Twitter och ställ dina frågor relaterade till det här inlägget. Tack för att du läste och vi ses nästa vecka!