4

I need help settling a disagreement, a co-worker and I have. We're working on a pretty huge Angular project and are in the process of looking at incorporating some Domain Driven Design principles into our project.

While we were looking at this picture, we discussed how our Dependency Injection Framework would factor into this.

enter image description here

My stance: Dependency Injection Frameworks should be used for Application Services, Domain Services, Repositories and Factories only. Domain Objects on the other hand are basically newable (though not typically newed except inside a factory) and don't have any constructor dependencies themselves.
His stance: That is impossible, because we have a DI-Framework and we should use it. His fear are huge monolith classes. He argues, that when classes have too many methods it's perfectly normal to split these into into smaller classes (because of the SRP) and bundle them in some sort of instrumentor class. This would be a chore without dependency injection. He claims, that DI-Frameworks are normal to use in every bigger project, and not using as much is possible is foolish.
Why it's so hard for me to convince him: We work with tables a lot and there is a class, that more or less represents the 'model' of a table (ignore for a second, that in most projects a 'table' is not really part of the problem domain, and accept the premise, that it might be in ours). Now first of all - even if our data and tables were simple - a class representing a table would have dozens of methods. Between adding and removing rows, resizing columns, getting and setting states like row selection, highlighting columns what have you.
Additionally even things that are conventionally simple, like adding a row, often have 5 different side effects on other parts of the table (these are actual requirements). So I think you can all agree, that having a 4000 line Table class is unacceptable. Consequentially we have to move some of its functionality to smaller classes, that only do one thing, or even just part of one thing. And now we're at an impasse. I think this still needs to be a class without dependency injection, but I don't know how to do it and he's adamant, that I'm just imagining this as a problem and wants to finagle services into the class, that do the jobs, that it conventionally should do itself.

So am I crazy? Is he crazy? Additionally, if you don't agree with him, you get bonus points for linking reputable sources, because 'anyone can write anything on stackoverflow'

1
  • 1
    "The real question might be: how can you split up a huge entity/aggregate root classes without resorting to dependency injection as a crutch" - dependency injection and the use of a Dependency injection container are not one and the same thing Sep 28, 2018 at 9:39

3 Answers 3

5

If you were to be guided by YAGNI, I don't think you would ever get to the point where you would introduce dependency injection into your domain objects.

Dependency injection is a manifestation of the strategy pattern; the value in having injectable dependencies is that you can replace one behavior with another. Of course, the easy way to replace one behavior with another is simply to rewrite the method in question -- so what we really mean is that we want to support two (or more) candidate behaviors and be able to make that choice from the outside.

The classical example would be an interface with side effects - say, to send an email. In production, we really connect to an SMTP server; in test, we just pretend to. So we would want to be able to control the behavior from the outside.

Domain objects are in memory representations of the businesses that they are modeling. They don't have (direct) side effects. Invoking a method on a domain object typically just reads from, or writes to, the private memory of the process.

When we want to include side effects as part of the work done by the domain model, we pass the capability as an argument to the method we are calling. That's one of the roles of domain services -- to act as a proxy for the outside world.

Because the domain model itself doesn't have side effects, the benefits one normally accrues from isolating a test subject from its environment don't usually apply. Unless your domain logic is extremely complicated, you aren't going to gain much from simplifying your call graph.

See Ian Cooper's talk TDD: Where did it all go wrong for additional discussion of test variation.

The one other flavor of variation that may be interesting is variation of data structure -- should this value in your domain model be a linked list or an array list, or maybe a heap.... To some extent, the entire point of "value objects" is to abstract away from the domain logic the concerns about how the data is arranged in memory.

In languages that require being specific about types (Java, for instance), the interfaces that you need to write to support substitution of graphs of value objects are complicated -- the variations of type don't get encapsulated. So you really have to want it.

The investment odds are lousy for introducing dependency injection into your domain model before you have a demonstrated need for it. The problems being that the probability of a payoff is small, and doing the work early doesn't significantly improve the payoff/cost ratio.

This... is wrong tool. -- Zathras

Links

4

I think it helps in situations like these to take a step back and ask one question, that question being "why do we use this pattern/framework/tool?". I find that a lot of the confusion around patterns in general and DI in particular really stem from a lot of pressure to use a pattern without a specific cost/benefit picture.

