Inversion of Control is a simple principle that can drastically improve your reusable code. In this lesson we'll go over the concept and you'll get an idea of how you can apply it for your own abstractions.
This is based on my blog post: Inversion of Control
Instructor: [0:00] Here we have a result and input variable here. We want to get a result from filtering on this input. Right now, all that our filter function does is, it filters out undefined and null. It does not filter out empty strings or zero, and that's what we're getting here.
[0:16] Let's say we get a whole bunch more of use cases for our code. Now we want to filter null false, we want to continue to filter undefined, here we don't want to filter undefined, but we'll continue filtering null, here we want to filter zero, here we want to filter the empty string, and then we could do a whole combination of all of these.
[0:34] How do we build that? Without inversion of control, we're going to build it with these options here. Let's go ahead and make that happen. We're going to say, here's our options, we're going to destructure those options. We'll default to an empty object. All of the options are filter null, and filter undefined, and filter empty string, and filter zero.
[0:57] These are going to have defaults as well. Filter null by default, we're going to keep the same API that we had before. Filter null will be true, filter undefined will also be true. Filter empty string will start out as false, and filter zero will start out as false as well.
[1:11] Now, we're going to have to change this logic quite a bit. What I'm going to do right here is, we'll just have an if statement. We'll have an if for every single one of this. Filter null and the element is null. Then, we'll continue and we won't add the element to the array. Otherwise, we'll add the element to the array.
[1:28] Then we're also going to want to do this if filter undefined and the elements is undefined. We're going to do this same thing for the other options as well. If we have filter empty string, then our element triple equals empty string then we'll want to filter it.
[1:46] Then filter zero if our element triple equals zero, then we'll want to continue and not add it to our new array. Let's go ahead and save that.
[1:55] We'll come down here. Now, we've got our filter null, false, and so we get null in there, filter undefined is false, so we get undefined in there. Filter is zero is true, so we no longer have that zero in that first slot, and filter empty string is true and so we no longer have the empty string here at the end.
[2:14] We can also combine these in all sorts of way. We could filter zero here and filter undefined is false and so then we get our undefined in there.
[2:23] You have all that control which you may feel is a nice API because it gives you so much power. It is limiting you because the users only have these options that you provided to be able to control the way that your function operates.
[2:38] That might be fine, if you only have one or two use cases. If you have several use cases and many combinations of use cases, then this if statement and many more like it, could be added to your abstraction to make it complicated.
[2:53] You start adding support for use cases that don't even exist, which means that you have to add tests for those use cases, you have to maintain those tests, and nobody even cares about those use cases anyway.
[3:03] Let's back up here, and we're right back with our original filter. If people came to us and said, "Hey, I want to be able to do this," you could say, "Hold on. No, no, no, this isn't what you wanna be able to do. What you wanna be able to do is control how this filtering works." What we're going to do is, we'll have a filter function.
[3:21] Instead of saying, "Oh, if the element is not null and not undefined, then we should add it," we're going to let you, as the user, be in total control over how we should determine whether something should be added to this new array.
[3:32] We're going to call your filter function with the element. That's going to give you control over whether or not you want this to be added to the array. If you return true, then we'll add it to the array. If you don't return true or you return false, then we won't add it to the array.
[3:47] Let's go ahead and see how this changes the API. For the first one, we're going to need to provide our own function that takes the element. We'll say, if the el is not equal to null and the el is not equal to undefined, then we want to keep it in the array. That gets us back to the original implementation of our filter function.
[4:05] For this one, we want to do the same thing, except we don't want to filter null. What we can do is, I'll copy this, and then we'll get rid of the part that we don't like. It's quite a bit more natural to do that.
[4:17] If somebody needed that use case and they saw this usage example, they'd say, "Oh, I basically need the same thing, I just don't need that part." You don't need to look up into the API docs to understand any of this stuff. You can copy it and modify it the way that you need.
[4:30] We can do the same thing for this one, filterUndefined. We'll switch this to the null version. Now we can keep our undefineds, but we filter out the nulls. For our filter zero true, we want to continue to filter out null and undefined, but also we want to filter out zero.
[4:44] I'm going to copy this, and we'll say, and el is not equal to zero. There we go. Then, we get rid of that zero. Perfect.
[4:53] Then, we'll copy this, paste it down here, and empty string. We get rid of that empty string. We've been able to support all of the use cases with a very simple implementation, and a pretty simple API.
[5:05] There's only two inputs, there's no options that people need to learn, they can focus on their specific use cases that they need. You may say, "Hey, this API is a lot more complicated than the API that we had before. I kinda liked the API we had before."
[5:21] What I would say to you is, you could have a function here, called filterNullAndUndefined, and this could take the inputs. It'll return all of this, and we'll take that input.
[5:34] Then you can say, filterNullAndUndefined, and you get that same nice API that you liked before. This is the beauty of composition, is it allows us to do this kind of thing easily.
[5:45] To take things even a step further, we could take some interesting input here, and filter it in a way that would be difficult to add an option for in our previous implementation.
[5:55] Again, if this is a common use case, then you make a function called filterByLegCount, take the array and leg count, and then we'll grab all this code, we'll return that, and instead of this baked-in array, we'll pass the array.
[6:12] Then here, we can call console.log(filterByLegCount). We'll pass in that array, and pass in the leg count we want to filter by zero.
[6:21] Inversion of control is a powerful mechanism for making implementations a lot simpler, by acknowledging the fact that, as use cases pile onto abstractions, it makes the abstraction more complicated internally as well as externally.
[6:35] When you see that happening, you can invert control by saying, "Hey, I'm not gonna be responsible for this part of what's going on here. I'm going to give you, as the user, responsibility over that." If there's something that's common to do, then you can compose that behavior together, so you can have a nice API and have the flexible API.
[6:54] For the vast majority of people who need to filter out null and undefined, they can use this function. If that isn't serving the purpose for other people, then they can dive down deeper into the lower level API without bugging you, as the maintainer of the abstraction, to add a special if statement for their particular use case.