DEV Community

loading...

A Successful IOC Pattern with Functions in TypeScript

Rasmus Schultz
Passionate web-developer since 1998 using various languages, databases and tools. Opinions all my own.
・8 min read

Over the past few months, I've been working on a TypeScript project, where I decided to challenge myself to use Functions only. This week, I refactored the codebase to use IOC everywhere, and it feels like I leveled up. πŸ˜„

There's been a lot of articles these past couple of years about "functional programming" in JavaScript, and for some reason these are mostly concerned with immutability, sets, map/reduce, and so on. I come from a background of mostly OOP, where the answer to IOC is largely just "use constructors and interfaces", so this hasn't been really helpful.

What was missing for me, was a functional perspective on IOC and dependency injection.

In this article, I will try to illustrate the problems and solutions with a silly example for illustration purposes: for some reason, your boss wants the browser to display a personalized welcome message using an old-fashioned alert. Yikes. Well, whatever you say, boss, but I expect this requirement will change in the future.

To make the most of this article, you should know some basic TypeScript, and you should be familiar with the terms "inversion of control" or "dependency injection" - at least in the sense of using constructors and interfaces.

While DEV thinks this is "an 8 minute read", I recommend you open the Playground and spend 20-30 minutes getting a feel for this.

Okay, let's say you come up with function like this:

function showMessage(window: Window, message: string) {
  window.alert(message);
}
Enter fullscreen mode Exit fullscreen mode

As you can see, I am already doing dependency injection. Rather than reaching out for the window global, this function asks for an instance of Window, which makes it easy to unit-test this function on a mock Window instance. So far so good.

πŸ’­ So we're done, right? 😁

Not quite.

Pretty soon, you will introduce functions that depend on showMessage - and, in order for another function to call showMessage, the other function needs to supply the window parameter - which means the dependency on Windows spreads to other functions:

function showWelcomeMessage(window: Window, user: string) {
  showMessage(window, `Welcome, ${user}`);
}
Enter fullscreen mode Exit fullscreen mode

But wait, now showWelcomeMessage internally depends on showMessage - we really should use dependency injection for that too, right?

type showMessage = typeof showMessage;

function showWelcomeMessage(showMessage: showMessage, window: Window, user: string) {
  showMessage(window, `Welcome, ${user}`);
}
Enter fullscreen mode Exit fullscreen mode

πŸ’­ This looks wrong. 🀨

showWelcomeMessage had to depend on Window, only so it could pass it along to showMessage - but it doesn't actually do anything with the Window object itself.

And while showMessage happens to use Window today, we might change that in the future, when someone realizes what a sad idea it was to use that alert. Maybe we decide to have it display a toast message on the page instead, and so the dependency changes from Window to Document. That's a breaking change. Now we have to run around and refactor everything that calls showMessage.

Calling any function gets increasingly cumbersome - anytime any of the dependencies of any function changes, we have to manually correct the calls and introduce more dependencies everywhere. We're in dependency hell, and by now we're wasting most of our time refactoring.

πŸ’­ There has to be a better way. πŸ€”

My first realization was, why should someone who wants to call showMessage need to know anything about it's internal dependencies? What I really want, is a function that is internally bound to an instance of Window, so that the caller doesn't need to know or care.

That means we need a factory-function for the actual function:

function createShowMessage(window: Window) {
  return function showMessage(message: string) {
    window.alert(message);
  }
}
Enter fullscreen mode Exit fullscreen mode

We'll need to extract the inner function-type - the one that has the message argument only, so that other units can depend on that:

type showMessage: ReturnType<typeof createShowMessage>;
Enter fullscreen mode Exit fullscreen mode

(Note the user of ReturnType here - you could have manually typed out the function signature of the inner function, but this helps avoid the duplication and the extra refactoring chore going forward.)

With that in place, our showWelcomeMessage no longer needs to care that showMessage internally uses window:

function showWelcomeMessage(showMessage: showMessage, user: string) {
  showMessage(`Welcome, ${user}`);
}
Enter fullscreen mode Exit fullscreen mode

This also makes showWelcomeMessage easier to test, since now we don't need to mock window anymore - we can mock showMessage instead and test that it's being called. The code and the tests will now refactor much better, as they have fewer reasons to change.

πŸ’­ So we're done, right? πŸ˜…

Yeah, but No.

Consider now what happens to the next function up the call hierarchy. Let's say we have a login function, and showing the welcome message happens to be part of what it does - and we apply dependency injection here, too:

type showWelcomeMessage = typeof showWelcomeMessage;

function login(showWelcomeMessage: showWelcomeMessage, user: string) {
  showWelcomeMessage(user)
}
Enter fullscreen mode Exit fullscreen mode

