Localizador de serviço - Mitos dissipadores


É incrível como uma prática que demonstra bom desempenho e usabilidade para uma plataforma é demonizada em um grupo de adeptos de outra plataforma. Esse destino é totalmente sentido pelo padrão do Service Locator, que é muito popular no .Net e tem uma má reputação no iOS.

Em alguns artigos, o ServiceLocator é negligenciado, chamando-o de "bicicleta". Outros argumentam que é antipadrão. Há quem tente manter a neutralidade descrevendo os lados positivo e negativo do localizador. Em grande medida, o próprio Habrahabr contribui para essas perseguições, que contêm vários artigos semelhantes com referências cruzadas. Cada novo desenvolvedor que encontra a implementação do Latitude é quase imediatamente infectado com negligência - bem, muitos desenvolvedores não podem criticar a mesma coisa sem razões objetivas.


É realmente? Nas palavras de um personagem famoso: “Você não gosta de gatos? Sim, você simplesmente não os tem! " De fato, você já tentou comer carne sem sal? E cru cru? Você não precisa se esforçar para descobrir o que comer carne de porco é ruim, especialmente na sexta à noite. Portanto, com o padrão Service Locator - quanto maior a prevalência de informações brutas - maior o preconceito contra elas.

Uma das maiores mentes da antiguidade, Titus Lucretius Car, estava convencida de que o sol gira em torno da terra, apesar do fato de que nas partes restantes de seu livro "Sobre a natureza das coisas", relacionadas ao século I aC. muitas previsões científicas precisas foram feitas - da força da gravidade à física nuclear. Sem questionar o poder da autoridade, mostramos que alguns preconceitos são facilmente eliminados usando uma alavanca do comprimento certo.

Primeiro, lembre-se do que é um localizador de serviço e para que ele pode ser usado.

Um serviço é um objeto autônomo que encapsula a lógica de negócios em si e pode ter links para outros objetos. De fato, uma instância de qualquer classe pode atuar como um serviço. Muitas vezes, em vez do conceito de serviço, o conceito de "gerente" é usado. No entanto, uma situação comum é quando uma classe estática atua como um gerente, que manipula os dados do repositório. A essência do serviço é que é uma instância da classe, com todas as conseqüências resultantes. Isso significa que ele não pode existir no vácuo - ele deve ter um suporte ao qual está conectado durante toda a vida útil do aplicativo. Essa transportadora é um localizador de serviço.

O usuário (aplicativo ou desenvolvedor) solicita um serviço do tipo especificado ao localizador de serviços e recebe uma instância pronta para uso. Muito parecido com uma fábrica abstrata, certo? A diferença é que, toda vez que você solicitar uma instância do tipo especificado ao localizador de serviço, receberá a mesma instância repetidamente, que armazena os dados que já foram usados. Parece que o serviço se comporta como um singleton típico, mas não é assim. Você pode criar quantas instâncias do serviço desejar e usá-las independentemente, a seu critério. Ao mesmo tempo, cada um deles encapsulará os dados que você publicar por toda a vida.


Por que isso pode ser necessário? O exemplo mais óbvio e amado é o perfil do usuário. Se você não usar nenhum armazenamento na forma de UserSettings ou CoreData, durante o tempo de vida do aplicativo, deverá manter o link da instância da classe UserProfile em algum lugar para usá-lo em várias telas do aplicativo. Além disso, se tal instância não for um singleton, ela terá que ser transferida de um formulário para outro. A dificuldade surgirá inevitavelmente quando, em algum ciclo de desenvolvimento do aplicativo, você precisar criar um usuário temporário ou outro usuário independente. Sington imediatamente se torna um gargalo. E instâncias independentes começam a sobrecarregar a lógica do aplicativo, cuja complexidade aumenta exponencialmente à medida que mais e mais controladores interessados ​​na instância são adicionados.


O localizador de serviço resolve elegantemente esse problema: se você tiver uma classe abstrata UserProfile e as classes específicas DefaultUserPrifile e TemporatyUserProfile herdadas dele (com uma implementação absolutamente vazia, ou seja, praticamente idêntica), o acesso ao localizador de serviço retornará dois objetos independentes idênticos.

Outra aplicação do localizador é a transferência de instâncias de dados através de uma cadeia de controladores: no primeiro controlador, você cria (recebe) um objeto e modifica-o, e no último - usa os dados inseridos no primeiro objeto. Se o número de controladores for muito grande e eles estiverem localizados na pilha, será difícil usar um delegado para esses fins. Da mesma forma, geralmente há uma necessidade na raiz da pilha de exibir informações que foram alteradas imediatamente depois de minimizar a pilha (lembramos que a pilha gosta de excluir todas as instâncias criadas em sua cópia). No entanto, se na parte superior da pilha você receber um serviço e modificá-lo, e depois disso iniciar a dobragem da pilha, quando o controlador raiz estiver disponível para o usuário, os dados modificados serão salvos e estarão disponíveis para exibição.

De um modo geral, se você usar o padrão "Coordenador", conforme descrito na maioria dos tutoriais (por exemplo, aqui , aqui , aqui , aqui ou aqui ), será necessário colocar sua instância da classe no AppDelegate ou passar um link para o coordenador para todos os ViewControllers que vai usá-lo. Necessário? E por que, na verdade?


