Localizador de servicios - Disipando mitos


Es sorprendente cómo se demoniza una práctica que demuestra un buen rendimiento y usabilidad para una plataforma en un campo de seguidores de otra plataforma. Este destino se siente completamente por el patrón de Service Locator, que es muy popular en .Net y tiene una mala reputación en iOS.

En algunos artículos, ServiceLocator se descuida, llamándolo "bicicleta". Otros argumentan que es antipatrón. Hay quienes intentan mantener la neutralidad describiendo los lados positivo y negativo del localizador. En gran medida, el propio Habrahabr contribuye a estas persecuciones, que contienen varios artículos similares con referencias cruzadas. Cada nuevo desarrollador que se encuentra con la implementación de Latitude se infecta casi inmediatamente con negligencia; bueno, muchos desarrolladores no pueden criticar lo mismo sin razones objetivas.


¿Es realmente? En palabras de un personaje famoso, “¿No te gustan los gatos? ¡Sí, simplemente no los tienes! " De hecho, ¿has intentado comer carne sin sal? ¿Y crudo crudo? No tiene que hacer un esfuerzo para descubrir qué comer carne de cerdo es malo, especialmente el viernes por la noche. Entonces, con el patrón Localizador de servicios, cuanto mayor es la prevalencia de la información en bruto, mayor es el prejuicio contra ella.

Una de las mentes más grandes de la antigüedad, Titus Lucretius Car estaba convencido de que el sol gira alrededor de la tierra a pesar del hecho de que en las partes restantes de su libro "Sobre la naturaleza de las cosas", relacionado con el siglo I a. C. Se han hecho muchas predicciones científicas precisas, desde la fuerza de la gravedad hasta la física nuclear. Sin cuestionar el poder de la autoridad, mostramos que algunos prejuicios se nivelan fácilmente usando una palanca de la longitud correcta.

Primero, recuerde qué es un localizador de servicios y para qué se puede utilizar.

Un servicio es un objeto autónomo que encapsula la lógica empresarial dentro de sí mismo y puede tener enlaces a otros objetos. De hecho, una instancia de cualquier clase puede actuar como un servicio. Muy a menudo, en lugar del concepto de servicio, se utiliza el concepto de "gerente". Sin embargo, una situación común es cuando una clase estática actúa como un administrador, que manipula los datos del repositorio. La esencia del servicio es que es una instancia de la clase, con todas las consecuencias resultantes. Esto significa que no puede existir en el vacío: debe tener un soporte al que esté conectado durante toda la vida útil de la aplicación. Tal operador es un localizador de servicios.

El usuario (aplicación o desarrollador) solicita un servicio del tipo especificado del localizador de servicios y recibe una instancia lista para usar. Muy similar a una fábrica abstracta, ¿verdad? La diferencia es que cada vez que solicita una instancia del tipo especificado del localizador de servicios, recibirá la misma instancia una y otra vez, que almacena los datos que ya se han utilizado. Parece que el servicio se comporta como un singleton típico, pero esto no es así. Puede crear tantas instancias del servicio como desee y utilizarlas de forma independiente a su discreción. Al mismo tiempo, cada uno de ellos encapsulará los datos que publique allí a lo largo de su vida.


¿Por qué podría ser esto necesario? El ejemplo más obvio y querido es el perfil del usuario. Si no utiliza ningún almacenamiento en forma de UserSettings o CoreData, durante la vigencia de la aplicación deberá mantener el enlace a la instancia de la clase UserProfile en algún lugar para usarlo en varias pantallas de la aplicación. Además, si dicha instancia no es un singleton, deberá transferirse de una forma a otra. La dificultad inevitablemente surgirá cuando en algún ciclo de desarrollo de la aplicación tenga que crear un usuario temporal o un usuario independiente. Sington se convierte inmediatamente en un cuello de botella. Y las instancias independientes comienzan a sobrecargar la lógica de la aplicación, cuya complejidad aumenta exponencialmente a medida que se agregan más y más controladores interesados ​​en la instancia.


El localizador de servicios resuelve este problema con elegancia: si tiene una clase abstracta de UserProfile y las clases específicas DefaultUserPrifile y TemporatyUserProfile heredan de ella (con una implementación absolutamente vacía, es decir, prácticamente idéntica), acceder al localizador de servicios le devolverá dos objetos independientes idénticos.

Otra aplicación del localizador es la transferencia de instancias de datos a través de una cadena de controladores: en el primer controlador, crea (recibe) un objeto y lo modifica, y en el último: use los datos que ingresó en el primer objeto. Si el número de controladores es bastante grande y están ubicados en la pila, entonces será bastante difícil usar un delegado para estos fines. De manera similar, a menudo existe la necesidad en la raíz de la pila de mostrar información que cambió en su parte superior inmediatamente después de minimizar la pila (recordamos que a la pila le gusta eliminar todas las instancias creadas en su copia). Sin embargo, si en la parte superior de la pila recibe un servicio y lo modifica, y luego inicia el plegado de la pila, cuando el controlador raíz esté disponible para el usuario, los datos modificados se guardarán y estarán disponibles para su visualización.

