Reagir nativo - Usar um singleton é a melhor alternativa ao DI?

8

Eu tenho lido muito sobre o padrão singleton e como ele é "ruim", porque dificulta o teste das classes, por isso deve ser evitado. Eu li alguns artigos explicando como o singleton poderia ser substituído pela injeção de dependência, mas parece desnecessariamente complexo para mim.

Aqui está o meu problema com mais detalhes. Estou construindo um aplicativo móvel usando o React Native e quero criar um cliente REST que se comunique com o servidor, obtenha dados, publique dados e lide com o logon (armazene o token de logon e envie-o a cada solicitação após o logon).

Meu plano inicial era criar um objeto singleton (RESTClient) que meu aplicativo usaria inicialmente para efetuar login e, em seguida, fazer o pedido enviando as credenciais quando necessário. A abordagem de DI parece realmente complicada para mim (talvez porque nunca usei DI antes), mas estou usando esse projeto para aprender o máximo possível, para que eu queira fazer o melhor aqui. Todas as sugestões e comentários são muito apreciados.

Edit: Agora eu percebi que tenho formulado minha pergunta mal. Eu queria algumas orientações sobre como evitar o padrão singleton no RN e devo fazê-lo. Felizmente, Samuel me deu o tipo de resposta que eu queria. Meu problema era que eu queria evitar o padrão singleton e usar o DI, mas parecia realmente complicado implementá-lo no React Native. Eu fiz algumas pesquisas adicionais e a implementei usando o sistema de contexto Reacts.

Para quem estiver interessado, aqui está como eu fiz. Como eu disse, usei o contexto no RN, que é algo como adereços, mas é propagado para todos os componentes.

No componente raiz, forneço as dependências necessárias como esta:

export default class Root extends Component {
  getChildContext() {
    restClient: new MyRestClient();
  }

  render() {...}
}
Root.childContextTypes = {restClient: PropTypes.object};

Agora restClient está disponível em todos os componentes abaixo de Root. Eu posso acessá-lo assim.

export default class Child extends Component {
  useRestClient() {
    this.context.restClient.getData(...);
  }

  render() {...}
}
Child.contextTypes = {restClient: PropTypes.object}

Isso efetivamente afasta a criação de objetos da lógica e desacopla a implementação do cliente REST dos meus componentes.

Mateo Hrastnik
fonte
3
Considere alterar o título da pergunta. No momento, REST e cliente são elementos irrelevantes. A única pergunta aqui é: o singleton é a melhor alternativa ao DI na minha situação específica? .
LAIV
Não sei muito sobre reagir, mas em geral o DI é um conceito simples. Se você está achando complexo, suspeito que não tenha lido / ouvido uma boa explicação.
Ben Aaronson

Respostas:

15

A injeção de dependência não precisa ser complexa e vale a pena aprender e usar. Geralmente é complicado pelo uso de estruturas de injeção de dependência, mas elas não são necessárias.

Na sua forma mais simples, a injeção de dependência é transmitir dependências em vez de importá-las ou construí-las. Isso pode ser implementado simplesmente usando um parâmetro para o que seria importado. Digamos que você tenha um componente nomeado MyListque precisa ser usado RESTClientpara buscar alguns dados e exibi-los ao usuário. A abordagem "singleton" seria algo como isto:

import restClient from '...' // import singleton

class MyList extends React.Component {
  // use restClient and render stuff
}

Este firmemente casais MyListpara restClient, e não há nenhuma maneira você pode teste de unidade MyListsem testes restClient. A abordagem de DI ficaria assim:

function MyListFactory(restClient) {
  class MyList extends React.Component {
    // use restClient and render stuff
  }

  return MyList
}

É tudo o que é necessário para usar o DI. Ele adiciona no máximo duas linhas de código e você elimina uma importação. A razão pela qual eu introduzi uma nova função "factory" é porque o AFAIK não pode passar parâmetros adicionais do construtor no React, e eu prefiro não passar essas coisas pelas propriedades do React porque ele não é portátil e todos os componentes pais devem saber passar todos adereços para crianças.

Então agora você tem uma função para construir MyListcomponentes, mas como você o usa? O padrão de DI borbulha a cadeia de dependência. Digamos que você tenha um componente MyAppque use MyList. A abordagem "singleton" seria:

