Global State & Static
- Writable global state makes our code brittle. One test might change the state of this global, affecting the outcome of the next test run. This means tests now must be run in order, and running tests in parallel is impossible.
- Read-only globals still allow our APIs (i.e. our method signatures) to deceive us (see Dependency Injection), giving our code hidden dependencies. This isn't great, but may be tolerable.
- Public static methods/properties consitute a form of global state because they allow an object to interact with the static component even if that component wasn't directly passed in using the API. Private static functions with no dependencies, that just run pure algorithmic code, would be ok, but why bother.
- Static classes/methods is difficult to mock because there's no instance to swap out; to do it requires ridiculous tricks you probably shouldn't be dealing with.
- Avoid them because they're just a form of global state (they hold one instance that's available anywhere).
- They prevent (intentionally) multiple instantiations, which makes our code inflexible. We almost never need this 1 instance limit enforced. We can just create only one instance. Then, if we create others, there must be a good reason. If we're only trying to solve the problem of making data global (holding aside whether that's a good idea), don't sneak in this one instance thing too.
- Getting our dependencies from within our classes (e.g. building them with the
newoperator or getting singletons from the global state or using static methods) makes our APIs into liars. The API pretends not to have any dependencies, when in fact it does, but these are just hidden in the source code. This makes the code much harder to follow.
- If we get our dependencies via one of the methods above rather than injecting them, we can't (easily) substitute mock objects (or nulls or subclasses) for real dependencies when we're doing our unit tests. This is critical if we want to test just one class in isolation of its dependencies. Essentially: we've mixed our initialization logic with our business/application logic.
- Not using dependency injection (i.e. getting our dependencies using the methods above) makes our code more tightly coupled and less reusable accross projects because we must manually swap-out all references to the dependencies.
- Factories are classes that take care of building all the objects we need for dependency injection.
- They can be replaced with automatic DI frameworks in advanced setups.
- Factories should also configure the objects before injecting them. Doing the initialization in the factory, rather than in the receiving object's constructor, again gives you more flexibility because you can inject an object that was initialized differently (e.g. for a mock). It also keeps the receiving objects constructor from spending time doing initialization procedures that may not be necessary.
- Taking the above one step further: objects should ask only for dependencies they really need. E.g. a car should ask for an engine, not an engine factory to build it's own. And an CalendarView should ask for events to include, not a database connection to look them up. Similarly, don't pass in big context/config objects if you only need a few values. This solves so many problems: easier to inject test data (mocks are simpler and you don't have to guess what parts of the bigger object are being used), easier reuse, faster constructors as mentioned, and less brittle—if the method for going from the intermediate object to the one you really want changes, you only have to update this code in your factory. Asking for one thing, only to use it get something else, is a "Law of Demeter" violation.
- If one object absolutely needs to create another object, pass an
OneObject's constructor. You'll then need a mockOtherObject and mockOtherObjectFactory (probably) in your tests.
- Context objects may sound good in theory (no need to change method signatures to change dependencies) but they are very hard to test.
- Adding up all the above on factories, we get that the constructor should not be doing real work.
- Also, make your constructors as lenient as possible, so your tests can more easily pass in mocks and nulls. Your constructor should still reject arguments which would never make sense/be absolutely unusable in any context. But it shouldn't reject argument sets that do make sense for testing, even if they wouldn't make sense in production (as your application currently stands). Rejecting those argument sets can fall on your factories.
For best testability, Make all of your objects one of two types: "injectables" or "
Injectables are objects which are injected into other objects (i.e. objects depend on them). A DI framework must be able to create them, so they can't require any custom values in their constructors. The only arguments they should take are other injectables, and maybe some fixed string configuration values.
Newables are objects that are made by passing only custom values to their constructor. They're glorified value holders; trivial to construct and low on behavior. They should be at the end of your object graph (duh), i.e. nothing should depend on them. An example would be an
Articleobject, which just holds the article's title, date of creation, author name, etc. and has no methods, except getters and setters (assuming you've moved the CRUD methods somewhere else). A newable could theoretically take an injectable in it's constructor, but if you need to do this there's a big problem in your program design and it will hurt testability (because it will force you to commit "Law of Demeter" violations).