Muy buenas, me llamo Luis y en esta ocasión les traigo este artículo.
Índice
Resuelva las dependencias fácilmente escribiendo sus propios contenedores
Si depende de algo, pierde algo de libertad. Pero si puede reemplazar fácilmente esa dependencia, ¡al menos tendrá la libertad de decidir de qué depende!
La dependencia concreta se asigna luego desde el exterior en tiempo de ejecución. Esto se denomina inyección de dependencia (DI) o, en un sentido más amplio, gestión de dependencia (DM).
Para los proyectos de DI en Swift, existen soluciones de marco, como Swinject . Crea un contenedor y registra todas las posibles dependencias en él. Al crear objetos, se consulta el contenedor, que resuelve las dependencias devolviendo estos objetos.
Una alternativa interesante es Weaver . A diferencia de Swinject, Weaver es una herramienta de línea de comandos y, por lo tanto, no se incluye como marco en el proyecto. Durante el proceso de compilación, generará automáticamente el código de dependencia.
Cualquiera que esté satisfecho con un marco o una herramienta de línea de comandos debería, por supuesto, usar uno. Sin embargo, hay cierta ironía en hacerse dependiente de algo nuevo con el fin de resolver dependencias.
Inyección manual de dependencia
En principio, necesitas un contenedor que contenga todas las dependencias y que pases a las clases que necesitan las dependencias. Para evitar depender directamente de este contenedor, utiliza protocolos y también para las propias dependencias.
protocol ServerInterface { func startRequest() } class Server: ServerInterface { func startRequest() {} } protocol DependencyContainerInterface { var server: ServerInterface { get } } class DependencyContainer: DependencyContainerInterface { let server: ServerInterface init(server: ServerInterface) { self.server = server } } class ViewController: UIViewController { let dependencies: DependencyContainerInterface init(dependencies: DependencyContainerInterface) { self.dependencies = dependencies } func viewDidAppear() { dependencies.server.startRequest() } func navigate() { let nextViewController = NextViewController(dependencies: dependencies) present(nextViewController) } } let container = DependencyContainer(server: Server()) let viewController = ViewController(dependencies: container)
La línea 5
define la dependencia: aquí una clase de servidor cuya tarea ejemplar es comunicarse con un servidor.
Para no depender directamente de esta clase específica, el protocolo asociado se define en la línea 1
.
En este caso, los protocolos se utilizan en el sentido clásico como interfaces y, por lo tanto, se denominan como tales. Volvamos a los viejos tiempos en los que se usaban archivos de encabezado.
Lo mismo ocurre en las líneas 9
y 13
, donde se define el contenedor. DependencyContainer
representa el contenedor que contiene la dependencia del servidor en la variable server
.
El contenedor ya no sabe nada sobre la clase Server
concreta porque solo hace referencia a ServerInterface
.
Un ViewController
ficticio luego hace que el contenedor se inyecte en el método init
. Nuevamente, ViewController
no conoce la clase DependencyContainer
concreta, solo su protocolo.
Sin embargo, esto es suficiente para iniciar una solicitud de servidor a través de la clase de servidor dependiente en la línea 29
.
Un objeto raíz, como un AppDelegate
, debe crear instancias concretas. Aquí, esto sucede como un ejemplo en la línea 38
.
La línea 39
finalmente crea el ViewController
concreto e inyecta las dependencias. Si no crea una instancia manual de su ViewController
, también puede pasar el contenedor a través de la inyección de propiedad, por lo que en lugar de un inicializador basado en método init
, la dependencia se asigna directamente a la propiedad.
Si un ViewController
llama a uno nuevo, esto se hace nuevamente a través de DI
, que se describe como ejemplo en un método navigate
en la línea 32
.
Si se requieren más dependencias, todo lo que tiene que hacer es extender el DependencyContainer
y pasar las dependencias concretas en la instanciación.
En un entorno UnitTest
, no le daría al DependencyContainer
una instancia de servidor específica, sino tal vez una instancia de ServerMock
.
Luego, el simulacro simplemente implementa ServerInterface
y verifica si se invocan los métodos correctos durante la prueba o si devuelve datos de prueba.
Y debido a que DependencyContainer
tiene su propio protocolo de interfaz, es fácil reemplazarlo con un código auxiliar de prueba especial que crea todas las simulaciones de dependencia como se muestra en el siguiente ejemplo.
class ServerMock: ServerInterface { var startRequestStub: () -> Void = { XCTFail() } func startRequest() { startRequestStub() } } class DependencyContainerStub: DependencyContainerInterface { var serverMock = ServerMock() var server: ServerInterface { return serverMock } } class ExampleTests: XCTestCase { var sut: ViewController! // Subject Under Test var dependencies: DependencyContainerStub! override func setUp() { dependencies = DependencyContainerStub() sut = ViewController(dependencies: dependencies) } func testServerRequestOnViewDidAppear() { let serverRequestExpectation = expectation(description: "serverRequestExpectation") dependencies.serverMock.startRequestStub = { serverRequestExpectation.fulfill() } sut.viewDidAppear() waitForExpectations(timeout: 1.0) } }
Eso es todo. Bastante manejable, ¿verdad? Pero, ¿qué hace realmente un marco DI?.
Un marco de inyección de dependencia simplemente proporciona un contenedor. Todavía tienes que escribir el resto tú mismo.
Entonces, ¿realmente necesita un marco? Todos tienen que decidir eso por sí mismos, pero en mi opinión, funciona muy bien sin uno, como se muestra arriba, ¡e incluso obtienes nuevas libertades!
Dependencias transitorias
Espere, la línea 33
crea un nuevo NextViewController
. ¿No es eso también una dependencia?
¡En efecto!
Es por eso que esto también pertenece al DependencyContainer
. Sin embargo, las dependencias de UIViewController
son algo especiales porque realmente no queremos mantener una instancia de NextViewController
en el contenedor.
Si solicitamos un NextViewController
a través de un contenedor, debería recrearse cada vez. Este componente dependiente es, por tanto, transitorio.
Además, realmente no queremos saber qué clase específica de NextViewController
es. Bastaría para la navegación si sabemos que es un UIViewController
. Esto evitará que ViewController
sepa algo sobre NextViewController
.
¡Patrón de fábrica al rescate!
El patrón de fábrica describe cómo un objeto puede crear nuevos objetos a través de una interfaz sin tener que conocer la clase específica de estos objetos.
enum Scene { case root case next } protocol DependencyContainerInterface { func scene(_ scene: Scene) -> UIViewController } class DependencyContainer: DependencyContainerInterface { init() {} func scene(_ scene: Scene) -> UIViewController { switch scene { case .root: return ViewController(dependencies: self) case .next: return NextViewController(dependencies: self) } } } class ViewController: UIViewController { let dependencies: DependencyContainerInterface init(dependencies: DependencyContainerInterface) { self.dependencies = dependencies } func navigate() { let nextViewController = dependencies.scene(.next) present(nextViewController) } } let container = DependencyContainer() let viewController = container.scene(.root)
El DependencyContainer
ahora proporciona un método de fábrica scene(_ scene: Scene) -> UIViewController
en la línea 13
. En función de la entrada, volvamos al hormigón deseada UIViewController
.
Definido en la línea 1
, Scene es una enumeración simple asignada a la clase concreta UIViewController
.
La instanciación tiene lugar en DependencyContainer
, donde el contenedor se inyecta a sí mismo como una referencia de dependencia a la nueva instancia de NextViewController
.
Esto permite que ViewController
en la línea 31
cree una instancia de NextViewController
sin depender directamente de la clase.
Por cierto, la dependencia directa del objeto raíz AppDelegate
en ViewController
también se resuelve de esa manera ahora en la línea 37
.
En lugar de depender de las clases concretas de UIViewController
, ahora se depende de la enumeración de Scene
.
Sin embargo, esto es justificable, porque un módulo Logic
o un ViewController
finalmente deben decir a qué nueva escena navegar. De esta manera, es un tipo simple e independiente.
Por lo tanto, si se agregan más controladores de vista, debe extender la enumeración de Scene
y DependencyContainer
. Los posibles conflictos de fusión deberían ser fáciles de solucionar. Generalmente.
Parámetro de configuración de escena
A menudo, desea inicializar una escena con datos adicionales, no sólo dependencias globales. Por ejemplo, el usuario en ViewController
podría hacer una entrada, que luego debería pasarse a NextViewController
para su procesamiento posterior.
Por lo tanto, este valor también es una dependencia para NextViewController
, pero solo para este y no para todos los demás controladores. Estos parámetros también deben pasarse a través de DI
. ¿Cómo funciona?.
enum Scene { case root case next(NextViewControllerSetup) } class DependencyContainer: DependencyContainerInterface { func scene(_ scene: Scene) -> UIViewController { switch scene { case .root: return ViewController(dependencies: self) case let .next(setup): return NextViewController(setup: setup, dependencies: self) } } } class ViewController: UIViewController { func navigate() { let setup = NextViewControllerSetup() let nextViewController = dependencies.scene(.next(setup)) present(nextViewController) } } struct NextViewControllerSetup {} class NextViewController: UIViewController { init(setup: NextViewControllerSetup, dependencies: DependencyContainerInterface) {} }
En pocas palabras, define una estructura como en la línea 25
que podría contener cualquier dato que desee pasar al nuevo controlador. Entonces, esta dependencia es de esperar en la línea 28
.
Por supuesto, ViewController
debe crearlo en la línea 19
y completarlo en consecuencia, antes de pasarlo como un valor asociado a la enumeración de Scene en la línea 20
.
El DependencyContainer
recupera entonces este valor asociado de la enumeración en la línea 11
y pasa como un parámetro adicional para la NextViewController
durante la instanciación.
Et voilà, ningún otro UIViewController
necesita saber nada sobre este parámetro.
Dependencias anidadas
En el primer ejemplo, había una dependencia del servidor que luego se podía pasar a cualquier número de UIViewControllers
.
Pero, ¿qué pasa si, durante el tiempo de ejecución, se produce una nueva dependencia? Por ejemplo, a partir de cierto punto, tenemos un usuario que ha iniciado sesión que se convierte en una nueva dependencia para todos los siguientes UIViewController
en el futuro.
La idea es que el nuevo objeto de usuario sea la llave con la que abrir la cerradura, es decir, crear un nuevo contenedor de dependencias.
Por supuesto, esto se hace como una dependencia del contenedor antiguo, por lo que el contenedor antiguo crea el contenedor nuevo.
class User {} enum PreLoginScene { case root } protocol PreLoginDependencyContainerInterface { func scene(_ scene: PreLoginScene) -> UIViewController func postLoginDependencies(user: User) -> PostLoginDependencyContainerInterface } class PreLoginDependencyContainer: PreLoginDependencyContainerInterface { func scene(_ scene: PreLoginScene) -> UIViewController { switch scene { case .root: return ViewController(dependencies: self) } } func postLoginDependencies(user: User) -> PostLoginDependencyContainerInterface { return PostLoginDependencyContainer(user: user) } } enum PostLoginScene { case next } protocol PostLoginDependencyContainerInterface { var user: User { get } func scene(_ scene: PostLoginScene) -> UIViewController } class PostLoginDependencyContainer: PostLoginDependencyContainerInterface { let user: User init(user: User) { self.user = user } func scene(_ scene: PostLoginScene) -> UIViewController { switch scene { case .next: return NextViewController(dependencies: self) } } } class ViewController: UIViewController { let dependencies: PreLoginDependencyContainerInterface init(dependencies: PreLoginDependencyContainerInterface) { self.dependencies = dependencies } func login() { let user = User() let postLoginDependencies = dependencies.postLoginDependencies(user: user) let nextViewController = postLoginDependencies.scene(.next) present(nextViewController) } } class NextViewController: UIViewController { let dependencies: PostLoginDependencyContainerInterface init(dependencies: PostLoginDependencyContainerInterface) { self.dependencies = dependencies } } let container = PreLoginDependencyContainer() let viewController = container.scene(.root)
Aquí se hace una distinción entre un estado de aplicación previo al inicio de sesión y un estado de aplicación posterior al inicio de sesión. En consecuencia, hay dos contenedores definidos en las líneas 13
y 36
.
Al principio, la aplicación se encuentra en el estado previo al inicio de sesión y, por lo tanto, solo existe PreLoginDependencyContainer
.
En un momento determinado, se realiza un inicio de sesión a través de la línea 58
. A partir de ahí, hay un objeto de usuario concreto, y la aplicación ahora debería cambiar al estado posterior al inicio de sesión con la transición correspondiente.
Para ello, se crea el PostLoginDependencyContainer
con el parámetro de usuario requerido en la línea 60
.
El PostLoginDependencyContainer
ahora conoce todas las escenas de los estados posteriores al inicio de sesión, que se definen en la línea 26
.
Por lo tanto, también puede crear el NextViewController
en la línea 61
y presentarlo. En cualquier caso, la dependencia del usuario existe para NextViewController
y cualquier controlador posterior.
Si aún se necesitan más dependencias, por ejemplo, el objeto del servidor, esto también debería pasarse al PostLoginDependencyContainer
. Por supuesto, mediante inyección.
Con suerte, he podido demostrar que la gestión de dependencias no es tan complicada y se puede hacer bien manualmente. No es necesario reinventar constantemente la rueda, pero a veces es más rápido que instalar una existente.
En mi proyecto DemoApp
(DAP)
, puede ver el uso manual de DI
en un entorno más complejo. Allí, la parte de fábrica se subcontrata en su propia clase de fábrica, que está en manos de DependencyContainer
como una dependencia. Pero eso es solo hielo.
Siéntete libre de comentar este artículo.
Añadir comentario