Pessoalmente, prefiro o AppDelegate a brilhar. E herdar do ICoordinatable e definir o campo do coordenador - não apenas gasta tempo (que, como sabemos, é equivalente a dinheiro), mas também priva a possibilidade de programação declarativa humana através de storyboards. Não, este não é o nosso método.

Criar um coordenador como um serviço normalmente cria as desvantagens das vantagens:

  • Você não precisa se preocupar em manter a integridade do coordenador;
  • o coordenador fica disponível em todo o aplicativo, mesmo nos controladores que não são herdados do ICoordenável;
  • você inicia um coordenador somente quando precisar.
  • Você pode usar o coordenador junto com o storyboard em qualquer ordem (conveniente para você).
  • O uso de um coordenador com um storyboard permite criar mecanismos de navegação não óbvios, mas eficazes.

Mas o coordenador é metade do padrão "Navegador" (o mecanismo para mover o aplicativo por um caminho calculado usando um roteador). Com sua implementação, a complexidade aumenta em uma ordem de magnitude. Recursos do navegador em conjunto com o localizador de serviço - este é um tópico amplo e separado. Vamos voltar ao nosso localizador.

Os motivos objetivos que levam ao motivo pelo qual o Localizador de serviço é ruim incluem a complexidade de sua manutenção para um serviço específico e a incapacidade de controlar o estado da memória do localizador.

O mecanismo tradicional para criar um serviço é 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()

ou assim:

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

Ele é terrível? Primeiro você precisa registrar o serviço e, em seguida, extraí-lo para usá-lo. No primeiro caso, é garantido que existirá quando solicitado. Mas o segundo não cria o serviço antes que seja necessário. Se você precisar executar o serviço em muitos lugares, a alternativa escolhida pode deixá-lo louco.

Mas é fácil transformar tudo isso em tal código:

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

Aqui é garantida a existência da instância do serviço, porque, se estiver ausente, é criada pelo próprio serviço. Nada a mais.

Aparentemente, a segunda reivindicação do localizador se deve ao fato de que os desenvolvedores que fazem o rastreamento de padrões com C # esquecem o trabalho do coletor de lixo e não se preocupam em fornecer a capacidade de limpar o localizador de uma instância desnecessária, embora isso não seja de todo difícil:

ProfileService.service.remove()

Se você não fizer isso, quando voltar para ProfileService.service (), obteremos uma instância do serviço com o qual já trabalhamos anteriormente em um local arbitrário no aplicativo. Mas se você remover (), ao acessar o serviço, receberá uma cópia limpa. Em alguns casos, em vez de remove (), você pode deixar clear () limpando a parte predefinida dos dados e continuando a trabalhar com a mesma instância.

O aplicativo de teste demonstra o trabalho coordenado de dois serviços: perfil do usuário e coordenador. O coordenador não é o objetivo do artigo, mas apenas um exemplo conveniente.


O vídeo mostra que o valor inserido no campo é transmitido para cada tela subseqüente do aplicativo. E na tela final, o coordenador é chamado para iniciar o aplicativo a partir da primeira tela. Observe que a dobra tradicional da pilha de navegação não ocorre aqui - ela é ejetada inteiramente da memória, uma nova pilha começa em seu lugar. Se você comentar a linha para excluir o serviço de perfil, o nome de usuário será transferido para a primeira tela, como se tivéssemos minimizado a pilha de navegação.

ProfileService.service.remove()

O aplicativo inteiro consiste em dois controladores de exibição, com um mínimo de operações preparatórias.


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

No primeiro controlador, uma instância de perfil é criada no método viewDidLoad () e as informações do nome do usuário são carregadas no campo de entrada. E depois de clicar no botão Login, os dados do serviço são atualizados novamente. Após o qual há uma transição forçada para a primeira página do assistente.
Dentro do assistente, os dados são exibidos na tela. Mas o evento está vinculado a um botão apenas na última tela do storyboard.

Parece que pode ser complicado? Mas, nos últimos 5 anos, encontrei constantemente desenvolvedores que não entendem como tudo funciona.

Obviamente, todo o trabalho principal ocorre no localizador e no próprio serviço.

Localizador:

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


Se você olhar mais de perto, perceberá que é possível limpar toda a área de dados do localizador com uma ação:

ServiceLocator.clear () Um

perfil de serviço não é muito mais 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 = ""
}

Pode ser ainda mais simplificado movendo a área de dados dentro do próprio serviço. Mas, dessa forma, fica clara a área de responsabilidade do modelo e serviço de dados.

É possível que, para o trabalho de seus serviços, você precise executar algumas operações preparatórias, como é o caso da criação de um serviço de coordenador.

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

Aqui você pode ver que, no momento em que o serviço é colocado no localizador, a pilha de navegação é criada e passada para a instância da classe de coordenador.

Se você estiver usando o iOS 13 (aparentemente acima), modifique a classe SceneDelegate. É necessário garantir a execução deste 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()
    }

Primeiro, extraímos a pilha de navegação padrão e a associamos à janela principal do aplicativo e, em seguida, abrimos a janela inicial do aplicativo com o controlador StartViewController.

O código fonte do caso de teste está disponível no GitHub .

All Articles