Why Dependency Injection isn't that bad

Do you like functional programming? I hate it. I like programming with immutable objects without side effects, but I hate functional programming. I think you shouldn't use functional programming just to show that you're smart enough to use difficult techniques. Whether it's functional programming or procedural programming, it's important to write code that's easy to understand and maintain.

Isn't it the cause of the arrogance such as "for sentence prohibition" that the technical supreme principle that the person who knows the difficult technique is great and the person who writes the more esoteric code is great? I think that the path that design patterns once followed follows functional programming.

xkcd

Why Dependency Injection is Bad

(The code in this section is based on this video)

They targeted DI containers as their next target.

In functional programming, functions should be pure. You must always return the same output for an input. The purpose of the IoC container is to allow the internal implementation of a module to be flexibly rewritten from the outside. In the first place, it is in conflict with the purpose of functional programming.

@ApplicationScoped
public class BusinessLogic{

    private final Config config;

    @Inject
    public BusinessLogic( Config config ) {
        this.config = config;
    }

    public Name readName() {
        var parts = config.getName().split( " " );
        return new Name( parts[0], parts[1] );
    }

    public Age readAge() {
        return new Age( config.getAge() );
    }

    public Person readPerson() {
        return new Person( readName(), readAge() );
    }
}

Each method depends on Config. Config is not passed directly as a method argument, but is obtained indirectly by the DI container. Impure functions lead to side effects. Side effects are not good, right?

Then, what to do is to make indirect dependence direct. All you have to do is pass all the dependencies directly as arguments. Let's move the acquisition of the Config instance, which was left to the DI container, to the argument.

def readName(config: Config): Name = {
    val parts = config.name.split(" ")
    Name(parts(0), parts(1))
}

In theory, that's fine, but real-world applications are full of dependencies. Settings, user information, sessions, DB access, cache server calls, external APIs, message queues ... If you do it, you'll end up with a ton of boilerplate that can't be ridiculous of Java, with a ton of arguments per function (and many still argue that it should be done). I'm sure).

A common solution to this problem is to use a leader monad or a state monad. Monads allow functions to remain pure and access context.

(def read-person
  (domonad reader-m  ; wtf is this?
    [name read-name
     age read-age]
    {:name name :age age}))

Now you have a beautiful code. I'm happy.

context

In fact, how good is the code with monads over the code with IoC containers? The clarity is subjective, but "I have to use monads for everything!" I don't think it's overwhelmingly easy to understand. Is it worth introducing a monad just because you want the function to be pure?

