La puissance du motif de conception délégué
29 mai 2019
La semaine dernière avant la WWDC et tout le monde était tellement excité par les nouvelles fonctionnalités que nous aurons dans quelques jours. Cependant, conservons les messages liés à la WWDC pour la semaine prochaine. Cette semaine, nous allons parler de mon délégué de modèle de conception préféré. Le délégué est le modèle le plus simple et le plus puissant.
En génie logiciel, le modèle de délégation est un modèle de conception orienté objet qui permet à la composition d’objets d’obtenir la même réutilisation de code qu’un héritage. En délégation, un objet gère une requête en déléguant à un second objet (le délégué). Un délégué est un objet d’assistance, mais avec le contexte d’origine.
Protocoles
Nous utilisons le modèle délégué tous les jours, et le SDK iOS l’utilise à de nombreux endroits. Par exemple, UITableView délègue à UITableViewDataSource le remplissage de la table avec des cellules, il délègue également la sélection de cellules et d’autres actions à UITableViewDelegate. Un autre excellent exemple de patters de délégués est le contrôleur de flux ou les coordinateurs. ViewControllers délègue la logique de navigation au coordinateur. J’ai séparé un article sur l’extraction de la logique de navigation dans les contrôleurs de flux.
Plongeons dans des échantillons de code. Supposons que vous travaillez sur un jeu. Vous avez extrait la logique du jeu dans un jeu de classe séparé et vous souhaitez déléguer les changements d’état du jeu à UIViewController qui rend ce jeu.
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 }}
Voici le code source d’un jeu simple qui génère des valeurs aléatoires. Le moteur de jeu génère un état basé sur des valeurs aléatoires. Chaque délégué d’appel de changement d’état pour passer les anciens et les nouveaux états. Nous définissons notre protocole délégué étendu à partir d’AnyObject, cela signifie que la seule instance de classe peut l’accepter. J’utilise également un mot clé faible pour définir un délégué de tenue de variable. Il fallait rompre le cycle de rétention entre le délégué et la classe de jeu. Jetons un coup d’œil à GameViewController maintenant.
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) }}
Nous avons ici une classe GameViewController qui alimente le jeu avec les actions de l’utilisateur et modifie l’état du rendu. GameViewController est conforme à GameDelegate et implémente tout le rendu nécessaire dans l’extension. En conséquence, nous avons une base de code composable à l’aide du modèle de conception délégué.
Closures
Parfois, lorsque vous n’avez qu’une seule méthode dans le délégué, vous pouvez la remplacer par closure. L’idée est la même, mais maintenant vous appelez la fermeture et passez l’état au lieu d’appeler la méthode par protocole. Jetons un coup d’œil à l’exemple avec fermeture.
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) } }}
Comme vous pouvez le voir, nous passons la fermeture à l’instance de classe de jeu qui gère les changements d’état. Nous utilisons weak pour rompre le cycle de rétention lors de la capture de contexte de fermeture. Une autre option ici peut être une utilisation du fait que toute fonction Swift est une fermeture. Ainsi, au lieu de créer une fermeture séparée, nous pouvons passer le nom de la fonction. Cependant, soyez prudent cette méthode crée un cercle de rétention. Voici un exemple de la façon dont nous pouvons le faire.
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() } }}
Conclusion
Aujourd’hui, nous avons discuté du modèle de conception le plus puissant et le plus simple du développement iOS. J’aime à quel point c’est simple et à quel point il peut être utile de composer des pièces pour découpler la base de code. N’hésitez pas à me suivre sur Twitter et à poser vos questions liées à ce post. Merci d’avoir lu et à la semaine prochaine!