Injeção de dependência com DITranquillity

A Injeção de Dependências é um padrão bastante popular que permite configurar o sistema de maneira flexível e criar corretamente as dependências dos componentes deste sistema. Graças à digitação, o Swift permite usar estruturas convenientes com as quais você pode descrever brevemente o gráfico de dependência. Hoje eu quero falar um pouco sobre um desses frameworks - DITranquillity .


Este tutorial abordará os seguintes recursos da biblioteca:


  • Registo de tipo
  • Implantação de inicialização
  • Incorporando em uma variável
  • Dependências cíclicas de componentes
  • Usando a biblioteca com UIStoryboard

Descrição do componente


O aplicativo consistirá nos seguintes componentes principais: ViewController , Router , Presenter , Networking - esses são componentes bastante comuns em qualquer aplicativo iOS.


Estrutura de componentes

ViewController e o Router serão introduzidos um no outro ciclicamente.


Preparação


Primeiro, crie um Aplicativo de exibição única no Xcode, adicione DITranquillity usando CocoaPods . Crie a hierarquia necessária de arquivos, adicione um segundo controlador ao Main.storyboard e conecte-o usando o StoryboardSegue . Como resultado, a seguinte estrutura de arquivo deve ser obtida:


Estrutura de arquivo

Crie dependências nas classes da seguinte maneira:


