Apesar do padrão existir há mais de uma dúzia de anos e de haver muitos artigos (e traduções), ainda assim, existem cada vez mais disputas, comentários, perguntas e várias realizações.
Há informações suficientes até no hub, mas o fato de ser discutido em todos os lugares
como fazê-lo, mas praticamente em nenhum lugar -
POR QUE, me inspirou a escrever o post. É possível criar uma boa arquitetura se você não souber para que serve e para que deve ser bom? Certos princípios e tendências claras podem ser levados em consideração - isso ajudará a minimizar problemas imprevistos, mas o entendimento é ainda melhor.
Injeção de dependência é um padrão de design no qual campos ou parâmetros para criar um objeto são configurados externamente.
Sabendo que muitos se limitarão à leitura dos primeiros parágrafos, mudei o artigo.
Apesar de essa “definição” de DI ser encontrada em muitas fontes, ela é ambígua, porque faz o usuário pensar que a injeção é algo que substitui a criação / inicialização de objetos, ou pelo menos está muito ativamente envolvida nesse processo. Obviamente, ninguém proibirá fazer essa implementação do DI. Mas o DI pode ser um invólucro passivo em torno da criação de um objeto que fornece o fornecimento de parâmetros de entrada. Nesta implementação, obtemos outro nível de abstração e uma excelente separação de tarefas: o objeto é responsável por sua inicialização e a injeção implementa o armazenamento de dados e fornece módulos de aplicativos.
Agora, sobre tudo em ordem e em detalhes.
Começarei com um simples, por que havia necessidade de novos padrões e por que alguns padrões antigos se tornaram muito limitados em escopo?
Na minha opinião, a maior parte das mudanças foi introduzida pela introdução maciça do autoteste. E para quem está escrevendo ativamente autotestes, este artigo é óbvio como um dia branco, você não pode ler mais. Só você não pode imaginar quantas pessoas não as escrevem. Entendo que pequenas empresas e empresas iniciantes não possuem esses recursos, mas, infelizmente, grandes empresas geralmente têm problemas mais prioritários.
O raciocínio aqui é muito simples. Suponha que você esteja testando uma função com os parâmetros
aeb , e espere obter o resultado
x . Em algum momento, suas expectativas não se realizam, a função retorna o resultado
y e, após passar algum tempo, você encontra um singleton dentro da função, que em alguns estados traz o resultado da função para um valor diferente. Esse singleton foi chamado de
dependência implícita e, de todas as formas possíveis, recusou-se a usá-lo nessas situações. Infelizmente, você não jogará palavras para fora da música, caso contrário, será uma música completamente diferente. Portanto, retiramos nosso singleton como uma variável de entrada na função. Agora temos 3 variáveis de entrada
a ,
b ,
s . Tudo parece óbvio: mudamos os parâmetros - obtemos um resultado inequívoco.
Enquanto eu não vou dar exemplos. Além disso, não estamos falando apenas de funções dentro de uma classe, é um argumento esquemático que também pode ser aplicado à criação de uma classe, módulo etc.
Singleton NotesNota 1. Se, dada a crítica ao padrão singleton, você decidir substituí-lo, por exemplo, por UserDefaults, em relação a essa situação, a mesma dependência implícita aparece.
Nota 2. Não é totalmente correto dizer que somente por causa do autoteste, não vale a pena usar singletones dentro do corpo da função. Em geral, do ponto de vista da programação, não é inteiramente correto que, com a mesma entrada, a função produza resultados diferentes. Só que nos autotestes esse problema apareceu mais claramente.
Complemente o exemplo acima. Você tem um objeto que contém 9 configurações do usuário (variáveis), por exemplo, o direito de ler / editar / assinar / imprimir / encaminhar / excluir / bloquear / bloquear / executar / copiar um documento. Sua função usa apenas três variáveis dessas configurações. O que você passa para a função: o objeto inteiro com 9 variáveis como um parâmetro ou apenas três configurações necessárias com três parâmetros separados? Muitas vezes aumentamos os objetos transferidos para não definir muitos parâmetros, ou seja, selecionamos a primeira opção. Este método será considerado a transferência de
"dependências razoavelmente amplas" . Como você já adivinhou, para fins de autoteste, é melhor usar a segunda opção e passar apenas os parâmetros usados.
Fizemos 2 conclusões:
- a função deve receber todos os parâmetros necessários na entrada
- a função não deve receber parâmetros de entrada desnecessários
Queríamos o melhor - mas conseguimos uma função com 6 parâmetros. Suponha que tudo esteja em ordem dentro da função, mas alguém deve assumir a responsabilidade de fornecer parâmetros de entrada para a função. Como já escrevi, meu raciocínio é superficial. Quero dizer, não apenas uma função de classe comum, mas uma função de inicialização / criação de módulo (vip, viper, objeto de dados, etc.). Nesse contexto, reformulamos a pergunta: quem deve fornecer os parâmetros de entrada para criar o módulo?
Uma solução seria mudar esse caso para o módulo de chamada. Porém, o módulo de chamada precisa passar os parâmetros da criança. Isso implica as seguintes complicações:
Primeiro, um pouco antes, decidimos evitar "dependências excessivamente amplas". Em segundo lugar, você não precisa se esforçar muito para entender que haverá muitos parâmetros e será muito tedioso editá-los toda vez que você adicionar módulos filhos, até custa pensar em excluir módulos filhos. A propósito, em alguns aplicativos, é impossível construir uma hierarquia de módulos: veja qualquer rede social: perfil -> amigos -> perfil do amigo -> amigos do amigo etc. Em terceiro lugar, o princípio SOLI
D pode ser lembrado neste tópico: “Os módulos de nível superior são independentes dos módulos de nível inferior”
Isso dá origem à idéia de fazer a criação / inicialização do módulo em uma estrutura separada. Então é hora de escrever algumas linhas como exemplo:
class AccountList { public func showAccountDetail(account: String) { let accountDetail = AccountDetail.make(account: account)
No exemplo, há um módulo da lista de contas AccountList, que chama o módulo de informações detalhadas na conta AccountDetail.
Para inicializar o módulo AccountDetail, são necessárias 3 variáveis. A variável Account AccountDetail recebe do módulo pai, as variáveis permission1, permission2 são injetadas. Devido à injeção, uma chamada de módulo com detalhes da fatura será semelhante a:
let accountDetail = AccountDetail.make(account: account)
em vez de
let accountDetail = AccountDetail(account: account, permission1: p1, permission2: p2)
e o módulo pai da lista de contas, AccountList, ficará isento da obrigação de passar parâmetros com permissões sobre as quais ele não sabe nada.
Transformei a implementação de injeção (montagem) em uma função estática em uma extensão de classe. Mas a implementação pode ser a seu critério.
Como vemos:
- O módulo recebeu os parâmetros necessários. Sua criação e execução podem ser testadas com segurança em todos os conjuntos de valores.
- Os módulos são independentes, não há necessidade de transferir nada para crianças ou apenas o mínimo necessário.
- Os módulos NÃO fazem o trabalho de fornecer dados, eles usam dados prontos (p1, p2). Portanto, se você deseja alterar algo no armazenamento ou fornecimento de dados, não é necessário fazer alterações no código funcional dos módulos (assim como em seus autotestes), mas você só precisa alterar o próprio sistema de montagem ou as extensões com a montagem.
A essência da injeção de dependência é a construção de um processo no qual, ao chamar um módulo de outro, um objeto / mecanismo independente transfere (injeta) dados para o módulo chamado. Em outras palavras, o módulo chamado é configurado externamente.
Existem vários métodos de configuração:
Injeção de Construtor ,
Injeção de Propriedade ,
Injeção de Interface .
Para Swift:
Injeção de Inicializador ,
Injeção de Propriedade ,
Injeção de Método .
As mais comuns são injeções e propriedades do construtor (inicialização).
Importante: em quase todas as fontes, recomenda-se que as injeções do construtor sejam preferidas. Compare a injeção de construtor / inicializador e a injeção de propriedade:
let account = .. let p1 = ... let p2 = ... let accountDetail = AccountDetail(account: account, permission1: p1, permission2: p2)
melhor que
let accountDetail = AccountDetail() accountDetail.account = .. accountDetail.permission1 = ... accountDetail.permission2 = ...
Parece que as vantagens do primeiro método são óbvias, mas por algum motivo alguns entendem a injeção como configurando um objeto já criado e usam o segundo método. Eu sou o primeiro método:
- a criação pelo designer garante um objeto válido;
- com a injeção de propriedade, não está claro se é necessário testar uma alteração em uma propriedade em locais que não sejam a criação;
- em idiomas que usam opcionalidade, para implementar a injeção de propriedade, você precisa tornar os campos opcionais ou criar métodos de inicialização inteligentes (os preguiçosos nem sempre funcionam). Opcionalidade excessiva adiciona código e suítes de testes desnecessários.
No entanto, até nos livrarmos de algumas dependências, apenas as trocamos de um ombro para outro. Uma pergunta lógica é de onde obter os dados no próprio assembly (função make no exemplo).
O uso de singletones no mecanismo de montagem não leva mais aos problemas acima com dependência oculta, porque Você pode testar a criação de módulos com qualquer conjunto de dados.
Mas aqui estamos diante de outro ponto negativo do singleton: manuseio inadequado (você provavelmente pode trazer muitos argumentos odiosos, mas preguiça). Não é bom espalhar suas muitas armazenadas / singletones em montagens, por analogia com qualquer pessoa, pois elas foram espalhadas em módulos funcionais. Mas mesmo essa refatoração já será o primeiro passo para a higiene, pois é possível restaurar a ordem nas montagens quase sem afetar os testes de código e módulo.
Se você deseja otimizar ainda mais a arquitetura, bem como testar transições e trabalhos de montagem, precisará trabalhar um pouco mais.
O conceito de DI nos permite armazenar todos os dados necessários em um contêiner. Isso é conveniente. Em primeiro lugar, salvar (registrar) e receber (resolver) dados passa por um único objeto de contêiner, respectivamente, para que seja mais fácil gerenciar dados e testá-los. Em segundo lugar, você pode levar em consideração a dependência dos dados entre si. Em muitos idiomas, incluindo o rápido, existem contêineres prontos para gerenciamento de dependências, geralmente dependências formam uma árvore. Os restantes prós e contras não vou listar, você pode ler sobre eles nos links que publiquei no início do post.
Aqui está a aparência da montagem usando o contêiner.
import Foundation import Swinject public class Configurator { private static let container = Container() public static func register<T>(name: String, value: T) { container.register(type(of: value), name: name) { _ in value } } public static func resolve<T>(service: T.Type, name: String) -> T? { return container.resolve(service, name: name) } } extension AccountDetail { public static func make(account: String) -> AccountDetail? { if let p1 = Configurator.resolve(service: Bool.self, name: "permission1"), let p2 = Configurator.resolve(service: Bool.self, name: "permission2") { return AccountDetail(account: account, permission1: p1, permission2: p2) } else { return nil } } }
Este é um possível exemplo de implementação. O exemplo usa a estrutura
Swinject , que nasceu não muito tempo atrás. O Swinject permite criar um contêiner para gerenciamento automatizado de dependências e também criar contêineres para Storyboards. Mais informações sobre o Swinject podem ser encontradas nos exemplos em
raywenderlich . Eu realmente gosto deste site, mas este exemplo não é o mais bem-sucedido, pois considera o uso do contêiner apenas em autotestes, enquanto o contêiner deve ser colocado na arquitetura do aplicativo. Você, no seu código, pode escrever um contêiner por conta própria.
Obrigado a todos por isso. Espero que você não tenha se entediado ao ler este texto.