Fábrica para DI

Fábrica para DI post thumbnail image

Este é um cross-post do meu blogue. Se desejar, pode ir aaqui para o ler.

No meu post anterior sobre Injeção de Dependência no iOS, mencionei que factories podem ser usadas para DI e que é simples de implementar. Hoje, vamos descobrir como fazer isso, quais opções estão disponíveis e por que pode ser mais vantajoso do que usar bibliotecas como Swinject.

NOTA: Nesta publicação, parto do princípio de que já está familiarizado com o conceito de DI (mas pode saber mais sobre o assunto na minha publicação anterior referida acima).

O Fábrica é um padrão de conceção normalmente utilizado no desenvolvimento de software para a criação de objectos. Encapsula a lógica de criação de objectos numa classe ou método separado, com base em parâmetros ou condições específicos.

enum ViewFactory {
static func createView() -> UIView {
UIView()
}
}
let view = ViewFactory.createView()

Permite extrair a lógica de criação de objectos do ponto de chamada e obedece a Princípio da responsabilidade única. Isto ajuda a manter a lógica de criação de objectos organizada e centralizada, facilitando a manutenção, atualização e teste.

O padrão Factory promove o acoplamento livre entre o código do cliente e os objectos que estão a ser criados. Os clientes dependem apenas da fábrica e não de classes concretas, o que permite uma manutenção mais fácil e reduz as dependências entre diferentes partes da base de código.

As fábricas podem ser muito úteis para implementar DI na sua aplicação. Por exemplo, pode criar um ecrã de início de sessão com a fábrica e injetar aí todas as dependências necessárias:

protocol AuthService {}
enum AuthServiceFactory {
static func create() -> AuthService {
AuthServiceImpl()
}
}
enum LoginScreenFactory {
static func createLoginScreen() -> UIViewController {
let authService = AuthServiceFactory.create()
let loginVM = LoginViewModel(authService: authService)
return LoginViewController(viewModel: viewModel)
}
}
let loginVC = LoginScreenFactory.create()
// presenting your screen

Como pode ver, a fábrica cria todas as dependências para este módulo MVVM e injeta-as para que tenhamos o view controller pronto para apresentar como resultado.

Pode utilizar fábricas para criar ecrãs, serviços ou qualquer coisa de que necessite na sua aplicação.

Em aplicações iOS mais pequenas, a utilização de fábricas pode ser uma solução viável para a DI. As fábricas podem fornecer uma simples e leve forma de lidar com DI sem a necessidade de terceiros externos bibliotecas ou estruturas externas.

Embora as fábricas possam ser uma abordagem adequada para gerir dependências em aplicações iOS mais pequenas, podem não ser suficientes para projectos maiores e mais complexos.

Em grandes projectos iOS com inúmeras dependências e gráficos de dependências complexos, a gestão de dependências através de fábricas pode tornar-se mais difícil. Tem de controlar o ciclo de vida das suas dependências para as gerir corretamente, e não é o que temos de imediato com o padrão de fábrica. Para além disso, seria útil detetar ciclos de dependência e simule dependências facilmente para testes unitários. Embora seja possível implementar estas funcionalidades, pode ser complicado, especialmente para aqueles que não estão familiarizados com o funcionamento dos contentores DI.

Vamos resumir o que esperamos de uma solução DI adequada e o que as fábricas realmente são capazes de fazer:

  • Segurança em tempo de compilação
    Quando faz DI, esperamos que seja seguro em tempo de compilação. Isto significa que o código não será compilado se existirem erros relacionados com a injeção de dependências, tais como dependências em falta ou incompatíveis, ou se as dependências não estiverem registadas no contentor. Queremos evitar falhas em tempo de execução, e isso pode ser conseguido evitando resolvedores de tempo de execução como o Swinject. À medida que a aplicação cresce, torna-se cada vez mais difícil garantir que todos os módulos utilizem apenas dependências registadas.
  • Preguiça
    Queremos que as nossas dependências sejam preguiçosas. Isso significa que os objetos são criados e inicializados quando são realmente necessários, em vez de criar e inicializar todos os objetos de uma vez no início da aplicação. Isso pode ajudar a melhorar o desempenho de uma aplicação, pois reduz a quantidade de memória e poder de processamento necessários na inicialização. Isso é especialmente importante em aplicações maiores, onde pode haver um grande número de objetos e dependências a serem gerenciados.
  • Controle o âmbito/ciclo de vida (+ redefina facilmente o âmbito quando necessário)
    O contentor DI deve ser responsável pela criação e inicialização de objectos, pela gestão do seu estado durante o tempo de execução e pela sua destruição quando já não são necessários. Pode ajudar a reduzir a utilização de memória, evitando que existam dependências não utilizadas na memória e libertando outros recursos do sistema quando já não são necessários.
  • Simule/substitua facilmente implementações para testes
    Esperamos ser capazes de substituir facilmente as dependências reais de um objeto por dependências específicas do teste, conhecidas como mocks ou teste duplos. Isto pode ajudar a isolar o comportamento de um objeto ou função durante os testes e garantir que está a funcionar corretamente.
  • Detetar ciclos de dependência
    Esperamos que possa identificar situações em que dois ou mais objectos têm dependências circulares, em que cada objeto depende de outro objeto no ciclo. Isto pode ser um problema porque pode levar a loops infinitos ou outros erros que podem fazer com que a aplicação falhe. Quanto maior for o crescimento de uma aplicação, mais crucial ela se torna.