This problem doesn't go away by just fixing it at one level - we need to apply the same pattern we applied to showMessage, wrapping it in a createShowMessage factory-function. And what happens when something else needs to call login? Same thing again.

In fact, as you may have realized by now, we might as well apply this pattern consistently, as a convention, to every function we write.

πŸ’­ Really? To every function?

Yes, really - and bear with me, because it doesn't look pretty:

function createShowMessage(window: Window) {
  return function showMessage(message: string) {
    window.alert(message);
  }
}

type showMessage = ReturnType<typeof createShowMessage>;

function createShowWelcomeMessage(showMessage: showMessage) {
  return function showWelcomeMessage(user: string) {
    showMessage(`Welcome, ${user}`);
  }
}

type showWelcomeMessage = ReturnType<typeof createShowWelcomeMessage>;

function createLogin(showWelcomeMessage: showWelcomeMessage) {
  return function login(user: string) {
    showWelcomeMessage(user);
  }
}

type createLogin = ReturnType<typeof createLogin>;
Enter fullscreen mode Exit fullscreen mode

It does what we wanted though. We can do all of our dependency injection from the top down now - we can now bootstrap everything from a single function in our entry-point script:

function bootstrap(window: Window) {
  const showMessage = createShowMessage(window);

  const showWelcomeMessage = createShowWelcomeMessage(showMessage);

  const login = createLogin(showWelcomeMessage);

  return {
    login
  }
}

// usage:

const { login } = bootstrap(window);

login("Rasmus");
Enter fullscreen mode Exit fullscreen mode

Note that, in this example, bootstrap returns only login - if you have multiple entry-points, you can return more functions.

Now, as helpful as this pattern was, this approach to bootstrapping does not really scale well. There are two problems:

  1. We're creating everything up front. In this simple example, we do need every component - but applications with multiple entry-points might only need some of the components, some of the time.

  2. The code is very sensitive to reordering: you have to carefully arrange your factory-function calls, so that the previous function can be passed to the next. It requires a lot of thinking about dependencies.

We can solve both of these problems by deferring the creation of dependencies until they're required - that is, by making the calls to the factory-functions from within another function. Let's call this a getter-function.