The main benefit of dependency injection is to decouple a class from its dependencies, making it possible to use the class in new contexts where a differing implementations of the capability provided by the dependency can be used. (Wikipedia breaks this down in more detail at https://en.wikipedia.org/wiki/Dependency_injection#Advantages) The canonical example is the persistence layer. Today, my service may get data from an SQL database but maybe tomorrow I want it to pull it from a web service.

Most of the advantages you will hear are special cases of this ability to replace dependency implementations. In practice I find that most codebases that use DI do so for unit testing. If a service component relies upon a repository component that is injected, it is easier to replace the repository with a mock or a stub for unit testing.

To be clear, DI has disadvantages as well, the biggest one being the increase of complexity that often translates into a minuscule amount of overhead in the running system and increased complexity when reading the code due to the additional layers of indirection.

Now that we see the problems that DI attempts to solve, we can be clear on the one it doesn't solve, namely object construction. There is nothing wrong with new. It isn't broken. It does what it says on the tin: it creates new instances of a class. So merely creating an object isn't an adequate reason to use DI. We need to be solving for the ability to replace capability implementations.

Is that the case with model/data objects? Typically not. They typically represent data and have relatively little capability of any kind. If there is nothing to inject, there is no reason to use dependency injection. (By way of clarification, I should note that merely composing objects really isn't the same has representing a component dependency; a Person may have an Address, but that may just be an attribute of a Person - it need not mean that a Person depends upon an Address).

On a less abstract, less theoretical note, it is also important to realize that many dependency injection libraries/frameworks make heavy use of singletons (this behavior depends upon the DI framework in use and how it is configured - for example, Spring is singleton-by-default, but it can be configured with other behaviors as well; you just need to know your stack). In fact, the reduction of active instances is sometimes cited as a benefit. This means that you might accidentally wind up with multiple pieces of code mutating the same model/data object. If this occurs, all sorts of random issues and race conditions can occur. If it doesn't, you're really just using the DI framework to implement a factory and it might be more clear to the code base's maintainers if the factory is separated from configuration code.

Which brings me to my last main point. Dependency injection is heavily tied up with the idea of configuration and that the configuration of the system will produce different object graphs. There is not typically much by way of configuration in model/data classes.

Summary: I would find it peculiar to see model or data objects constructed via dependency injection, as they do not typically represent components with dependencies, nor do they typically have a configuration-sensitive aspect. Finally, I would expect to create and discard instances regularly, which doesn't play well with the singleton-first mindset of most DI frameworks.

I would definitely recommend reading Martin Fowler's description of the pattern (https://martinfowler.com/articles/injection.html) and some of the discussion on StackOverflow (https://stackoverflow.com/questions/871405/why-do-i-need-an-ioc-container-as-opposed-to-straightforward-di-code being one of the most interesting to me) for some good perspectives on what DI is for and why it may or may not apply to a given use case.

3
  • Thanks for your insight! In the second article you linked, you can see the problem he's trying to solve with dependency injection. Arguing, that it wasn't originally intended for that purpose, is one thing, but it's also used to split up huge classes into many tiny worker classes. The real question might be: how can you split up a huge entity/aggregate root classes without resorting to dependency injection as a crutch.
    – KeTr
    Sep 28, 2018 at 6:01
  • That's a fair point. My knee-jerk reaction would be to say that factories were made for this purpose. My next thought would be to ask how the domain model reached that level of complexity - it seems like there might be an opportunity to do some streamlining.
    – Michael
    Oct 3, 2018 at 16:05
  • "There is nothing wrong with new. It isn't broken. It does what it says on the tin: it creates new instances of a class." +1 for this tidbit alone. A DI framework is basically the Factory Pattern on steroids. If you don't need polymorphism, then it is perfectly fine architecture to new up an object precisely where you need it. Jan 29, 2020 at 23:05
0

Couple of points I would like to add here:

You are both right imho. Your colleague on the topic of avoiding huge "God" classes and violating SRP and you on the basic usage of a DI framework which should hydrate configurable components but probably not domain objects.

Maybe you can work out a solution on which you both agree.

  • DI does not imply usage of a framework or an IoC container. in its essence classes having a constructor to get instatiated with whatever they need to function is already DI since you are not constructing your dependencies yourself (via new or else)
  • factor your "god" classes into multiple classes (and/or interfaces which have a "trait" character) according to SRP. if possible your former god class now just composes a lot of value objects/entities, each containing a vital, separated piece of logic.
  • DDD does not shy away from redundancies. I have witnessed (non-ddd) projects with huge Person entities which have several bounded contexts overlap on one class. If your realworld entity belongs to multiple contexts, model it multiple times and cover the relevant aspects of this context in the resulting entities/aggregates (this might lead to perceived "redundancies" in data/fields)

Your Answer

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

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