Usando o Padrão de fábrica dá-nos apenas 2 das 5 funcionalidades necessárias:

  • ✅ Segurança em tempo de compilação
  • ✅ Preguiça
  • Controle o âmbito/ciclo de vida
  • Facilmente simule/substitua implementações para testes
  • Detecte ciclos de dependência

Como é que podemos alcançar todos estes objectivos?

Uma opção é implementar uma solução personalizada, mas uma abordagem mais direta é utilizar uma solução pronta a usar. E se eu lhe disser que a solução é um Fábrica novamente, mas não o padrão, mas o biblioteca? – Biblioteca da fábrica. Criada em 2022, esta biblioteca leva o padrão de fábrica para o próximo nível, que eu chamaria de Padrão de fábrica com esteróides.

Um exemplo simples de utilização do Factory:

extension Container {

var service1: Factory<Service1> {
self { ServiceImpl1() }
.singleton
}

var service2: Factory<Service2> {
self { ServiceImpl2(service1: self.service1()) }
}
}
// you can use it like that later for DI
let vm = ScreenViewModel(service1: Container.shared.service1(),
service2: Container.shared.service2())

Parece bonito e simples, certo? Declara as suas dependências como propriedades do Contentor. Envolve a sua dependência real no tipo Factory e simplesmente endereça esta propriedade com () para depois resolver a instância. Isso funciona devido à propriedade callAsFunction introduzida no Swift 5.2.

Vamos verificar nossa lista de requisitos novamente para a biblioteca Factory:

  • Segurança em tempo de compilação
    Se declarar uma propriedade Factory, pode ter a certeza de que será capaz de resolver a instância.
  • Preguiça
    Como pode ver, utilizámos propriedades calculadas, o que dá origem a preguiça.
  • Controle o ciclo de vida/âmbito
    Fábrica fornece nos com 5 escopos: Singleton, Cached, Shared, Graph, Unique.
    Pode criar os seus próprios âmbitos personalizados.
    Pode repor as caches de âmbitos individuais.
  • ✅ Usar mocks para testes é muito fácil
    Basta registar o mock para atualizar a instância original
struct ContentView_Previews: PreviewProvider {     
static var previews: some View {
let _ = Container.myService.register { MockService2() }
ContentView()
}
}
  • Detetar ciclos de dependência
    O Factory oferece a capacidade de detetar ciclos no gráfico de dependências, mas apenas em tempo de execução. Quando a sua dependência vai ser resolvida mais do que dependencyChainTestMax = 8 vezes durante um ciclo de resolução, a biblioteca accionará o erro fatal. O valor máximo pode ser alterado se necessário.

Para além disso, Fábrica é leve e fácil de compreender. Também fornece InjectedObject para o SwiftUI se o utilizar no seu projeto.

Como reparou, temos um Fábrica para o fecho que cria a instância de dependência como resultado. Fornece-nos algum açúcar sintático. Pedimos simplesmente ao contentor envolvente que crie um Factory devidamente ligado para nós utilizando self.callAsFunction { ... }.

extension Container {
var sugared: Factory<MyServiceType> {
self { MyService() }
}
var formal: Factory<MyServiceType> {
Factory(self) { MyService() }
}
// result is the same for both properties
}

O Contentor é basicamente um espaço de nome para as propriedades de fábrica que pode criar na extensão.

Contentor possui o Gestor de contentores que gere o registo e os mecanismos de armazenamento em cache do âmbito de um determinado contentor.

Fábrica é apenas um wrapper para Registo de Fábrica dentro dela.

Registo de Fábrica é utilizado internamente para gerir o registo e resolução processo.