import MyList from '...'
class MyApp extends React.Component {
  render() {
    return <MyList />
  }
}

A abordagem de DI é:

function MyAppFactory(ListComponent) {
  class MyApp extends React.Component {
    render() {
      return <ListComponent />
    }
  }

  return MyApp
}

Agora podemos testar MyAppsem testar MyListdiretamente. Poderíamos até reutilizar MyAppcom um tipo completamente diferente de lista. Esse padrão borbulha até a raiz da composição . É aqui que você liga para suas fábricas e liga todos os componentes.

import RESTClient from '...'
import MyListFactory from '...'
import MyAppFactory from '...'

const restClient = new RESTClient(...)
const MyList = MyListFactory(restClient)
const MyApp = MyAppFactory(MyList)

ReactDOM.render(<MyApp />, document.getElementById('app'))

Agora, nosso sistema usa uma única instância de RESTClient, mas nós o projetamos de maneira que os componentes sejam fracamente acoplados e fáceis de testar.

Samuel
fonte
Se você não se importa, gostaria de fazer referência à sua resposta, porque acho que ilustra muito bem os pontos que mencionei apenas superficialmente.
LAIV
2
@ Laiv, acho que o termo "DI do pobre homem" está saindo de moda, a favor de "DI puro". Embora não intencionado, o primeiro parece muito negativo, pois implica que os contêineres de IoC são "mais ricos".
David Arno
Este. Eu pensei que a injeção de dependência era extremamente complicada quando a vi pela primeira vez, mas depois de um tempo é extremamente fácil de entender. E o @Samuel explicou bem!
Rhys Johns
1
@ Samuel Então, para aplicar corretamente o DI no React, eu precisaria criar fábricas para cada componente? Também no seu exemplo, não deve MyAppFactoryretornar a MyAppturma?
Mateo Hrastnik
@MattHammond Componentes simples que não têm nenhuma dependência não precisam de fábricas. E é assim que eu fiz, pode haver outras abordagens para o DI no React que são melhores, mas eu estava buscando a simplicidade aqui. Obrigado pela correção; Eu consertei agora.
Samuel
5

De acordo com suas premissas (aprendizado), a resposta mais simples é não, os singletons não são a melhor alternativa à injeção de dependência .

Se o objetivo é aprender, você encontrará o DI como um recurso mais valioso na sua caixa de ferramentas do que o Singleton. Pode parecer complexo, mas a curva de aprendizado está quase em tudo o que você precisa aprender do zero. Não se deite na sua zona de conforto ou não haverá aprendizado.

Tecnicamente, há pouca diferença entre Singleton e instância única (o que eu acho que você está tentando fazer). Aprendendo o DI, você perceberá que pode injetar instâncias únicas em todo o código. Você verá que seu código é mais fácil de testar e está pouco acoplado. ( Para mais detalhes, consulte a resposta de Samuel )

Mas não pare por aqui. Implemente o mesmo código com o Singleton. Em seguida, compare as duas abordagens.

Compreender e familiarizar-se com as duas implementações permitirá que você saiba quando elas são apropriadas e provavelmente você estará em posição de responder a si mesmo à pergunta.

Agora, durante o treinamento, você cria seus Golden Hammers ; portanto, se você decidir evitar o DI, é provável que acabe implementando singletons sempre que precisar.

Laiv
fonte
0

Concordo com as outras respostas de que aprender o que é DI e como usá-lo é uma boa ideia.

Dito isto, o aviso de que singletons dificultam o teste é geralmente feito por pessoas que usam linguagens de tipo estaticamente (C ++, C #, Java etc.) .
Por outro lado, em uma linguagem dinâmica (Javascript, PHP, Python, Ruby etc.) , geralmente é muito mais difícil substituir um singleton por uma implementação específica de teste do que seria no caso de você usar DI.

Nesse caso, recomendo usar o design mais natural para você e seus co-desenvolvedores, porque isso tenderá a evitar erros. Se isso resultar em singletons, que assim seja.

(Mas, novamente: aprenda o DI antes de tomar essa decisão.)

Lutz Prechelt
fonte