It's important to distinguish here between single instances and the Singleton design pattern.
Single instances are simply a reality. Most apps are only designed to work with one configuration at a time, one UI at a time, one file system at a time, and so on. If there's a lot of state or data to be maintained, then certainly you would want to have just one instance and keep it alive as long as possible.
The Singleton design pattern is a very specific type of single instance, specifically one that is:
- Accessible via a global, static instance field;
- Created either on program initialization or upon first access;
- No public constructor (cannot instantiate directly);
- Never explicitly freed (implicitly freed on program termination).
It is because of this specific design choice that the pattern introduces several potential long-term problems:
- Inability to use abstract or interface classes;
- Inability to subclass;
- High coupling across the application (difficult to modify);
- Difficult to test (can't fake/mock in unit tests);
- Difficult to parallelize in the case of mutable state (requires extensive locking);
- and so on.
None of these symptoms are actually endemic to single instances, just the Singleton pattern.
What can you do instead? Simply don't use the Singleton pattern.
Quoting from the question:
The idea was to have this one place in the app which keeps the data stored and synced, and then any new screens that are opened can just query most of what they need from there, without making repetitive requests for various supporting data from the server. Constantly requesting to the server would take too much bandwidth - and I'm talking thousands of dollars extra Internet bills per week, so that was unacceptable.
This concept has a name, as you sort of hint at but sound uncertain of. It's called a cache. If you want to get fancy you can call it an "offline cache" or just an offline copy of remote data.
A cache does not need to be a singleton. It may need to be a single instance if you want to avoid fetching the same data for multiple cache instances; but that does not mean you actually have to expose everything to everyone.
The first thing I'd do is separate out the different functional areas of the cache into separate interfaces. For example, let's say you were making the world's worst YouTube clone based on Microsoft Access:
MSAccessCache
▲
|
+-----------------+-----------------+
| | |
IMediaCache IProfileCache IPageCache
| | |
| | |
VideoPage MyAccountPage MostPopularPage
Here you have several interfaces describing the specific types of data a particular class might need access to - media, user profiles, and static pages (like the front page). All of that is implemented by one mega-cache, but you design your individual classes to accept the interfaces instead, so they don't care what kind of an instance they have. You initialize the physical instance once, when your program starts, and then just start passing around the instances (cast to a particular interface type) via constructors and public properties.
This is called Dependency Injection, by the way; you don't need to use Spring or any special IoC container, just so long as your general class design accepts its dependencies from the caller instead of instantiating them on its own or referencing global state.
Why should you use the interface-based design? Three reasons:
It makes the code easier to read; you can clearly understand from the interfaces exactly what data the dependent classes depend on.
If and when you realize that Microsoft Access wasn't the best choice for a data back-end, you can replace it with something better - let's say SQL Server.
If and when you realize that SQL Server isn't the best choice for media specifically, you can break up your implementation without affecting any other part of the system. That is where the real power of abstraction comes in.
If you want to take it one step further then you can use an IoC container (DI framework) like Spring (Java) or Unity (.NET). Almost every DI framework will do its own lifetime management and specifically allow you to define a particular service as a single instance (often calling it "singleton", but that's only for familiarity). Basically these frameworks save you most of the monkey work of manually passing around instances, but they are not strictly necessary. You do not need any special tools in order to implement this design.
For the sake of completeness, I should point out that the design above is really not ideal either. When you are dealing with a cache (as you are), you should actually have an entirely separate layer. In other words, a design like this one:
+--IMediaRepository
|
Cache (Generic)---------------+--IProfileRepository
▲ |
| +--IPageRepository
+-----------------+-----------------+
| | |
IMediaCache IProfileCache IPageCache
| | |
| | |
VideoPage MyAccountPage MostPopularPage
The benefit of this is that you never even need to break up your Cache
instance if you decide to refactor; you can change how Media is stored simply by feeding it an alternate implementation of IMediaRepository
. If you think about how this fits together, you will see that it still only ever creates one physical instance of a cache, so you never need to be fetching the same data twice.
None of this is to say that every single piece of software in the world needs to be architected to these exacting standards of high cohesion and loose coupling; it depends on the size and scope of the project, your team, your budget, deadlines, etc. But if you're asking what the best design is (to use in place of a singleton), then this is it.
P.S. As others have stated, it's probably not the best idea for the dependent classes to be aware that they are using a cache - that is an implementation detail they simply should never care about. That being said, the overall architecture would still look very similar to what's pictured above, you just wouldn't refer to the individual interfaces as Caches. Instead you'd name them Services or something similar.
Very briefly, it makes program state unpredictable.
To elaborate, imagine you have a couple of objects that both use the same global variable. Assuming you're not using a source of randomness anywhere within either module, then the output of a particular method can be predicted (and therefore tested) if the state of the system is known before you execute the method.
However, if a method in one of the objects triggers a side effect which changes the value of the shared global state, then you no longer know what the starting state is when you execute a method in the other object. You can now no longer predict what output you'll get when you execute the method, and therefore you can't test it.
On an academic level this might not sound all that serious, but being able to unit test code is a major step in the process of proving its correctness (or at least fitness for purpose).
In the real world, this can have some very serious consequences. Suppose you have one class that populates a global data structure, and a different class that consumes the data in that data structure, changing its state or destroying it in the process. If the processor class executes a method before the populator class is done, the result is that the processor class will probably have incomplete data to process, and the data structure the populator class was working on could be corrupted or destroyed. Program behaviour in these circumstances becomes completely unpredictable, and will probably lead to epic lossage.
Further, global state hurts the readability of your code. If your code has an external dependency that isn't explicitly introduced into the code then whoever gets the job of maintaining your code will have to go looking for it to figure out where it came from.
As for what alternatives exist, well it's impossible to have no global state at all, but in practice it is usually possible to restrict global state to a single object that wraps all the others, and which must never be referenced by relying on the scoping rules of the language you're using. If a particular object needs a particular state, then it should explicitly ask for it by having it passed as an argument to its constructor or by a setter method. This is known as Dependency Injection.
It may seem silly to pass in a piece of state that you can already access due to the scoping rules of whatever language you're using, but the advantages are enormous. Now if someone looks at the code in isolation, it's clear what state it needs and where it's coming from. It also has huge benefits regarding the flexibility of your code module and therefore the opportunities for reusing it in different contexts. If the state is passed in and changes to the state are local to the code block, then you can pass in any state you like (if it's the correct data type) and have your code process it. Code written in this style tends to have the appearance of a collection of loosely associated components that can easily be interchanged. The code of a module shouldn't care where state comes from, just how to process it. If you pass state into a code block then that code block can exist in isolation, that isn't the case if you rely on global state.
There are plenty of other reasons why passing state around is vastly superior to relying on global state. This answer is by no means comprehensive. You could probably write an entire book on why global state is bad.
Best Answer
If you push any rule far enough its going to come down to a subjective choice. But 'global state is bad' is about as canon as you get.
We all know that sometimes it is quicker to use global state to solve a problem. Or perhaps a particular framework we are using restricts other options to such an extent that Global State is an acceptable solution.
But these are extreme positions where we accept we are doing a 'bad thing'(tm) and promise to be careful.
You have stated how you have managed to avoid some of the pitfalls of global state, but not what the extreme pressure, money, time or technical limitation was that forced you to go down that route!
Given that eliminating global state is fairly simple and is almost universally regarded as producing 'better' code. Why the hell would you not do it???
If you argument is 'global state is not bad, look at this code' then the only thing to do is write the same app both ways and see which has less lines of code, or whatever measure of goodness you want to apply. But you would have to post both full code bases or link to them on github or something.
Presumably they would both be fairly similar and we would be arguing over whether being able to run unit tests concurrently etc should be part of the 'measure of goodness'