Localisateur de services - Dissiper les mythes


Il est étonnant de voir comment une pratique démontrant de bonnes performances et la convivialité d'une plate-forme est diabolisée dans un camp d'adhérents d'une autre plate-forme. Ce sort est entièrement ressenti par le modèle de Service Locator, qui est très populaire dans .Net et a une mauvaise réputation dans iOS.

Dans certains articles, ServiceLocator est négligé, le qualifiant de «vélo». D'autres soutiennent que c'est contre-modèle. Il y a ceux qui essaient de maintenir la neutralité en décrivant les côtés positifs et négatifs du localisateur. Dans une large mesure, Habrahabr lui-même contribue à ces persécutions, qui contient plusieurs articles similaires avec des références croisées. Chaque nouveau développeur qui rencontre l'implémentation de Latitude est presque immédiatement infecté par la négligence - eh bien, de nombreux développeurs ne peuvent pas critiquer la même chose sans raisons objectives.


Est ce que c'est vraiment? Pour reprendre les mots d'un personnage célèbre: «N'aimez-vous pas les chats? Oui, vous ne les avez tout simplement pas! " En fait, avez-vous essayé de manger de la viande non salée? Et brut cru? Vous n'avez pas à faire d'effort pour comprendre ce que manger du porc est mauvais, surtout le vendredi soir. Ainsi, avec le modèle Service Locator - plus la prévalence des informations brutes est élevée - plus les préjugés contre elles sont importants.

L'un des plus grands esprits de l'antiquité, Titus Lucretius Car était convaincu que le soleil tourne autour de la terre malgré le fait que dans les autres parties de son livre "Sur la nature des choses", relatives au 1er siècle avant JC. de nombreuses prévisions scientifiques précises ont été faites - de la force de gravité à la physique nucléaire. Sans remettre en cause le pouvoir d'autorité, nous montrons que certains préjugés sont facilement nivelés en utilisant un levier de la bonne longueur.

Tout d'abord, rappelez-vous ce qu'est un localisateur de services et à quoi il peut servir.

Un service est un objet autonome qui encapsule la logique métier en lui-même et peut avoir des liens avec d'autres objets. En fait, une instance de n'importe quelle classe peut agir comme un service. Très souvent, au lieu du concept de service, le concept de «manager» est utilisé. Cependant, une situation courante est lorsqu'une classe statique agit comme un gestionnaire, qui manipule les données du référentiel. L'essence du service est qu'il s'agit d'une instance de la classe, avec toutes les conséquences qui en découlent. Cela signifie qu'il ne peut pas exister dans le vide - il doit avoir un support auquel il est attaché pendant la durée de vie de l'application. Un tel transporteur est un localisateur de services.

L'utilisateur (application ou développeur) demande un service du type spécifié au localisateur de services et reçoit une instance prête à l'emploi. Très similaire à une usine abstraite, non? La différence est que chaque fois que vous demandez une instance du type spécifié au localisateur de services, vous recevrez la même instance encore et encore, qui stocke les données qui ont déjà été utilisées. Il semblerait que le service se comporte comme un singleton typique, mais ce n'est pas le cas. Vous pouvez créer autant d'instances du service que vous le souhaitez et les utiliser indépendamment à votre discrétion. Dans le même temps, chacun d'eux encapsulera les données que vous y publierez tout au long de votre vie.


Pourquoi cela pourrait-il être nécessaire? L'exemple le plus évident et le plus apprécié est le profil utilisateur. Si vous n'utilisez pas de stockage sous la forme de UserSettings ou CoreData, alors pendant toute la durée de vie de l'application, vous devrez maintenir le lien vers une instance de la classe UserProfile quelque part afin de l'utiliser sur différents écrans de l'application. De plus, si une telle instance n'est pas un singleton, elle devra être transférée d'un formulaire à un autre. La difficulté surviendra inévitablement lorsque, dans un certain cycle de développement de l'application, vous devrez créer un utilisateur temporaire ou un autre utilisateur indépendant. Sington devient immédiatement un goulot d'étranglement. Et les instances indépendantes commencent à surcharger la logique d'application, dont la complexité augmente de façon exponentielle à mesure que de plus en plus de contrôleurs intéressés par l'instance sont ajoutés.


Le localisateur de services résout élégamment ce problème: si vous avez une classe UserProfile abstraite et les classes spécifiques DefaultUserPrifile et TemporatyUserProfile héritées d'elle (avec une implémentation absolument vide, c'est-à-dire pratiquement identique), l'accès au localisateur de service vous renverra deux objets indépendants identiques.

Une autre application du localisateur est le transfert d'instances de données via une chaîne de contrôleurs: sur le premier contrôleur, vous créez (recevez) un objet et le modifiez, et sur le dernier - utilisez les données que vous avez entrées sur le premier objet. Si le nombre de contrôleurs est assez important et qu'ils sont situés sur la pile, l'utilisation d'un délégué à ces fins sera assez difficile. De même, il est souvent nécessaire à la racine de la pile d'afficher des informations qui ont changé sur son sommet immédiatement après avoir minimisé la pile (nous nous souvenons que la pile aime supprimer toutes les instances créées dans sa copie). Cependant, si en haut de la pile vous recevez un service et le modifiez, et après cela lancez le pliage de la pile, alors lorsque le contrôleur racine devient disponible pour l'utilisateur, les données modifiées seront enregistrées et seront disponibles pour l'affichage.

De manière générale, si vous utilisez le modèle "Coordinateur", comme il est décrit dans la plupart des didacticiels (par exemple, ici , ici , ici , ici ou ici ), vous devez soit placer son instance de la classe dans AppDelegate, soit transmettre un lien vers le coordinateur à tous les ViewControllers qui va l'utiliser. Nécessaire? Et pourquoi, en fait?


