Domanda

I've been reading a lot about the singleton pattern and how it's "bad" because it makes the classes using it hard to test so it should be avoided. I've read some articles explaining how the singleton could be replaced with dependency injection, but it seems unnecessarily complex to me.

Here's my problem in a bit more detail. I'm building a mobile app using React Native and I want to make a REST client that will communicate with the server, get data, post data and handle the login (store the login token and send it with each request after login).

My initial plan was to create a singleton object (RESTClient) that my app will use initially to login and then make request sending the credentials where needed. The DI approach seems really complicated to me (maybe because I never used DI before) but I'm using this project to learn as much as can so I want to do what's best here. Any suggestions and comments are much appreciated.

Edit: I've now realized I've worded my question poorly. I wanted some guidance on how to avoid the singleton pattern in RN and should I even do it. Luckily Samuel gave me just the kind of answer I wanted. My problem was I wanted to avoid the singleton pattern and use DI, but it seemed really complicated to implement it in React Native. I did some further research and implemented it using Reacts context system.

For anyone interested here's how I did it. Like I said, I used the context in RN which is something like props but it's propagated down to every component.

In the root component I provide the needed dependencies like this:

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

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

Now restClient is available in all components below Root. I can access it like this.

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

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

This effectively moves object creation away from logic and decouples the REST client implementation from my components.

È stato utile?

Soluzione

Dependency injection does not need to be complex at all, and it's absolutely worth learning and using. Usually it's complicated by the usage of dependency injection frameworks, but they aren't necessary.

In its simplest form, dependency injection is passing dependencies instead of importing or constructing them. This can be implimented by simply using a parameter for what would be imported. Lets say you have a component named MyList that needs to use RESTClient to fetch some data and display it to the user. The "singleton" approach would look something like this:

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

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

This tightly-couples MyList to restClient, and there is no way you can unit test MyList without testing restClient. The DI approach would look something like this:

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

  return MyList
}

That's all it takes to use DI. It adds at most two lines of code and you get to eliminate an import. The reason why I introduced a new function "factory" is because AFAIK you can't pass additional constructor parameters in React, and I prefer not to pass these things down through React properties because it's not portable and all parent components must know to pass all props to children.

So now you have a function to construct MyList components, but how do you use it? The DI pattern bubbles up the dependency chain. Say you have a component MyApp that uses MyList. The "singleton" approach would be:

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

The DI approach is:

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

  return MyApp
}

Now we can test MyApp without testing MyList directly. We could even reuse MyApp with a completely different kind of list. This pattern bubbles up to the composition root. This is where you call your factories and wire all the components.

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

Now our system uses a single instance of RESTClient, but we've designed it in a way that components are loosely-coupled and easy to test.

Altri suggerimenti

According to your premises (learning), the simplest answer is no, singletons are not the best alternative to Dependency Injection.

If learning is the goal, you will find DI to be a more valuable resource in your toolbox than Singleton. It might seem complex, but the learning curve is almost on everything you have to learn from scratch. Don't lay on your comfort zone or there won't be learning at all.

Technically, there's little difference between singleton and single instance (what I think you are trying to do). Learning DI you will realise that you can inject single instances all over the code. You will find your code to be easier to test and loosely coupled. (For more details check out Samuel's answer)

But don't you stop here. Do implement the same code with Singleton. Then compare both approaches.

Understanding and to be familiar with both implementations will allow you to know when they are appropriated and probably you will be in a position to answer yourself the question.

It's now, during the training, when you forge your Golden Hammers, so if you decide to avoid learning DI it's likely probable that you will end up implementing singletons everytime you need DI.

I agree with the other answers that learning what DI is and how to use it is a good idea.

That said, the warning that singletons make testing too hard is usually made by people using statically typed languages (C++, C#, Java, etc.).
In contrast in a dynamic language (Javascript, PHP, Python, Ruby, etc.), it is usually hardly harder to replace a singleton with a test-specific implementation than it would be in the case when you are using DI.

In that case, I recommend using the design that feels more natural to you and your co-developers, because that will tend to avoid mistakes. If that results in singletons, so be it.

(But then again: Learn DI before you make that decision.)

Autorizzato sotto: CC-BY-SA insieme a attribuzione
scroll top