What are advantages of using huge Context object over passing dependencies directly

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

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.

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

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

Trả lời

Email của bạn sẽ không được hiển thị công khai. Các trường bắt buộc được đánh dấu *