Personnellement, je préfère AppDelegate pour briller. Et pour hériter d'ICoordinatable et définir le champ du coordinateur - non seulement passe du temps (ce qui, comme nous le savons, équivaut à de l'argent), mais prive également la possibilité d'une programmation déclarative humaine via des storyboards. Non, ce n'est pas notre méthode.

La création d'un coordinateur en tant que service fait gracieusement les inconvénients des avantages:

  • Vous n'avez pas besoin de vous soucier du maintien de l'intégrité du coordinateur;
  • le coordinateur devient disponible tout au long de l'application, même dans les contrôleurs qui ne sont pas hérités d'ICoordinatable;
  • vous ne lancez un coordinateur que lorsque vous en avez besoin.
  • Vous pouvez utiliser le coordinateur avec le storyboard dans n'importe quel ordre (pratique pour vous).
  • L'utilisation d'un coordinateur avec un storyboard vous permet de créer des mécanismes de navigation non évidents mais efficaces.

Mais le coordinateur est la moitié du modèle «Navigateur» (le mécanisme pour se déplacer dans l'application via un chemin calculé à l'aide d'un routeur). Avec sa mise en œuvre, la complexité augmente d'un ordre de grandeur. Fonctionnalités du navigateur en conjonction avec le localisateur de services - il s'agit d'un vaste sujet distinct. Revenons à notre localisateur.

Les raisons objectives qui conduisent à la raison pour laquelle le localisateur de service est mauvais incluent la complexité de sa maintenance pour un service particulier et l'incapacité de contrôler l'état de la mémoire du localisateur.

Le mécanisme traditionnel de création d'un service est ce code:

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

ou comme ça:

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

Est-il terrible? Vous devez d'abord enregistrer le service, puis l'extraire pour l'utiliser. Dans le premier cas, il est garanti qu'elle existera lorsqu'elle sera demandée. Mais le second ne crée pas le service avant qu'il ne soit nécessaire. Si vous devez retirer le service dans de nombreux endroits, l'alternative de choix peut vous rendre fou.

Mais il est facile de transformer tout cela en un tel code:

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

Ici, l'instance de service est garantie d'exister, car si elle est absente, elle est créée par le service lui-même. Rien de plus.

La deuxième revendication du localisateur est apparemment due au fait que les développeurs qui effectuent le traçage de modèle avec C # oublient le travail du ramasse-miettes et ne prennent pas la peine de fournir la possibilité de nettoyer le localisateur d'une instance inutile, bien que ce ne soit pas du tout difficile:

ProfileService.service.remove()

Si vous ne le faites pas, lorsque vous vous tournerez vers ProfileService.service (), nous obtiendrons une instance du service avec lequel nous avons déjà travaillé à un endroit arbitraire de l'application. Mais si vous supprimez (), lors de l'accès au service, vous obtiendrez une copie propre. Dans certains cas, au lieu de remove (), vous pouvez rendre clear () en effaçant la partie prédéfinie des données et en continuant à travailler avec la même instance.

L'application de test démontre le travail coordonné de deux services: le profil utilisateur et le coordinateur. Le coordinateur n'est pas le but de l'article, mais seulement un exemple pratique.


La vidéo montre que la valeur entrée dans le champ est transmise à chaque écran suivant de l'application. Et sur l'écran final, le coordinateur est appelé pour lancer l'application depuis le premier écran. Notez que le pliage traditionnel de la pile de navigation ne se produit pas ici - il est éjecté entièrement de la mémoire, une nouvelle pile commence à sa place. Si vous commentez la ligne de suppression du service de profil, le nom d'utilisateur sera transféré sur le premier écran, comme si nous avions réduit la pile de navigation.

ProfileService.service.remove()

L'application entière se compose de deux contrôleurs de vue, avec un minimum d'opérations préparatoires.


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

Dans le premier contrôleur, une instance de profil est créée dans la méthode viewDidLoad () et les informations de nom d'utilisateur sont chargées dans le champ de saisie. Et après avoir cliqué sur le bouton Se connecter, les données de service sont à nouveau mises à jour. Après quoi, il y a une transition forcée vers la première page de l'assistant.
À l'intérieur de l'assistant, les données s'affichent à l'écran. Mais l'événement n'est lié à un bouton que sur le dernier écran du storyboard.

Il semblerait que cela puisse être compliqué? Mais au cours des 5 dernières années, j'ai rencontré constamment des développeurs qui ne comprennent pas comment tout cela fonctionne.

Bien sûr, tout le travail principal a lieu dans le localisateur et le service lui-même.

Localisateur:

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 vous regardez de plus près, vous remarquerez que vous pouvez potentiellement nettoyer toute la zone de données du localisateur en une seule action:

ServiceLocator.clear () Un

profil de service n'est pas beaucoup plus compliqué:

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

Il peut être encore simplifié en déplaçant la zone de données à l'intérieur du service lui-même. Mais sous cette forme, il devient clair le domaine de responsabilité du modèle de données et du service.

Il est possible que pour le travail de vos services vous ayez à effectuer certaines opérations préparatoires, comme c'est le cas avec la création d'un service coordinateur.

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

Ici, vous pouvez voir qu'au moment où le service est placé dans le localisateur, la pile de navigation est créée et transmise à l'instance de classe coordinatrice.

Si vous utilisez iOS 13 (apparemment ci-dessus), assurez-vous de modifier la classe SceneDelegate. Il est nécessaire d'assurer l'exécution de ce code:

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

Tout d'abord, nous extrayons la pile de navigation par défaut et l'associons à la fenêtre principale de l'application, puis ouvrons la fenêtre de démarrage de l'application avec le contrôleur StartViewController.

Le code source du scénario de test est disponible sur GitHub .

All Articles