I've got a web application with an architecture somewhat like a front-controller MVC design. I get HTTP requests, route them, run them through a filter layer, dispatch them to my domain classes which then when necessary access an intelligent data service through data layer classes. The domain classes then return a standard payload to presentation classes which know how to generate the HTTP/web page output.

The application has 3 major cross-cutting concerns: audit logging, instrumentation/metrics, and localization. The first 2 need to be available to almost every class in the composition root, domain, data and presentation categories. The latter, in just a handful of classes, though they are both in my domain and in my presentation layers.

My app uses a dependency injection container. Adding the 3 above services as dependencies causes code smells: too many parameters in the constructors, and a dependency each class does not really need to do its own responsibilty but yet must be able to access.

I've done a lot of reading (Mark Seemann, thanks for your extensive writing on related topics!) trying to find an optimal solution for my situation. I can imagine several possibilities:

  1. Use an aspect oriented programming library which will introspect my code and allow me to add arbitrary point cuts (join points) and wire up my services as advices at those points. This seems elegant. I do worry about adding more complexity, dependencies and performance costs (I push execution limits on some pages that are very data heavy).
  2. Use an abstract factory pattern of some variation to allow leaf classes to get their needed "outside" dependencies. This would require some difficult refactoring, as the core structure is in third party libraries I'd rather not customize and don't lend themselves to easy decoration.
  3. Create Decorators for the classes which need to log, record metrics or localize. This is rather what the aspect library would be doing automatically, isn't it? However, it might address performance concerns.
  4. Use an Ambient Context object. My reading says this is a bad choice.
  5. Inject with the DIC and live with the code smell. ;-)

I do want to retain my ability to unit test everything and use mocks/stubs.

Are there other possibilities I've overlooked? (Redesign the entire app is not an option, by the way.)

  • Hi there, also new here and just spitballing - what about injecting properties or using setters where the constructor does not immediately need the dependency? – datashaman Jul 18 '20 at 4:37

I'd just inject them and not think much about it, but try to automate the first two.

In Symfony the container automatically injects the logger in any class that implements the FIG LoggerAwareInterface. It might be worth it to automate it in your stack as well. You don't say where your DIC comes from, but if it's a standard one this will probably be a known use case and if you wrote it you can just add this feature in yourself.

I like this approach because it does not bloat the DIC definitions, making them easier to understand and mantain. It is convenient to add the logger to any class by just adding an interface and a trait (which is simple and standard, so much so that it comes from the same PSR) in the same file you were working on when you realized you needed a logger call.

It is trickier to give advice on metrics, because you don't say what you are measuring. If they are ubiquitous, I'd probably follow the same pattern that is used for the logger. You'd have to write your own 10-line interface and trait for those though.

I'm a bit surprised that you need localization outside of the presentation layer, unless your domain really has in i18n aspect, such as localized versions of entities. Even then, I'd expect that to be a different job than localizing the UI. In any case, since you mention that only a few classes need that, I'd add that dependency manually.

  • Thanks for the well-thought out answer. It's really good food for thought. – CXJ Nov 5 '20 at 1:06

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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