Defining Specifications Concept0:37
So you might say something like, customer is gold, right? That is a very important concept in your domain, because if a customer is labeled gold, then they get access to all of these additional things that no one else in your application gets. So we take this concept and we elevate it to a first-class citizen as a class. Now, we're going to think of this as a specification. And what we do is we apply it to any existing, in this case, customer objects, and we determine if that customer satisfies the rule. So we take whatever logic is required to determine if a customer is in fact gold, and we accept a customer object and perform that business rule upon it and find the answer. For instance, the method is satisfiedBy, and we are working with customers here, so I'm going to accept a customer object. Now, how would we verify that a customer is gold?
For instance, method is satisfied by, and we are working with customers here, so I'm going to accept a Customer object. Now, how would we verify that a Customer is gold? And the answer is, well, I don't know. We don't have a real project here. Maybe you reference some kind of relationship. Maybe it's as simple as checking to see if the customer's plan or type equals gold. Or maybe it's something way more complex than that. It doesn't matter. We just take the logic for determining if a customer is in fact gold, and we wrap it as a class that you can reference anywhere in your projects. Now, this begs the question, well, if we're doing that, why don't you just have, for example, on your Customer class an isGold method where all you do is return this type equals gold? Absolutely, that's the case. And that's what I mean when I say in plenty of situations,
When to Use Pattern2:03
on your Customer class an isGold method where all you do is return $this type equals gold? Absolutely, that's the case. And that's what I mean when I say in plenty of situations, this is all you need. And in fact, like I was saying earlier, I would default to this second method right here. However, you might find some situations where you're not content with this just being nested as some random method among many within your Customer class. As with anything, you need to measure both options and decide, does this option make it better? Just because it's a pattern doesn't mean it's better. You measure both and you say, you know what, I did this, but I don't feel like I got any value out of it. So I'm just going to go back to this simpler option, and it seems to be doing the trick for me. Perfect, you're doing it great. Or you might say, you know what, this actually isn't what I need,
Applying Specs and Testing3:22
customer specification, and that would be an interface that simply contains a single method, isSatisfiedBy. Or if you don't want to, you don't have to, you're in charge. Now, we're going to stick with this very simple logic, but remember, in real life, it might require you querying a database or something of that nature. That's fine as well. So next, how do we actually interact with this? And we do it in some form like this. You would new up your specification. Next, and we'll call that spec. Next, we would say, spec, are you satisfied by this customer object that I give you? And that's either going to return true or false. And that way, like I said, we've taken this concept, we elevate it to a first class citizen, and now it can be referenced anywhere simply by newing up an object. Why don't we write a test for this, and then we'll continue forward. I'll create customer.php, and we'll have class Customer. If you are working with Laravel,
newing up an object. Why don't we write a test for this, and then we'll continue forward. I'll create customer.php, and we'll have class Customer. If you are working with Laravel, you might have this extend Eloquent, but in this case, it can be a simple class. So why don't we say, when we initialize this, you know, in real life, you'd have a name and an email address and all of that stuff. We're going to keep it as basic as we can and just accept type. All right, so let's build this. I'm going to use a macro here and accept a plan. Now, I want to write a test for this. So why don't we do this? If I list my files, well, why don't we just create a composer.json file really quickly. And then I can say composer require, we'll use phpunit for this. Cool. So if we can't composer.json, we've pulled that in. Next, I will make a directory called tests. And let's go ahead and create a, and now what exactly
require, we'll use phpunit for this. Cool. So if we can't composer.json, we've pulled that in. Next, I will make a directory called tests. And let's go ahead and create a, and now what exactly are we testing, it would be the CustomerIsGoldTest.php. And let's work on that. Okay, so we'll open this up. I will go ahead and add a test case here called CustomerIsGoldTest. Okay, so what exactly does it do? Well, maybe we could say this, a customer is gold if they have the respective type. Remember, we don't really have anything real life here. So in your situation, you'd have something a little more specific, but this will do. Okay, so I want to test this, the first thing I would need to do is new up CustomerIsGold, but I haven't created that yet. We'll store this within a src directory, something like this. And we'll say class CustomerIsGold, like that. Or I can take what we built and just switch that over right here.
We'll store this within a source directory, something like this. And we'll say class Customer is gold, like that. Or I can take what we built and just switch that over right here. Alright, now I can take index.php, remove that completely. And then customer.php. And let's go ahead and rename that so that I can move it into the source directory. Okay, so now you see both of them right here. Finally, I want some auto loading setup, right. And we could use something like PSR-4, which I would use in real life with namespacing. But it just seems like overkill for this case. So instead, we'll say, within my composer.json file, I want to auto load just a class map and anything specifically within the source directory. Okay, auto load all of these files. So let's run a composer dump-autoload in order for these changes to be reflected. And then within here, well, remember, if you're running your tests, you first need to bootstrap your
files. So let's run composer dump-autoload in order for these changes to be reflected. And then within here, well, remember, if you're running your tests, you first need to bootstrap your tests so that we can load composer's auto loader. This is just composer one on one type stuff. So why don't we create a tests/bootstrap.php file, or we can reference the auto loader when we call phpunit as well from the command line. But this is fine. And we'll just say require vendor/autoload.php. And finally, I can say phpunit with colors on the tests directory. And we get green. Of course we get green, we haven't written a test yet. So we'll say Customer here. And now in order to test it, how would I do that? How do you go about testing something like this? It's the easiest thing in the world. Just pass in a Customer object and make sure it returns what you expect. And when you're doing something like TDD, especially for classes
something like this? It's the easiest thing in the world. Just pass in a customer object and make sure it returns what you expect. And when you're doing something like TDD, especially for classes like this, it's the most natural thing in the world. So let's tackle that. I'm going to call this spec or specification. And then we new up a customer, this will be a goldCustomer. So we'll say goldCustomer. And finally, to test this, I only have to say specification is satisfied by a goldCustomer. And what that should be true, right? So this assertTrue. And then we reference that. That's all there is to it. So let's try it. And we get red, undefined property customerType. And you know what, that's probably because we used a different variable name. All right, we compare against the type, but within the customer, it's actually a plan. It doesn't matter which one it is. We'll just switch this over to type. And if we run it again,
we compare against the type, but within the customer, it's actually a plan. It doesn't matter which one it is. We'll just switch this over to type. And if we run it again, this time cannot access protected property type, right? This is a protected property. However, our specification is trying to access it here. So we either make it public if that's fine, or if you need to encapsulate it, so to speak, then we could wrap it within a method and then return this type. It just depends upon what you're building here. All right, so we'll bring this back. We'll use the method option in this example and return it. Okay, so if we run it now, sure enough, we get green. But just to make sure it passes, well, if we check to see if it's silver, that should fail, right? And of course it does. All right, so we'll go back to our test and do one more and say, if I build up a new customer,
Building Repository Filtering9:29
want to have a way to fetch all items from the database or from your collection, which pass a specification that you pass in. Wouldn't that be useful? Let's see if we can do that. And well, you know what? Why don't we use TDD here? It's a good learning opportunity. So I will create tests/CustomersRepositoryTest.php. And I will build up my test case, CustomersRepositoryTest. Okay, so what does it do? It fetches all customers who match a given specification. Okay, how do we test this? Well, first, we would need some kind of CustomersRepository, right? So we'll do that. Next, when I say customers, and we could say by specification. And then when we give it a specification, so we need one, let's use that same CustomerIsGoldSpecification. And I will call that spec. Now, if I pass that through here, what do I expect that to return? Well, we don't have a real database just yet. So we'll use sort
gold specification. And I will call that spec. Now, if I pass that through here, what do I expect that to return? Well, we don't have a real database just yet. So we'll use sort of like an in memory collection. But you know what, if you do want to see an actual database example, and how this can be transformed for actually performing database queries, rather than comparisons against existing objects, we'll do a part two of this video to cover that it's pretty cool and useful. Okay, so maybe we can just instantiate this with an array of customers. So for example, we will say new Customer gold, and then let's duplicate that a few more times, we'll have bronze, silver, and then another gold one. Alright, okay, so when we instantiate CustomerRepository, we build up the existing list in memory. Next, we set up our specification. And then we say, okay, I want to find all customers who match this specification. And in fact, we can
customerRepository, we build up the existing list in memory. Next, we set up our specification. And then we say, okay, I want to find all customers who match this specification. And in fact, we can inline this if you want, like that. Now, what is our test? Well, our test is what gets returned are these two customers? Okay, let's see results. And then well, to begin, we could say this assertCount is two, and compare that against our results. Alright, let's run our tests. And of course, it fails, we have no customersRepository. Alright, source customersRepository.php, build that up. Like so. Next, we run it again. What's our next step? Well, we still get that. But remember, it's because we're not using PSR-4. So we don't follow a convention for auto loading, we just auto load everything within a folder, which means we need to do a composer dump-autoload to refresh. Okay, what's the next step? We don't have a method by specification. All right,
we just auto load everything within a folder, which means we need to do a composer dump-autoload to refresh. Okay, what's the next step? We don't have a method by specification. All right, run it again. Next, Customer is spec not found. That's just a typo on my part. Sorry. Let's do it again. Okay, so something a little more readable. Argument two of assertCount must be countable or traversable, which means hey, you probably returned null or nothing at all. And that doesn't work for assertCount. Fair enough. Let's tackle that. And if we think about it, if we're really following the true art of TDD, which you're free to make up your own mind, then we could do something as simple as this to make a pass. And that's kind of one of the rules like do the simplest possible thing to make a pass, it may seem a little silly to you. But actually, there is a bit of merit to it. Nonetheless, it's a good exercise, if nothing more. Okay, we get green,
by and now well, what do we do here Customer, remember, we have a collection. So if we want to find all Customers who satisfy this specification, well, we need to do a foreach, right. And secondly, if we go back, don't forget, we pass this through the constructor. Okay, so you know what, it sounds like we should write one quick test first, let's make sure we can actually fetch all Customers. Fair enough, right. So we will say it fetches all Customers. And then let's do our setup within a setUp method, where we can say this->customers equals, and we're basically going to repeat exactly what we have here. That way, we only have to do this once. Alright, so we will set the property. And now if we want to test that we can fetch all Customers, I can say this->customers->all() and just write our assertion. assertCount for against results should be enough. Let's run our test. It certainly fails because we don't have an all method.
I can say this customers all and just write our assertion. assertCount for against results should be enough. Let's run our test. It certainly fails because we don't have an all method. So we will create that right here. all and all that's going to do in this particular in memory example, don't forget, we'll cover a database persistence layer in the next video. So we will return that. And then we'll say when you instantiate this, you will give us an array of customers. Alright, so if we run this, I think we'll get green and we do. Cool. So now we can move on to this next test. Once again, let's see where we're at. Okay, undefined variable customer on line 13. Let's check it out. Okay, sure enough, we just had that placeholder here. So here's what we'll do. We'll say foreach $this->customers as $customer, then we can perform this test. And we'll say matches is an array, then filter through all of them. And for each one
So here's what we'll do. We'll say for each $customers, as $customer, then we can perform this test. And we'll say $matches is an array, then filter through all of them. And for each one that does match, we can append to it, like so. And then finally, when we're done, return all of our $matches. And I think that should do it. So we run it. And no, it didn't. Unexpected A on line 15. Yeah, sorry about that. Run it again. And this time we get green, but we don't. Okay, failed asserting that the actual size four matches the expected size two. Let's go back. And yeah, obvious thing. But this is actually where tests are pretty helpful. It gets you into a flow where if you just have a brain freeze, like I just did, it'll immediately tell you and you know, you can fix it. So in this case, we're going to wrap it within a conditional as we should have done in the first place. And only on the condition if it matches, do we actually add the
you can fix it. So in this case, we're going to wrap it within a conditional as we should have done in the first place. And only on the condition if it matches, do we actually add the customer to the result set, obviously. Okay, this time, I'm pretty sure it'll pass. And sure enough, it does. Okay, so that is actually one pretty cool thing about the specification pattern. If you're using a Collection or a Repository class, and you want to find all records that match some kind of business rule. In this case, we want customers who are gold. Well, you can take the logic that performs that specific calculation, no matter how complex it might be. And you wrap that within its own object. And then in your repository. Remember, if you don't really understand repositories, just think of it as like collection containers, you can fetch from a database or you can fetch from an in memory set of customers. In this example, it doesn't matter. It's just like a
repositories, just think of it as like collection containers, you can fetch from a database or you can fetch from an in memory set of customers. In this example, it doesn't matter. It's just like a collection that you can add to you can select from stuff like that very useful in some specific cases. Anyways, you pass in your specification. And now if we want to determine which customers satisfy that specification, that's actually a fairly readable way to go about it. And actually, as we finish up, I think this is pretty common, I kind of like matching specification, give me all the customers who match the given specification. So that fails, let's make a pass, like so. And that should be all there is to it. Cool. Okay, so consider that part one of our review of the specification pattern. In the next video, we'll take this further. And I'll leave you with a little teaser. Think about just using a typical repository, like if you're using Laravel, where
Extending Specs for Database17:36
specification pattern. In the next video, we'll take this further. And I'll leave you with a little teaser. Think about just using a typical repository, like if you're using Laravel, where you are querying from the database. Well, in this case, we wouldn't have an in memory set of users, you would be doing something like Customer::all(). So that means if we want to find all customers who match a specification, your very first step would be to get every single customer from the database, and then filter through them. If you have 30 customers, that's fine. But if you have 30,000 customers, this is the slowest possible way to figure that out. So instead, it sounds like our specifications need to be a little more flexible. Or maybe we can extend it, maybe you could have one method to determine, and this is great for single objects, is this customer object a gold customer. But maybe we also want to include a method that, for example,
maybe you could have one method to determine, and this is great for single objects, is this customer object a gold customer. But maybe we also want to include a method that, for example, performs a scope on one of Eloquent builder objects, or maybe it returns SQL, or something database specific. If that's the case, it doesn't really matter. But that would fit just fine within this same class. And we'll review that in the next lesson. So in closing, it's really important for me to get across that this is something you reach for when you need it. You do not start a project by immediately creating specification classes for every possible business rule that you would need to query against. No, I would always say keep it simple and add a method to your model. However, when you find that you need something a little bit more, that is only when you consider creating a specification. Okay, I'll see you in part two.