En términos generales, si usa el patrón "Coordinador", como se describe en la mayoría de los tutoriales (por ejemplo, aquí , aquí , aquí , aquí o aquí ), debe colocar su instancia de la clase en AppDelegate o pasar un enlace al coordinador a todos los ViewControllers que lo usaré ¿Necesario? ¿Y por qué, en realidad?


Personalmente, prefiero que AppDelegate brille limpiamente. Y para heredar de ICoordinatable y establecer el campo coordinador, no solo pasa tiempo (que, como sabemos, es equivalente al dinero), sino que también priva la posibilidad de programación declarativa humana a través de guiones gráficos. No, este no es nuestro método.

Crear un coordinador como un servicio hace que las desventajas de las ventajas sean graciosas:

  • No necesita preocuparse por mantener la integridad del coordinador;
  • el coordinador está disponible en toda la aplicación, incluso en aquellos controladores que no se heredan de ICoordinatable;
  • Usted inicia un coordinador solo cuando lo necesita.
  • Puede usar el coordinador junto con el guión gráfico en cualquier orden (conveniente para usted).
  • El uso de un coordinador con un guión gráfico le permite crear mecanismos de navegación no obvios pero efectivos.

Pero el coordinador es la mitad del patrón "Navegador" (el mecanismo para moverse por la aplicación a través de una ruta calculada usando un enrutador). Con su implementación, la complejidad aumenta en un orden de magnitud. Características del navegador junto con el localizador de servicios: este es un tema amplio aparte. Volvamos a nuestro localizador.

Las razones objetivas que conducen a la razón por la cual el Localizador de servicios es malo incluyen la complejidad de su mantenimiento para un servicio en particular y la incapacidad de controlar el estado de la memoria del localizador.

El mecanismo tradicional para crear un servicio es este código:

...
 ServiceLocator.shared.addService(CurrentUserProvider() as CurrentUserProviding)
...
let userProvider: UserProviding? =  ServiceLocator.shared.getService()
guard let provider =  userProvider else { return }
self.user = provider.currentUser()

o así:

 if let_:ProfileService = ServiceLocator.service() {
            ServiceLocator.addService(ProfileService())
        }
 let service:ProfileService = ServiceLocator.service()!
  service.update(name: "MyName")

¿Es terrible? Primero debe registrar el servicio y luego extraerlo para poder usarlo. En el primer caso, se garantiza que existirá cuando se solicite. Pero el segundo no crea el servicio antes de que sea necesario. Si necesita retirar el servicio en muchos lugares, entonces la alternativa de elección puede volverlo loco.

Pero es fácil convertir todo esto en dicho código:

ProfileService.service.update(name: "MyName")

Aquí se garantiza que la instancia de servicio existe, porque si está ausente, es creada por el propio servicio. Nada extra

El segundo reclamo del localizador aparentemente se debe al hecho de que los desarrolladores que realizan el rastreo de patrones con C # se olvidan del trabajo del recolector de basura y no se molestan en proporcionar la capacidad de limpiar el localizador de una instancia innecesaria, aunque esto no es del todo difícil:

ProfileService.service.remove()

Si no hace esto, cuando recurra a ProfileService.service (), obtendremos una instancia del servicio con el que ya trabajamos anteriormente en un lugar arbitrario en la aplicación. Pero si elimina (), al acceder al servicio, obtendrá una copia limpia. En algunos casos, en lugar de remove (), puede hacer clear () borrando la parte predefinida de los datos y continuando trabajando con la misma instancia.

La aplicación de prueba demuestra el trabajo coordinado de dos servicios: perfil de usuario y coordinador. El coordinador no es el propósito del artículo, sino solo un ejemplo conveniente.


El video muestra que el valor ingresado en el campo se transmite a cada pantalla posterior de la aplicación. Y en la pantalla final, se llama al coordinador para iniciar la aplicación desde la primera pantalla. Tenga en cuenta que el plegado tradicional de la pila de navegación no ocurre aquí: se expulsa por completo de la memoria, una nueva pila comienza en su lugar. Si comenta la línea para eliminar el servicio de perfil, el nombre de usuario se transferirá a la primera pantalla, como si hubiéramos minimizado la pila de navegación.

ProfileService.service.remove()

La aplicación completa consta de dos controladores de vista, con un mínimo de operaciones preparatorias.


StartViewController:

import UIKit

class StartViewController: UIViewController {

    @IBOutlet private weak var nameField: UITextField!

    override func viewDidLoad() {
        super.viewDidLoad()
        self.nameField.text = ProfileService.service.info.name
    }

    @IBAction func startAction(_ sender: UIButton) {
        ProfileService.service.update(name: self.nameField.text ?? "")
        CoordinatorService.service.coordinator.startPageController()
    }

}

