Swift con Majid

Il potere del Delegato design pattern

29 maggio 2019

La scorsa settimana prima del WWDC e tutti così entusiasti delle nuove funzionalità che avremo solo tra pochi giorni. Tuttavia, manteniamo i post relativi al WWDC per la prossima settimana. Questa settimana ci accingiamo a parlare del mio delegato modello di progettazione preferito. Delegato è il modello più semplice e potente.

Nell’ingegneria del software, il modello di delega è un modello di progettazione orientato agli oggetti che consente alla composizione degli oggetti di ottenere lo stesso riutilizzo del codice come ereditarietà. In delega, un oggetto gestisce una richiesta delegando a un secondo oggetto (il delegato). Un delegato è un oggetto helper, ma con il contesto originale.

Protocolli

Usiamo Delegato modello ogni giorno, e iOS SDK lo usa in molti luoghi. Ad esempio, UITableView delega a UITableViewDataSource che popola la tabella con le celle, delega anche la selezione delle celle e altre azioni a UITableViewDelegate. Un altro eccellente esempio di pattern delegati è FlowController o Coordinators. ViewControllers delega la logica di navigazione al Coordinatore. Ho separato il post sull’estrazione della logica di navigazione in FlowControllers.

Immergiamoci in esempi di codice. Si supponga che si sta lavorando su un gioco. Hai estratto la logica di gioco in un gioco di classe separato e vuoi delegare le modifiche allo stato del gioco a UIViewController che rende questo gioco.

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

Ecco il codice sorgente di un semplice gioco che genera valori casuali. Il motore di gioco genera lo stato in base a valori casuali. Ogni cambiamento di stato chiama delegato per passare stati vecchi e nuovi. Definiamo il nostro protocollo delegato esteso da AnyObject, il che significa che l’unica istanza di classe può accettarlo. Uso anche la parola chiave debole per definire il delegato di holding variabile. Aveva bisogno di interrompere il ciclo di conservazione tra delegato e classe di gioco. Diamo un’occhiata a GameViewController ora.

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

Qui abbiamo una classe GameViewController che alimenta il gioco con le azioni dell’utente e rende le modifiche allo stato. GameViewController è conforme a GameDelegate e implementa tutto il rendering necessario nell’estensione. Di conseguenza, abbiamo una base di codice componibile con l’aiuto del modello di progettazione delegato.

Closures

A volte, quando si dispone di un solo metodo nel delegato, è possibile sostituirlo con closure. L’idea è la stessa, ma ora chiami la chiusura e passi lo stato invece di chiamare il metodo per protocollo. Diamo un’occhiata all’esempio con chiusura.

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

Come puoi vedere, passiamo la chiusura all’istanza della classe di gioco che gestisce le modifiche di stato. Usiamo weak per interrompere il ciclo di conservazione durante l’acquisizione del contesto di closure. Un’altra opzione qui può essere un uso del fatto che qualsiasi funzione Swift è una chiusura. Quindi, invece di creare una chiusura separata, possiamo passare il nome della funzione. Tuttavia, fai attenzione che questo metodo crei retain circle. Ecco un esempio di come possiamo farlo.

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

Conclusione

Oggi abbiamo discusso il modello di progettazione più potente e diretto nello sviluppo di iOS. Mi piace quanto sia semplice e quanto sia utile nel comporre pezzi per rendere disaccoppiato il codebase. Sentitevi liberi di seguirmi su Twitter e porre le vostre domande relative a questo post. Grazie per la lettura e ci vediamo la prossima settimana!



+