The Power of Delegate Design Pattern
29 Mai 2019
Letzte Woche vor der WWDC und alle so aufgeregt über neue Funktionen, die wir in ein paar Tagen haben werden. Lassen Sie uns jedoch die Beiträge zu WWDC für die nächste Woche beibehalten. Diese Woche werden wir über meine Lieblings-Design-Muster-Ideen sprechen. Delegieren ist das einfachste und leistungsfähigste Muster.
In der Softwaretechnik ist das Delegierungsmuster ein objektorientiertes Entwurfsmuster, mit dem die Objektzusammensetzung dieselbe Wiederverwendung von Code wie eine Vererbung erreichen kann. Bei der Delegierung behandelt ein Objekt eine Anforderung, indem es an ein zweites Objekt (den Delegaten) delegiert. Ein Delegat ist ein Hilfsobjekt, jedoch mit dem ursprünglichen Kontext.
Protokolle
Wir verwenden das Delegatenmuster jeden Tag, und iOS SDK verwendet es an vielen Stellen. Beispielsweise delegiert UITableView an UITableViewDataSource, um die Tabelle mit Zellen zu füllen, und delegiert die Zellauswahl und andere Aktionen an UITableViewDelegate. Ein weiteres hervorragendes Beispiel für delegierte Muster sind FlowController oder Koordinatoren. viewControllers delegiert die Navigationslogik an den Koordinator. Ich habe einen Beitrag zum Extrahieren der Navigationslogik in Flowcontroller.
Lassen Sie uns in Codebeispiele eintauchen. Angenommen, Sie arbeiten an einem Spiel. Sie haben die Spiellogik in die separate Klasse Game extrahiert und möchten Änderungen am Spielstatus an UIViewController delegieren, der dieses Spiel rendert.
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 }}
Hier ist der Quellcode eines einfachen Spiels, das zufällige Werte generiert. Die Spiel-Engine generiert den Status basierend auf zufälligen Werten. Jeder Zustandsänderungsaufruf delegiert, um alte und neue Zustände zu übergeben. Wir definieren unser Delegatenprotokoll, das von AnyObject erweitert wurde, dh die einzige Klasseninstanz kann es akzeptieren. Ich verwende auch das Schlüsselwort weak , um den Delegaten für variable Holding zu definieren. Es musste den Aufbewahrungszyklus zwischen Delegat und Spielklasse durchbrechen. Schauen wir uns jetzt GameViewController an.
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) }}
Hier haben wir eine GameViewController-Klasse, die das Spiel mit Benutzeraktionen füttert und Statusänderungen rendert. GameViewController entspricht GameDelegate und implementiert alle erforderlichen Renderings in der Erweiterung. Als Ergebnis haben wir eine composable Codebasis mit Hilfe von Delegate Design Pattern.
Closures
Manchmal, wenn Sie nur eine Methode im Delegaten haben, können Sie sie durch closure ersetzen. Die Idee ist dieselbe, aber jetzt rufen Sie den Abschluss auf und übergeben den Status, anstatt die Methode per Protokoll aufzurufen. Schauen wir uns das Beispiel mit closure an.
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) } }}
Wie Sie sehen, übergeben wir den Abschluss an die Game-Klasseninstanz, die Statusänderungen verarbeitet. Wir verwenden weak , um den Retain-Zyklus während der Kontexterfassung von closure zu unterbrechen. Eine andere Option kann hier die Verwendung der Tatsache sein, dass jede Swift-Funktion eine Schließung ist. Anstatt also einen separaten Abschluss zu erstellen, können wir den Funktionsnamen übergeben. Seien Sie jedoch vorsichtig, dass diese Methode einen Kreis erstellt. Hier ist ein Beispiel, wie wir das tun können.
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() } }}
Fazit
Heute haben wir das leistungsfähigste und unkomplizierteste Entwurfsmuster in der iOS-Entwicklung besprochen. Ich genieße, wie einfach es ist und wie nützlich es sein kann, Stücke zu komponieren, um die Codebasis zu entkoppeln. Fühlen Sie sich frei, mir auf Twitter zu folgen und Ihre Fragen zu diesem Beitrag zu stellen. Danke fürs Lesen und bis nächste Woche!