You can glean a lot from a type signature. An example (in Haskell):
mystery :: a -> [a] -> Bool
There are really only a couple of things this function can sensibly do.
Further, because this is Haskell (which wisely shuns false economies like
mutation) we can tell that mystery does not depend upon nor modify any
global mutable state.
Well-crafted object-oriented code can imply similar behavioural attributes through method signatures. An example class:
class RegistrationService(object):
def __init__(self, userRepo, mailService):
# elided
Just from the constructor the reader can infer that RegistrationService is
going to be doing something with users — maybe updating the database — and
something with mail — sending a welcome message perhaps. We can immediately
see the interactions this class has with the rest of the system, and we can
see exactly what dependencies have to be satisfied, either in production or
test code.
It can be difficult for client code to wire up those dependencies but we have dependency injection these days to take care of this. Now, injection is fantastic in production, but I’m weary of using it in test code1, so tests tend to directly invoke constructors with mocked dependencies in code.
The problem here is that as a class’ dependencies change over time, those
instantiations in test code must also be changed. This is especially bad in
poorly isolated “unit” tests that effectively hand-wire each layer in some
jaunty incomprehensible pyramid, mocking only where absolutely necessary.
One “solution” is to reach for the deus ex machina of service location. I vehemently oppose this cheat. Service location is nothing more than global variables for people who should know better. We must eschew such “pragmatic” laziness for it obscures the requirements and behaviours of our class that are clearly observed and inferred when dependencies are specified up front in the constructor. Code becomes intractable and temporally coupled to the state of the service locator, and those tests you were trying to avoid maintaining are just going to start failing anyway because you haven’t added the new dependency to the service locator!
Instead we should view the pain of maintaining tests not as something to be avoided, but as a necessary signal that something is wrong and that we must fix it.
If a constructor specifies twelve collaborators as parameters, well that’s unfortunate but at least it’s honest. Those parameters are hard dependencies that must be satisfied for the code to compile. Culling them and replacing them with calls to a service locator does not remove the dependency, it doesn’t simplify or fix the system, it just hides the problem temporarily. So, reduce and manage your dependencies, even if it means splitting services into multiple classes, and focus your unit tests on individual units for Pete’s sake!
-
Believe me, it has been tried: I advise against it. ↩