Declaração de componente
 protocol Presenter: class { func getCounter(completion: @escaping (Int) -> Void) } class MyPresenter: Presenter { private let networking: Networking init(networking: Networking) { self.networking = networking } func getCounter(completion: @escaping (Int) -> Void) { // Implementation } } 

 protocol Networking: class { func fetchData(completion: @escaping (Result<Int, Error>) -> Void) } class MyNetworking: Networking { func fetchData(completion: @escaping (Result<Int, Error>) -> Void) { // Implementation } } 

 protocol Router: class { func presentNewController() } class MyRouter: Router { unowned let viewController: ViewController init(viewController: ViewController) { self.viewController = viewController } func presentNewController() { // Implementation } } 

 class ViewController: UIViewController { var presenter: Presenter! var router: Router! } 

Limitações


Diferentemente de outras classes, o ViewController não é criado por nós, mas pela biblioteca UIKit dentro da implementação UIStoryboard.instantiateViewController , portanto, usando o storyboard, não podemos injetar dependências nos herdeiros do UIViewController usando o inicializador. O mesmo acontece com os herdeiros do UIView e do UITableViewCell .


Observe que os objetos ocultos atrás dos protocolos são incorporados em todas as classes. Essa é uma das principais tarefas da implementação de dependências - criar dependências não nas implementações, mas nas interfaces. Isso ajudará no futuro a fornecer diferentes implementações de protocolo para reutilizar ou testar componentes.


Injeção de Dependência


Depois que todos os componentes do sistema são criados, prosseguimos para a conexão dos objetos entre si. No DITranquillity, o ponto de partida é o DIContainer , que adiciona o registro usando o método container.register(...) . Para separar as dependências em partes, DIFramework e DIPart , os quais devem ser implementados. Por conveniência, criaremos apenas uma classe ApplicationDependency , que implementará o DIFramework e servirá como local de registro para todas as dependências. A interface DIFramework requer que você implemente apenas um método - load(container:) .


 class ApplicationDependency: DIFramework { static func load(container: DIContainer) { // registrations will be placed here } } 

Vamos começar com a inscrição mais simples que não tem dependências - MyNetworking


 container.register(MyNetworking.init) 

Este registro usa a implementação através do inicializador. Apesar do fato de o componente em si não ter dependências, o inicializador deve ser fornecido para deixar claro para a biblioteca como criar o componente.


Da mesma forma, registre MyPresenter e MyRouter .


 container.register1(MyPresenter.init) container.register1(MyRouter.init) 

Nota: Observe que não é o register usado, mas o register1 . Infelizmente, isso é necessário para indicar se o objeto possui uma e apenas uma dependência no inicializador. Ou seja, se houver 0 ou duas ou mais dependências, você só precisará usar o register . Essa restrição é um bug do Swift versão 4.0 e superior.


É hora de registrar nosso ViewController . Ele injeta objetos não através do inicializador, mas diretamente na variável, portanto a descrição do registro será um pouco mais.


 container.register(ViewController.self) .injection(cycle: true, \.router) .injection(\.presenter) 

A sintaxe do formulário \.presenter é SwiftKeyPath, graças à qual é possível implementar de forma concisa as dependências. Como o Router e o ViewController ciclicamente dependentes um do outro, você deve indicar explicitamente isso usando cycle: true . A própria biblioteca pode resolver essas dependências sem uma indicação explícita, mas esse requisito foi introduzido para que a pessoa que está lendo o gráfico entenda imediatamente que existem ciclos na cadeia de dependências. Observe também que NOT ViewController.init , mas ViewController.self . Isso foi descrito acima na seção Limitações .


Você também deve registrar o UIStoryboard usando um método especial.


 container.registerStoryboard(name: "Main") 

Agora, descrevemos todo o gráfico de dependência para uma tela. Mas ainda não há acesso a este gráfico. Você deve criar um DIContainer que permita acessar os objetos nele.


 static let container: DIContainer = { let container = DIContainer() // 1 container.append(framework: ApplicationDependency.self) // 2 assert(container.validate(checkGraphCycles: true)) // 3 return container }() 

  1. Inicialize o contêiner
  2. Adicione uma descrição do gráfico a ele.
  3. Verificamos que fizemos tudo certo. Se um erro for cometido, o aplicativo falhará não durante a resolução de dependências, mas imediatamente ao criar um gráfico

Então você precisa tornar o contêiner o ponto de partida do aplicativo. Para fazer isso, implementamos o método didFinishLaunchingWithOptions no didFinishLaunchingWithOptions em vez de especificar Main.storyboard como o ponto de Main.storyboard nas configurações do projeto.


 func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { window = UIWindow(frame: UIScreen.main.bounds) let storyboard: UIStoryboard = ApplicationDependency.container.resolve() window?.rootViewController = storyboard.instantiateInitialViewController() window?.makeKeyAndVisible() return true } 

Lançamento


No primeiro início, ocorrerá uma queda e a validação falhará pelos seguintes motivos:


  • O contêiner não encontrará os tipos Router , Presenter , Networking , porque registramos apenas objetos. Se queremos dar acesso não a implementações, mas a interfaces, devemos especificar explicitamente as interfaces
  • O contêiner não entende como resolver a dependência cíclica, porque é necessário indicar explicitamente quais objetos não devem ser recriados toda vez que o gráfico for resolvido.

É simples corrigir o primeiro erro - existe um método especial que permite especificar em quais protocolos o método no contêiner está disponível.


 container.register(MyNetworking.init) .as(check: Networking.self) {$0} 

Descrevendo o registro da seguinte forma, dizemos: o objeto MyNetworking ser acessado via protocolo de Networking . Isso deve ser feito para todos os objetos ocultos nos protocolos. {$0} para a verificação correta dos tipos pelo compilador.


O segundo erro é um pouco mais complicado. É necessário usar o chamado scope , que descreve com que frequência o objeto é criado e quanto vive. Para cada registro participando de uma dependência circular, você deve especificar um scope igual a objectGraph . Isso deixará claro para o contêiner que, durante a resolução, é necessário reutilizar os mesmos objetos criados, em vez de criar a cada vez. Assim, verifica-se:


 container.register(ViewController.self) .injection(cycle: true, \.router) .injection(\.presenter) .lifetime(.objectGraph) container.register1(MyRouter.init) .as(check: Router.self) {$0} .lifetime(.objectGraph) 

Após reiniciar, o contêiner passa na validação com êxito e nosso ViewController é aberto com as dependências criadas. Você pode colocar um ponto de interrupção no viewDidLoad e garantir.


Transição entre telas


Em seguida, crie duas classes pequenas, SecondViewController e SecondPresenter , adicione SecondViewController ao storyboard e crie um Segue entre elas com o identificador "RouteToSecond" , que permite abrir o segundo controlador a partir do primeiro.


Adicione mais dois registros ao nosso ApplicationDependency para cada uma das novas classes:


 container.register(SecondViewController.self) .injection(\.secondPresenter) container.register(SecondPresenter.init) 

.as há necessidade de especificar .as , porque não SecondPresenter o SecondPresenter por trás do protocolo, mas usamos a implementação diretamente. Então, no método viewDidAppear do primeiro controlador, chamamos performSegue(withIdentifier: "RouteToSecond", sender: self) , start, o segundo controlador é aberto, no qual a dependência do secondPresenter deve ser afixada. Como você pode ver, o contêiner viu a criação de um segundo controlador a partir do UIStoryboard e UIStoryboard êxito as dependências.


Conclusão


Essa biblioteca permite que você trabalhe convenientemente com dependências cíclicas, storyboards e use totalmente a inferência automática de tipo no Swift, que fornece uma sintaxe muito curta e flexível para descrever o gráfico de dependência.


Referências


Código de amostra completo na biblioteca do github


DITranquillity no github


Artigo em inglês

Source: https://habr.com/ru/post/pt450788/


All Articles