We've built a base class that is starting to sprawl, and now we want to unit test and refactor out the caching logic into a mixin that will handle this functionality.
This lesson is part of a series.
Now we've got caching working. The cache illustrates for us a common pattern that we're going to use as we build the base class, and that's that we're going to define bits of the functionality -- isolated components like caching -- and we're going to include them in the base class.
We can already see from the pattern as we've used it so far that the base class has to know a lot about how cache works, in order for it to work.
It has to know that it's got to expose some kind of a cache property, and that's going to be equal to a new cache. It has to know that the cache is a constructor, and that it has to construct it. Also, in order to cache individual instances -- which is what we're really interested in doing with the cache -- it has to know that it has to call onto this cached object.
It has to call the cache method, and it has to know the signature of this function. It has to know that it passes in an instance and a primary key.
This is a lot of knowledge that the base class has to know about the cache module, so it has a lot of really obvious dependencies on the cache module.
I want to avoid this, and the way that I want to avoid this is by creating mixins. Really, the way I want mixins to work is that we would say "constructor.extend" and we would pass in the name of the module that we want to extend with, like "cacheable." What extend will do is it will mix in class methods and class properties. That means that it will expose properties on the constructor.
I also want to define something called "include," and we could include cacheable. What include will do is it will add properties to new instances of any of our base classes, so this instance will get properties added to it that are exposed via include.
The way that we're going to do this in each of our classes, in each of the modules that we're including here, is by exposing things on this. If we say "this.cached=newCache," it will be the equivalent of adding this line here to the constructor. If we say "this.cache=this.cached.cache," it will be the equivalent of adding this here.
We're going to simplify this signature so that we only have to pass in an instance. That way, what we're really interested in doing -- which is saying constructor.cache a particular instance -- will be entirely handled by the cache module, which we'll redefine as "cacheable."
In terms of exposing instance properties, what we're going to do is expose things with double underscores. This is just something that will be a particular syntax for us and for our library. If we say "this.cache," for instance, this would expose a cache method on the instance -- which we don't need to do any of with the cacheable module, but we will need to do some in the future, so this is just going to be an example.
Obviously, this is just pseudo-code comments, to show you how all this is going to work, so I'm going to pull all this out right now, and let's get started.
Let's dive right in and write a failing spec.
Here we're going to describe the extend function, and before each, what we're going to want to do is to say that our post extends from something called postable, and we'll describe that here. Remember, anything that we define on the public interface of postable is going to be transferred over to the post class.
We'll define a property, we'll define a function, and we're also going to use object.defineProperty to ensure that we copy over getters and setters that might be set for us. Here we'll just create something with a "get" function, and we'll just say it returns to me -- I'm the poster.
Let's say here that it adds the properties from the mixin to the class -- simple enough. Obviously, the expectation here is that post.posted is true. Fair enough. We're going to do the same thing with functions. We want to make sure that post is defined and we want to do the same thing with defined properties.
Here, we want to make sure that post.poster=me. These fail right now, and we're on our way.
The location that we've been defining public functions in is this base class down here, so we'll keep defining them here. Here we're going to add, again, function.prototype.extend. It's going to be a function, and it's going to take a module as an argument.
We're going to say that the properties of this are going to be equal to a new module -- we want to instantiate it so that each time we get a new copy of what's inside, I want to add the property names as a variable. This is going to use Object.getOwnPropertyNames, and we're going to do that for the properties object.
What that's going to look like is an array of the property names. In the case that we saw before, it's going to be post, it's going to be poster, and it's going to be posted. That's what that will look like.
What we're really interested in are the class prop names, as distinguished from the double underscore instance prop names that we're going to define as well. What we want to do is use _.remove on the property names, so each time we're going to get a property name.
We only want to remove the property names, return, propName.slice zero to two, is not equal to __. Those are the ones we want.
Now what we want to do is loop through each of the class property names, so we'll go through each class property name. We want to make sure we set the context here, because we're going to be adding each class property name to this, which is the function that we're extending.
What we want to say is the property descriptor is Object.getOwnPropertyDescriptor. This is another function we get that's similar to getOwnPropertyNames, so we'll get it from properties and we'll use the class property name. Then we'll say Object.defineProperty, and we want to define it on this. We want to define the object by the name of the class property name, and we want to use the property descriptor as the description.
There we go -- all three tests are passing.
Of course, for our instance methods everything is going to look exactly the same, so we'll just add some double underscores here, and we'll instantiate the post and expect the same three things -- post.posted to be true, et cetera.
We save and we see we get the same three failures.
The setup for include looks almost identical to the setup for extend, with the exception of us looking exclusively for these double underscore values. Next I want to copy out the constructor function, because we need to override it so that whenever we create new instances, we're going to update them with all of these instance property names.
Here, we'll just say this.new is equal to a function. It doesn't matter what gets passed into it, because we can say the value of the instance is equal to oldConstructor.apply to the context this, and with the arguments that are passed in, so we're sure that everything that would have happened in the old constructor function is going to continue to happen in this constructor function. We're just wrapping it here and doing some additional work around it.
The last thing to do is identical, again, to extend. We'll copy each of these over to the new instance, and then we'll return the instance -- since this is, after all, a constructor -- and all of our tests pass. As we see, we now have extend and include working the way we expect them to.
Since we already have a nice test harness with our cache, we can go ahead and start refactoring this here, knowing full well that if it breaks we can always get back to the right spot, so we'll say that constructor will extend cacheable. It doesn't need to include cacheable because it won't have any instance methods, so we'll copy over constructor.cached=newCache.
Instead, here we want to say this.cached, and it's not going to be equal to a newCache because we want cache to set this up, so instead we'll say var cached equals an empty object, and everywhere we had this in here before, we're just going to use this cached -- start to replace this -- so now this.cached will just equal this private property cached.
We can start to clean this up.
Now we want to move this caching functionality off, so we want the constructor to just be able to call cache, and not have to worry about the primary key. In here, what we can say is that the primary key is equal to this.primaryKey. Since the context will now always be the constructor, we know we'll have the primary key, so we can totally remove the need to inject the primary key here each time as an argument.
We'll make sure that the instance has the primary key. We need to update this name to "cacheable," and the other value we want to expose is this.cache. That can be this.cached.cache so we'll know about that, and if we head back in here we can say "constructor.cacheInstance," and that should do it.
Now we know that we've successfully refactored that, and this looks a whole lot cleaner, just by extending with cacheable.