In any case, with so many "what's a monad" article, you can conclude that monads are neither easy nor straightforward (there are a lot of object-oriented and commentary articles). You might argue that it is, but object-orientation is also difficult enough (I still don't know the correct way to inherit classes in Java). And the hard part is bad in itself. It creates a much bigger problem than the one Monad was trying to solve.

The problem in the first place was to eliminate the dependency and express it in a function. But here comes one question. Why wasn't Config passed as a parameter in the programming model using DI in the first place?

The answer is simple, because Config is not a parameter. Because it is a ** context ** in the execution of the application. In the parameter passing method, the * caller * determines the context of the context (it is of course the caller who decides what to pass as an argument). In the DI container, neither the caller nor the caller is responsible for the context. That's exactly what the DI container is responsible for. This is exactly the detriment of trying to pass everything in parameters, which is why many functions have parameters that they don't need to use themselves.

Leader monad

  1. Leader monads are a common way to pass context
  2. Basically, wrap an implicit read inside a monad
  3. Advantage: Import is abstracted as a type
  4. But I believe this is like hitting a sparrow with a cannon
  5. Monads are about sequencing, not about passing context

Why Dependency Injection Is Not So Bad

To summarize the previous section, the context is not an input, so the context is not passed as an argument. Dependency injection is passing an argument is ** not **. If you combine the contexts from the parameters and regard them as the input of the function, it will be difficult to see which is the context and which is the parameter. Monads are a way to abstract the context and separate it from the parameters, but at the cost of the complexity of the monad itself.

And above all, the problem of Dependency Rejection, instead of tackling the original problem of dealing with "context", is dogmatically thinking that all problems can be solved by functional programming.

DI makes a clear distinction between parameters and context. Parameters are passed to the function and context is passed from the DI container through the constructor. The dependencies are clearly shown in the constructor. DI is very simple at its core. As an added bonus, code with DI containers has the advantage of being "normal" code. Any programmer who has mastered the language can read it naturally (aside from that, I'm not very fond of using field injection in Java for this very reason).

What separates context and parameters is actually quite vague. Even with the Config given in the example above, there are some situations where it is easier to understand if it is treated as a parameter rather than as a context (for example, parsing a JSON-formatted config string to a Config instance is a good idea. It would be more natural to be a function). However, this argument is still valid in that ** in the function ** clearly showing what is in the context and what is the parameter to the person reading the code reduces the cognitive load. Because it contributes.

Leader monads have the distinct advantage of separating context and parameters. The same is true for DI containers. And DI is much easier to get started than monads.

Scala implicit

Of course, Scala has Scalaz, and if you don't use Scala, you'll be considered a Java programmer and ridiculed (!). Interestingly, Scala also has a feature called ʻimplicit` that allows for contextual abstraction. In Video used for the code example earlier, I will quote the code presented by Professor Odelsky.

type Configured[T] = implicit Config => T

def readPerson: Configured[Option[Person]] =
  attempt(
    Some(Person(readName, readAge))
  ).onError(None)

ʻImplicit` allows you to pass context parameters without the hassle of handwriting arguments. By the way, thanks to the compiler and currying, it's more efficient than making closures. And like DI, you don't have to write "strange" code like monads.

I'm not a Scala programmer and I can't tell if ʻimplicit` works. However, there are some concerns.

However, what I wanted to say here is that there are various techniques for dealing with context. Monads are not the only right thing to do.

Java EE

JSR 365 and CDI are responsible for the DI function in Java EE. Previously, the impression I had when using Java EE at work was the worst. Worst means that it's better than EJB.

However, recently I read the Specifications diagonally, and the impression changed significantly. I was ashamed to say that I was criticizing CDI without knowing the details, only with the knowledge I searched online. Perhaps it was common sense to everyone, but let me pretend to be forgotten.

Typical CDI code looks like the one shown at the beginning of this post. Describe the modules that depend on it as a parameter of the constructor with @Inject.

class BillingService {
  private final CreditCardProcessor processor;
  private final TransactionLog transactionLog;

  @Inject
  BillingService(CreditCardProcessor processor, 
      TransactionLog transactionLog) {
    this.processor = processor;
    this.transactionLog = transactionLog;
  }

  public Receipt chargeOrder(PizzaOrder order, CreditCard creditCard) {
    ...
  }
}

(This example is taken from Guice, not exactly JSR 365 ... Guice's Wiki learns DI I think it's a very useful resource, so please take a look)

I think DI frameworks in any language will look pretty much the same. When testing, you can switch the implementation of CreditCardProcessor so that you don't have to pay for the actual card.

By the way, in Java EE, the HttpServletRequest class can be the target of DI. I couldn't help but wonder this all the time. IoC should be a mechanism for decoupling implementations by relying on abstract modules. But the request is just data. Isn't it natural to pass it as an argument like functional programming? In fact, [Receive as an argument in a raw Servlet](https://docs.oracle.com/javaee/7/api/javax/ servlet / http / HttpServlet.html # doGet-javax.servlet.http.HttpServletRequest-javax.servlet.http.HttpServletResponse-) That's right. For example, isn't it seemingly strange to be able to inject a request into a singleton module? At the implementation level, the actually injected instance is retrieved through a proxy. The proxy swaps the appropriate instances for each context, allowing you to inject into modules with a wider scope. But is it necessary to make it that way?

So CDI is more than just a DI framework. It not only functions as an IoC container, but also has the aspect of providing functions for actively abstracting and handling the context.

The code below is an excerpt from a sample specification.

@SessionScoped @Model
public class Login implements Serializable {
    @Inject Credentials credentials;
    @Inject @Users EntityManager userDatabase;

    private CriteriaQuery<User> query;
    private Parameter<String> usernameParam;
    private Parameter<String> passwordParam;

    private User user;

    @Inject
    void initQuery( @Users EntityManagerFactory emf ) {
        CriteriaBuilder cb = emf.getCriteriaBuilder();
        usernameParam = cb.parameter( String.class );
        passwordParam = cb.parameter( String.class );
        query = cb.createQuery( User.class );
        Root<User> u = query.from( User.class );
        query.select( u );
        query.where(
                cb.equal( u.get( User_.username ), usernameParam ),
                cb.equal( u.get( User_.password ), passwordParam )
        );
    }

    public void login() {
        List<User> results = userDatabase.createQuery( query )
                                         .setParameter( usernameParam, credentials.getUsername() )
                                         .setParameter( passwordParam, credentials.getPassword() )
                                         .getResultList();
        if ( !results.isEmpty() ) {
            user = results.get( 0 );
        }
    }

    public void logout() {
        user = null;
    }

    public boolean isLoggedIn() {
        return user != null;
    }

    @Produces @LoggedIn User getCurrentUser() {
        if ( user == null ) {
            throw new NotLoggedInException();
        } else {
            return user;
        }
    }
}

I'm sure it's full of Java-like redundancy, and I admit that there are a lot of unpleasant parts (like ʻinitQuery`). But when you squint and look at it from the perspective of context control, this class works really perfectly.

@SessionScoped indicates that the instance will be instantiated for each HTTP session.

<f:view>
    <h:form>
        <h:panelGrid columns="2" rendered="#{!login.loggedIn}">
            <h:outputLabel for="username">Username:</h:outputLabel>
            <h:inputText id="username" value="#{credentials.username}"/>
            <h:outputLabel for="password">Password:</h:outputLabel>
            <h:inputText id="password" value="#{credentials.password}"/>
        </h:panelGrid>
        <h:commandButton value="Login" action="#{login.login}" rendered="#{!login.loggedIn}"/>
        <h:commandButton value="Logout" action="#{login.logout}" rendered="#{login.loggedIn}"/>
    </h:form>
</f:view>

The nice thing is that the class that uses this class doesn't have to worry about the session status or the connection to the database at all. You can just @Inject the Login class and access the context without even knowing what's going on behind the scenes. You don't even have to be aware that the Login class is session scope. This is a trick that cannot be imitated in functional programming (the Login class itself is a stateful object that embodies the context of the login process). This class is a mutable object, but the login state is essentially different in the first place. The changes are modeled directly in the code. Other powerful features such as lifecycle hooks, events, and interceptors are provided that are not covered in detail here.

That's why Java's DI isn't just DI, it's named ** Context and ** Dependency Injection.

In fact, I'm still pretty skeptical when asked if I want to write a program in Java EE. The drawback of redundancy is serious, and I don't think the context control mentioned here will work in * all * cases. Variable state control can be tragic if you make a mistake. Nonetheless, it's clear that the person who created the JSR 365 spec took the context issue seriously and designed it very carefully (although it's very rude to write this). I regret that it is not good to criticize without knowing anything.

Summary

The problem with Dependency Rejection is that it ignores context issues. Monads are too difficult as a way to abstract context. DI isn't perfect, but it does allow you to work with context in a relatively straightforward way. You don't have to feel inferior just because you're using Java and because you don't know monads.

If you read this post and adopt Jakarta EE and your project burns, I'm not responsible.

Recommended Posts

Why Dependency Injection isn't that bad
Dagger2 --Android Dependency Injection
What is DI (Dependency Injection)
DI: What is Dependency Injection?
Summarize Ruby and Dependency Injection