PageViewController:

import UIKit

class PageViewController: UIViewController {

    @IBOutlet private weak var nameLabel: UILabel!

    override func viewDidLoad() {
        super.viewDidLoad()
        self.nameLabel.text = ProfileService.service.info.name
    }


    @IBAction func finishAction(_ sender: UIButton) {
        ProfileService.service.remove()
        CoordinatorService.service.coordinator.start()
    }
}

En el primer controlador, se crea una instancia de perfil en el método viewDidLoad (), y la información del nombre de usuario se carga en el campo de entrada. Y después de hacer clic en el botón Iniciar sesión, los datos del servicio se actualizan nuevamente. Después de lo cual hay una transición forzada a la primera página del asistente.
Dentro del asistente, los datos se muestran en la pantalla. Pero el evento está vinculado a un botón solo en la última pantalla del guión gráfico.

Parece que puede ser complicado? Pero en los últimos 5 años, me he encontrado constantemente con desarrolladores que no entienden cómo funciona todo.

Por supuesto, todo el trabajo principal se lleva a cabo en el localizador y en el servicio en sí.

Locador:

import Foundation

protocol IService {
    static var service: Self {get}
    
    func clear()
    func remove()
}
protocol IServiceLocator {
    func service<T>() -> T?
}

final class ServiceLocator: IServiceLocator {
    
    private static let instance = ServiceLocator()
    private lazy var services: [String: Any] = [:]
    
    // MARK: - Public methods
    class func service<T>() -> T? {
        return instance.service()
    }
    
    class func addService<T>(_ service: T) {
        return instance.addService(service)
    }
    
    class func clear() {
        instance.services.removeAll()
    }
    
    class func removeService<T>(_ service: T) {
        instance.removeService(service)
    }
    
    func service<T>() -> T? {
        let key = typeName(T.self)
        return services[key] as? T
    }
    
    // MARK: - Private methods
    private fun caddService<T>(_ service: T) {
        let key = typeName(T.self)
        services[key] = service
    }
    
    private func removeService<T>(_ service: T) {
        let key = typeName(T.self)
        services.removeValue(forKey: key)
    }
    
    private func typeName(_ some: Any) -> String {
        return (some isAny.Type) ? "\(some)" : "\(type(of: some))"
    }
}


Si observa más de cerca, notará que puede limpiar todo el área de datos del localizador con una sola acción:

ServiceLocator.clear () Un

perfil de servicio no es mucho más complicado:

import UIKit

final class ProfileService: IService {
    
    private (set) var info = ProfileInfo()
    
    class var service: ProfileService {
        if let service: ProfileService = ServiceLocator.service() {
            return service
        }
        
        let service = ProfileService()
        ServiceLocator.addService(service)
        return service
    }

    func clear() {
        self.info = ProfileInfo()
    }

    func remove() {
        ServiceLocator.removeService(self)
    }
    
    func update(name: String) {
        self.info.name = name
    }
}

struct ProfileInfo {
    varname = ""
}

Se puede simplificar aún más moviendo el área de datos dentro del propio servicio. Pero de esta forma, queda claro el área de responsabilidad del modelo de datos y el servicio.

Es posible que para el trabajo de sus servicios necesite llevar a cabo algunas operaciones preparatorias, como es el caso de la creación de un servicio coordinador.

import UIKit

final class CoordinatorService: IService {
    
    private (set)var coordinator: MainCoordinator!
    
    var navController: UINavigationController {
        return self.coordinator.navigationController
    }
    
    class var service: CoordinatorService {
        if let service: CoordinatorService = ServiceLocator.service() {
            return service
        }
        
        let service = CoordinatorService()
        service.load()
        ServiceLocator.addService(service)
        return service
    }

    func clear() {
    }

    func remove() {
        ServiceLocator.removeService(self)
    }
    
    // MARK - Private
    private func load() {
        let nc                    = UINavigationController()
        nc.navigationBar.isHidden = true
        self.coordinator          = MainCoordinator(navigationController:nc)
    }
}

Aquí puede ver que en el momento en que el servicio se coloca en el localizador, la pila de navegación se crea y se pasa a la instancia de clase del coordinador.

Si está utilizando iOS 13 (aparentemente arriba), asegúrese de modificar la clase SceneDelegate. Es necesario garantizar la ejecución de este código:

 func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        guard let scene = (scene as? UIWindowScene) else { return }
        let window = UIWindow(windowScene: scene)
        window.rootViewController = CoordinatorService.service.navController
        self.window = window
        CoordinatorService.service.coordinator.start()
        window.makeKeyAndVisible()
    }

Primero, extraemos la pila de navegación predeterminada y la asociamos con la ventana principal de la aplicación, y luego abrimos la ventana de inicio de la aplicación con el controlador StartViewController.

El código fuente para el caso de prueba está disponible en GitHub .

All Articles