El poder del patrón de diseño Delegado
29 de mayo de 2019
La semana pasada antes de WWDC y todo el mundo tan entusiasmado con las nuevas características que tendremos en pocos días. Sin embargo, mantengamos las publicaciones relacionadas con WWDC para la próxima semana. Esta semana vamos a hablar de mi Delegado de patrones de diseño favorito. Delegar es el patrón más directo y poderoso.
En ingeniería de software, el patrón de delegación es un patrón de diseño orientado a objetos que permite que la composición de objetos logre la misma reutilización de código que una herencia. En la delegación, un objeto maneja una solicitud delegando en un segundo objeto (el delegado). Un delegado es un objeto auxiliar, pero con el contexto original.
Protocolos
Usamos el patrón delegado todos los días, y el SDK de iOS lo usa en muchos lugares. Por ejemplo, UITableView delega a UITableViewDataSource rellenando la tabla con celdas, también delega la selección de celdas y otras acciones a UITableViewDelegate. Otro excelente ejemplo de patrones de delegado es FlowController o Coordinators. ViewController delega la lógica de navegación en Coordinator. He separado un post sobre la extracción de lógica de navegación en controladores de flujo.
Vamos a sumergirnos en ejemplos de código. Asume que estás trabajando en un juego. Ha extraído la lógica del juego en juego de clase separado, y desea delegar los cambios de estado del juego a UIViewController, que representa este juego.
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 }}
Aquí está el código fuente de un juego simple que genera valores aleatorios. El motor de juego genera estado basado en valores aleatorios. Cada estado llama a un delegado para que pase a los estados antiguos y nuevos. Definimos nuestro protocolo de delegado extendido desde AnyObject, lo que significa que la única instancia de clase puede aceptarlo. También uso palabras clave débiles para definir delegado de retención de variables. Necesitaba romper el ciclo de retención entre la clase delegada y la clase de juego. Echemos un vistazo a GameViewController ahora.
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) }}
Aquí tenemos una clase GameViewController que alimenta el juego con acciones de usuario y cambios de estado de renderizado. GameViewController se ajusta a GameDelegate e implementa todo el renderizado necesario en extensión. Como resultado, tenemos una base de código componible con la ayuda de Delegate design pattern.
Cierres
A veces, cuando solo tiene un método en el delegado, puede reemplazarlo con cierre. La idea es la misma, pero ahora llama al cierre y pasa el estado en lugar de llamar al método por protocolo. Echemos un vistazo al ejemplo con cierre.
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) } }}
Como puede ver, pasamos el cierre a la instancia de clase de juego que maneja los cambios de estado. Usamos débil para romper el ciclo de retención durante la captura de contexto de cierre. Otra opción aquí puede ser el uso del hecho de que cualquier función Swift es un cierre. Así que en lugar de crear un cierre separado, podemos pasar el nombre de la función. Sin embargo, tenga cuidado de que este método cree un círculo de retención. He aquí un ejemplo de cómo podemos hacerlo.
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() } }}
Conclusión
Hoy hemos discutido el patrón de diseño más potente y directo en el desarrollo de iOS. Disfruto de lo simple que es y lo útil que puede ser para componer piezas para desacoplar la base de código. Siéntete libre de seguirme en Twitter y hacer tus preguntas relacionadas con este post. Gracias por leer y nos vemos la próxima semana!