2

Uncle Bobs Fitnesse application uses huge Context object as a way to pass different read only constants and dependencies. He also uses same approach in his code casts. For example his Context object may have static variable repository.

As I see it, it is an alternative to classic DI and it prevents constructor of objects from having too much arguments. However, it looks like this is some kind of global state and also it may act like magnet object, growing and growing over time.

I want to understand why not to use regular DI and what are advantages of using this Context approach.

What are other alternatives of classic DI (passing dependencies in constructor)? I personally think the only disadvantage of classic DI is big constructors, but big advantage is explicit dependencies without global state.

0

3 Answers 3

2

Having fewer objects to inject or to pass around is beneficial. Sticking to a single Context class no matter how large and incohesive it gets is definitely not.

FitNesse is a real life application and as such it's code is not a perfect example in every sense. If you take a closer look at it, you'll notice that FitnesseContext depends on many classes that depend on FitnesseContext. That is somewhat problematic in practice and also a clear violation of Acyclic dependencies principle defined by Uncle Bob himself.

It's possible to get rid of explicit cyclic dependencies between classes just by hiding FitnesseContext behind an interface. However, that wouldn't still solve the problem of having cyclic dependencies between modules. Besides, you would still have a large class violating Single responsibility principle and a large interface violating Interface segregation principle, both advocated by Uncle Bob too.

Perhaps a more robust approach would be to define a composite interface (ApplicationContext) and its parts (AuthenticationContext, PageContext etc) in one package and write implementations of them in feature level modules. For instance, the implementation of AuthenticationContext belongs to the authentication package as it relies on conrete classes in the authentication package. Then you could pass around a composite object of type ApplicationContext without cyclic dependencies between classes or packages and without writing a class or an interface that handles it all.

0

Service location is considered an antipattern...

I personally think the only disadvantage of classic DI is big constructors, but big advantage is explicit dependencies without global state.

You are right that you usually don't want to inject a bundle of dependencies when those dependencies aren't all needed. Definitely not just to cut down on constructor size (annoying as it may be).

Injecting such a bundle is reminiscent of a service locator pattern, which is commonly considered to be an antipattern, or at least inferior to direct dependency injection (i.e. injecting the dependency, not the locator).

While the Context in your question may not exactly be a service locator, it fulfills the same purpose as a service locator. It acts as a bag of dependencies that the consuming class can take dependencies from at will.


...but a unit of work is not.

For example his Context object may have static variable repository. [..] it looks like this is some kind of global state and also it may act like magnet object, growing and growing over time.

These context objects often acts as a unit of work, which precisely justifies them being a magnet for all the data access they provide.

A unit of work provides transactional behavior. Transactions can span across multiple repositories (e.g. either I input your user details, addresses, and billing history; or I input nothing at all). That is desired behavior to ensure you don't end up with half-done data entries.

If your consuming code were to handle the repositories separately, it would be both hard and unreadable to figure out exactly when the transaction could be committed or rolled back.

By providing access to the unit of work object itself, the consuming code is able to explicitly cause the transaction to be committed (or rolled back).

By putting the repositories in the unit of work itself (as opposed to separatedly injecting them), you both allow for the use of multiple units of work (= different repository instances, so repositories can't just be injected independent of the unit of work they belong to) and it's made much clearer exactly which repositories are managed by the unit of work (in case your codebase has other persistence stores as well).

3
  • 1
    @Laiv: Looking at the example in the link in the question, this class has a lot of public properties for what seem to be dependencies (i.e. not value objects, e.g. ResponderFactory, PageFactory, VersionsController, Authenticator, ...), which in turn suggests that this class provides those dependencies to its own consumer. While this means the dependencies are not dynamically looked up the way they usually are in a service locator, my inference of it being "a dependency that provides other dependencies" seems to hold for the case in point.
    – Flater
    Commented Sep 16, 2020 at 13:15
  • Oh! you are right. I'm so biased by my own practices that I didn't realise U.B has made all the dependencies final but public.
    – Laiv
    Commented Sep 16, 2020 at 13:22
  • Looking at the history in the repo, it seems this class started out relatively small, perhaps as a quick and dirty solution to pass some contextual information around, and print it (e.g., that version had a toString that produced a formatted string with some info in it). Then, I guess as people started contributing, this class turned into a dumping ground for stuff that didn't have a more obvious home, and it was never refactored/redesigned in a nontrivial way. In other words, this is probably more due to entropy, rather then by design. Commented Sep 17, 2020 at 4:20
-1

Yes a Context object is a form of global state, but so are the properties/field/variables on the object/scope/namespace/package your function is running within the context of.

That is they are global in the sense that all but the most anemic implementation will have some form of state sharing, and as such some state is in a non-local (globalish) context.

The Advantage/Disadvantage of a context object comes down to how you want that state shared. It is much like a scope which provides different sets of collaborators with the same values and references to work upon.

  • protected shares between base, and inheritors
  • private is just the functions in the scope
  • public is anyone who can get their mits on it
  • internal is just for those in the package/dll
  • a context object shares information across a subset of instances not necessarily in the same namespace/package/type.

As for DI, I honestly cannot tell the difference between injecting a Context Collaborator, and injecting any other collaborator. Its all Dependency Injection.

A judicious choice of Dependencies to inject can significantly increase/reduce the size of constructors, but that is already a solved problem. Most programs will either dedicate a builder/factory to the task to ensure invariants are maintained, or the DI framework used abstracts the constructor away entirely to a simple call like resolve<T>().

1
  • 1
    I would argue, that use of Context tend to regress to World Injection, which is much easier to avoid with classical Dependency Injection. Remember: do not reuse same context for components of different nature!
    – Basilevs
    Commented Sep 16, 2020 at 10:59

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.