Now, since these getter-functions could potentially be called more than once (although, in this simple example, they're not) we want them to return the same dependency every time - rather than generating new ones.

We can solve this by adding a tiny helper-function once to construct these wrapper-functions and memoize the result:

function once<T>(f: () => T): () => T {
  let instance: T;

  return () => {
    if (instance === undefined) {
      instance = f(); // first call
    } 

    return instance;
  }
}
Enter fullscreen mode Exit fullscreen mode

Let's refactor again: we'll wrap all of our initializations in closures and apply once to them - and our bootstrap function will now return the getLogin function.

(Note that the once function would generate singletons, if you were to call it from the global scope - but since we're calling it from the bootstrap function scope, new instances of all dependencies will be generated for every call to bootstrap.)

The new bootstrap-function looks like this:

function bootstrap(window: Window) {
  const getLogin = once(() => createLogin(getShowWelcomeMessage()));

  const getShowWelcomeMessage = once(() => createShowWelcomeMessage(getShowMessage()));

  const getShowMessage = once(() => createShowMessage(window));

  return {
    getLogin
  }
}

// usage:

const app = bootstrap(window);

const login = app.getLogin();

login("Rasmus");
Enter fullscreen mode Exit fullscreen mode

I've purposely mixed-up the order of these getter-functions, to illustrate the fact that the order no longer matters: we're now free to arrange and group these lines in any order that makes sense - and we're also no longer creating anything before one of the getter-functions is actually called, which removes any concerns about potential future performance problems.

πŸ’­ So we're...?

Yes, done! πŸ†βœ¨

Footnote: When not to apply this pattern

You don't need to apply this pattern to every function. Some functions don't have dependencies, or maybe they depend only on standard JavaScript environment functions.

For example, there's no benefit to injecting the Math.max function, since that's a pure function with no side-effects. Whereas, on the other hand, there's a clear benefit to injecting Math.random, since a mock can return values that aren't actually random - making it possible to write predictable tests for your function.

Bonus: Mutable State

I made one more little discovery this week that I'd like to share.

I think we've all been here one time or another?

let loggedInUser: string | undefined;

function setLoggedInUser(user: string) {
  loggedInUser = user;
}

function getLoggedInUser(): string {
  return loggedInUser;
}
Enter fullscreen mode Exit fullscreen mode

It's dangerously easy and natural to do this in JavaScript. πŸ’£

But even if you put this inside a module, this is global state - and it makes things difficult to test, since setLoggedInUser leaves behind in-memory state that persists between tests. (And you could write more code to clear out this state between tests, but, ugh.)

If you must have mutable state, we need to model that mutable loggedInUser state as a dependency, and then apply the create-function pattern described above.

interface LoginState {
  loggedInUser: string | undefined;
}

function createSetLoggedInUser(state: LoginState) {
  return function setLoggedInUser(user: string) {
    state.loggedInUser = user;
  }
}

function createGetLoggedInUser(state: LoginState) {
  return function getLoggedInUser(user: string) {
    return state.loggedInUser;
  }
}
Enter fullscreen mode Exit fullscreen mode

I could have abbreviated this more, but I actually like seeing the word state here, clarifying the fact that a shared state is being either read or written.

It might be tempting to just take the previous version of this code, wrap it all in a single create-function, and return both of the functions, bound to the same state - but I wouldn't recommend that, because you could end up with many functions that depend on this state, and you don't want to be forced to declare them all in the same create-function. (Also, if you have to write a function that depends on several different state objects, that approach does not work.)

One more piece of advice: don't just create one big state object for all of your mutable state - this will muddy your dependencies, as functions will appear to depend on "the entire application state", even when those functions only actually depend on one property. (If you have multiple properties in the same state object, the cohesion should be high - ideally 100%, meaning every function depends on all of the properties of that object.)

The setLoggedInUser function does have a side-effect, but now the effect is on state that you instantiate and control - making it easy to inject a new state for every test.

I'm not a functional programming Guru yet, and maybe there is more to learn here, but it's definitely a step up from global state. πŸ™‚

Conclusion

I feel like I've finally found a JS/TS code-style that really scales - both in terms of complexity and performance.

Applying this to my codebase has been an absolute breeze. I'm spending considerably less time juggling dependencies or refactoring things. Unit-testing is never a problem anymore.

For years, I've heard proponents of functional programming talk about the benefits - but the articles are mostly about arrays and immutability, which is great, and I've heard all the other great arguments. But it didn't really help me write software, and the outcome of prior attempts too often was either unmanageable or untestable. (But usually both.)

Unlocking this feels like the "next level" for me, and I really hope this puts somebody else on the path to more productive and scalable codebases with TypeScript or JavaScript.

Thanks for reading. Have fun! πŸ˜€βœŒ

Discussion (3)

Collapse
komanton profile image
Anton Komarov • Edited

Thanks Rasmus!πŸ‘
Very useful article especially for programmers with OOP experience.

For now, my question at this moment what is the best choice for my client and for the feature: write code only with higher-order functions, like you presented, or use only classes with constructors?

Collapse
mindplay profile image
Rasmus Schultz Author

That's a difficult question to answer without getting into religious territory. πŸ˜‰

There are FP purists who will knock on OOP and ridicule most of the best practices we rigorously apply to ensure quality. I'm not a purist, and not experienced enough with FP to say how right or wrong they might be.

I can say this from my recent experience, which is what prompted me to write this article: I've been sticking exclusively to functions in this project, and the dependency injection pattern demonstrated here - and I've never had an easier time writing or maintaining my tests.

A function has precisely the dependencies it needs to do precisely the thing it needs to do - whereas, to test a method of a class, you need all the state and dependencies required by the entire class. Whenever your class has a new constructor argument, all the method tests and mocks need updates - you don't have this friction with functions.

I've found my tests are more resilient to refactoring somehow. With OOP tests, my unit tests would frequently break with refactoring - sometimes to the point where it's tempting to scrap the test or rewrite it. It has been much easier to adjust my function tests, even after major refactoring.

In fact, this week I introduced a class to the project, and immediately felt the difficulty of testing and then refactoring that class. Unfortunately, I can't get rid of this class, as it is part of a public API that must be backwards compatible with a legacy system. If I had the choice, I would definitely refactor that to functions.

It seems to scale to greater complexity as well. With OOP, I generally find that development slows down as the project grows - whereas with functions (and this IOC approach) my pace has been steady, even as this project grows larger and more complex. Dependency management is a no-brainer, it just sort of "happens" without much thinking about dependencies beyond the unit you're working on. It all just "unfolds" at run-time without much planning. The only pain that seems to carry over from OOP is the risk creating a circular dependency - this can still be a bit difficult to identify and fix.

The benefits are quite clear to me at this point, even if I still have some difficulty explaining the precise reasons. So this is subjective, but I feel like this is lighter, faster, and the functions are more independent, more fluid to work with than classes.

I would highly recommend you try it. πŸ™‚

Collapse
komanton profile image
Anton Komarov

Thanks! I will try it exactly! Looks very attractive! πŸ‘

Also, I noticed very similar approach in Rust programming language where no classes at all. Only structs and Traits (the concept which similar to higher-order functions in JS).