public struct Factory<T>: FactoryModifying {
public init(_ container: ManagedContainer,
key: String = #function,
_ factory: @escaping () -> T) {
self.registration = FactoryRegistration<Void,T>(id: "\(key)<\(T.self)>",
container: container,
factory: factory)
}
}

Como pode ver no código, registo chave é uma concatenação de nome da função (nome do imóvel neste caso) e tipo de dependência.

Registo de Fábrica utiliza o correspondente âmbito para resolver a dependência. Âmbitos são basicamente diferentes tipos de caches (cada âmbito tem o seu próprio nível de cache).

public struct FactoryRegistration<P,T> {
internal func resolve(with parameters: P) -> T {
...
return scope?.resolve(
using: manager.cache,
id: id,
factory: { factory(parameters) }
) ?? factory(parameters)
}
}

Âmbito lojas encerramento de fábricas nos dicionários por registo chaves:

public class Scope {
internal func resolve<T>(using cache: Cache, id: String, factory: () -> T) -> T {
if let cached: T = unboxed(box: cache.value(forKey: id)) {
return cached
}
let instance = factory()
if let box = box(instance) {
cache.set(value: box, forKey: id)
}
return instance
}
}
extension Scope {
final class Cache {
typealias CacheMap = [String: AnyBox] // Dictionary for reflection
func value(forKey key: String) -> AnyBox? {
cache[key]
}
func set(value: AnyBox, forKey key: String) {
cache[key] = value
}
}
}

Fábrica A ideia de implementação é bastante semelhante à bibliotecas baseadas em reflexão como Swinject. Reflexão significa mapear um conjunto de chaves para um conjunto de objectos. Assim, por exemplo, o Factory e o Swinject utilizam ambos um Dicionário para mapear o chaves (tipos de instância) para valores de instâncias.

Fábrica é semelhante à biblioteca Swinject em termos de como funciona nos seus bastidores. É um biblioteca baseada em reflexão.

Mas porquê então Fábrica é mais preferível do que Swinject?

A principal razão para preferir Fábrica sobre Swinject é segurança em tempo de compilação.

Swinject fornece integralmente todas as outras características que esperamos de uma solução DI exceto segurança em tempo de compilação e é crucial. Swinject resolve instâncias a pedido em tempo de execução com sem segurança para evitar acidentes.

Pelo contrário, Fábrica é seguro em tempo de compilação.

Existe uma grande diferença na abordagem de como se registam e resolvem instâncias.

Quando Swinject resolve a instância que existe nenhuma garantia que esta instância tenha sido registada antes disso.

Com Fábrica quando chama a propriedade como uma função para resolver a instância, a declaração da propriedade já é uma garantia de que a instância foi registada. É por isso que é seguro em tempo de compilação.

Para resumir, usando o padrão Factory pode ser uma solução adequada para Injeção de Dependência em aplicações iOS mais pequenas, uma vez que fornece uma forma simples e leve de lidar com a DI sem bibliotecas ou estruturas externas de terceiros. No entanto, para projectos maiores e mais complexos, a gestão de dependências através de fábricas pode tornar-se um desafio, e pode não ser suficiente para cumprir todos os requisitos de uma DI adequada. Embora as fábricas possam garantir a segurança e a preguiça em tempo de compilação, elas não podem controlar o escopo ou o ciclo de vida das dependências, nem facilmente simular ou substituir implementações para testes, nem detetar ciclos de dependência. Para resolver esses problemas, pode-se usar uma solução pronta para uso, como o Fábrica que pode fornecer todas as funcionalidades necessárias.

Padrão de fábrica

  • ✅ Segurança em tempo de compilação
  • ✅ Preguiça
  • ❌ Controle o ciclo de vida (Controle o âmbito/ redefina facilmente o âmbito quando necessário – âmbitos que permitam uma melhor gestão da memória)
  • Facilmente simule/substitua implementações para testes
  • Detecte ciclos de dependência

Swinject

  • ❌ Segurança em tempo de compilação
  • ✅ Preguiça
  • ✅ Controle o ciclo de vida
  • Facilmente simule/substitua implementações para testes
  • Detecte ciclos de dependência (mas apenas em tempo de execução)

Biblioteca de fábrica – nova abordagem agradável para DI para Swift e SwiftUI

  • ✅ Segurança em tempo de compilação
  • ✅ Preguiça
  • ✅ Controle o ciclo de vida
  • Facilmente simule/substitua implementações para testes
  • Detecte ciclos de dependência (mas apenas em tempo de execução)

Obrigado por ler este post, espero que tenha sido útil.

Related Post