تماشای این درس نیاز به اشتراک حرفه‌ای دارد.

Refactor for multiple retailers0:00

Okay, so my tests are currently passing, which means I'm ready to move on to the Stock model where we can address this. But first, it's important for me to note, if BestBuy were the only retailer, then keep it simple. There's no need to extract a bunch of classes, an interface, things like that. Keep it simple. However, in real life and for production, you're probably going to have a dozen different retailers that you or clients that you track, and so you'd end up with some variant of this, and you definitely don't want that. Okay, so why don't we begin refactoring this bit by bit?

So it sounds like we want to update the local database at the end, regardless of which retailer we track. So that leaves this. At the moment, this is how we are querying the Best Buy API, at least how we're faking it for now. So why don't we start by extracting a method called checkAvailability, and everything should still work. And it does. But yeah, now imagine you have a new one, like checkTarget. Well, now it sounds like we need to be more specific, right?

But yeah, now imagine you have a new one, like checkTarget. Well, now it sounds like we need to be more specific, right? CheckTargetAvailability, and then this one would become checkBestBuyAvailability, right? So you start working towards something like that. Now I don't necessarily love this, but often I do begin by extracting methods, because the way I name the method is sort of an indication of the next refactor. So notice, we really wanted the method to be called check, or checkAvailability. But then we had to make it specific, because there were different strategies for how we check availability based on the retailer. So we had checkBestBuy, checkWalmartAvailability, checkTargetAvailability.

Extract retailer client classes2:06

check availability based on the retailer. So we had checkBestBuy, checkWalmartAvailability, checkTargetAvailability. So when you have these situations, instead, you might consider creating classes for each of these clients. And then, for each of those respective classes, you can return to a simple method name like check, or checkAvailability. So why don't we give that a shot? Let's start by getting rid of target. We don't have that yet. Tests are passing.

We don't have that yet. Tests are passing. Okay, so we can try another refactor. I'm going to create a directory here called clients. That's pretty typical in terms of naming convention. This would be a client for interacting with a specific API, a target client, or a Stripe client, or a Best Buy client. So within here, we're going to add our first one for Best Buy, and within it, a method called checkAvailability, and then it'll accept the stock. Okay, this is our API.

called checkAvailability, and then it'll accept the stock. Okay, this is our API. So if I come back now, this code here can move to our new class. Now if I switch back, we can get rid of all of that, and this can now become a new BestBuy checkAvailability for the stock, and that would be the current instance. Remember, we have a Stock model. So let's give it a reformat, rerun the tests. Oh, it fails. It looks like we have to import HTTP. All right, run it again, and now it passes.

It looks like we have to import HTTP. All right, run it again, and now it passes. So that's a decent refactor. So yeah, now when you have another one, let's say for target, then you'd have a target client. And notice the signature or the API that each of these conforms to is a class that offers a method checkAvailability. So we could either use duck typing here, or we could force them to conform to an interface. So if you wanted to take that approach, and that might be useful if you have, you know, sometimes if it's even one or two, just don't worry about it. It depends on the team.

Add client interface4:06

sometimes if it's even one or two, just don't worry about it. It depends on the team. But if I have many, it can be useful. So we could add an interface here called Client, like so. And yet again, we're going to have checkAvailability for stock. There's your interface. So now every strategy we have here needs to implement that interface. And what's nice here is it gives you a little bit of pointers. So as long as you implement that, phpStorm is going to let me know, hey, you got to add this method here.

So as long as you implement that, phpStorm is going to let me know, hey, you got to add this method here. But we've already done that. Okay, so we're making progress. Now, if I do supportTarget, we would say class Target implements OurClient. And then it would need to add a method stub here. And then we query or track the stock at Target however we need to. Maybe you're doing web scraping. Maybe they offer an API you have access to. Maybe you're using an unofficial third-party API.

Normalize API responses5:07

Maybe they offer an API you have access to. Maybe you're using an unofficial third-party API. It doesn't matter. Add your code here. So let's come back to stock. We'll go ahead and import this. And now if you're paying attention, you might see something off. So notice when we update our local database, we are assuming that what is returned from this API call in terms of the keys is going to be the same across every client. So notice here, we're assuming target's API returns JSON that has an available key and

this API call in terms of the keys is going to be the same across every client. So notice here, we're assuming target's API returns JSON that has an available key and a price key. And we know that's not going to be the case. They all choose their own keys. So what Best Buy may call available, target may call in stock. Or Walmart might call availability. So we need to figure out some way to normalize all of these down into a simple object that we can then pass to our update method. And that way, when we call update, it doesn't matter whether it came from Walmart or Target.

we can then pass to our update method. And that way, when we call update, it doesn't matter whether it came from Walmart or Target or Best Buy. It's still going to follow the same basic structure. So I think we can normalize the results of these API calls into like a simple POPO, like a plain old PHP object like this. When I run, or when I query against Best Buy's API, we're going to assume that it returns... Let's bring this back. It returns an available key and a price key. So if that's the case, let's normalize it into a class like something that represents

It returns an available key and a price key. So if that's the case, let's normalize it into a class like something that represents the response, like ClientResponse or the stock availability or stock status, whatever you want. But then we could pass in the availability and the price and whatever else is necessary. So now, we will create this new class here that represents the status of a stock. And you know what? I don't necessarily love that name. We might tweak that. But yeah, to start, we need to know if it's available and what the price is.

We might tweak that. But yeah, to start, we need to know if it's available and what the price is. And that's the only thing we're tracking at the moment. So let's add our constructor there, like so, and there we go. Now, BestBuy will instantiate that class and pass in the relevant data. And now it will return not an array, but the status of the stock. And we get something like that. Okay, so now, if we go back to our client, let's update this. If you conform to this API, your implementation needs to return a StockStatus instance because we need that in order to work with it.

If you conform to this API, your implementation needs to return a stock status instance because we need that in order to work with it. So now, target, whenever we implement that, would do the exact same thing. Great. Let's go back to stock, see how we're doing. Now, we don't have a results array, we have the status. So something like this, but I'm just going to call it status, and then the same thing here. Okay, so now, we can update this section. And actually, real quick before we do, keep in mind, somewhere around here, we need to

Okay, so now, we can update this section. And actually, real quick before we do, keep in mind, somewhere around here, we need to check. If we don't have a Client class available for the Retailer, we need to throw an exception. And we'll take care of that soon. But one thing at a time. So now, I can say status, and check it out, we have our keys here, or our properties. status, price. All right, let's see how we're doing. We run the code, and it works.

All right, let's see how we're doing. We run the code, and it works. How cool is that? But we're not done yet. So yeah, we still have the repetition, right? So if we had Walmart, then you're doing this. And it's basically the same code, with a few variables swapped out. So in these cases, well, what if I said this retailerName, that's going to be Best Buy, right? And if I passed that to str_replace, what that's going to do is basically convert it.

Dynamically resolve client class9:09

right? And if I passed that to String Steadly, what that's going to do is basically convert it to this, which looks like our class name. So what if we save that? And then the path to the class would be app/clients/Class, right? So now we will have app/clients/BestBuy. And it looks like we can dynamically figure out what the client class is. So have a look. If we got rid of this, I could now say new, and hold on, I don't like that. Let's do class, and then we'll just inline it here.

If we got rid of this, I could now say new, and hold on, I don't like that. Let's do class, and then we'll just inline it here. Like that. Okay. So now, create the class dynamically, then call checkAvailability. Like this. And let's see if that worked. Ah, it didn't. Oh yeah, except the status. And run it, and now we get green.

Test missing client exception10:04

Oh yeah, except the status. And run it, and now we get green. So now that'll work for every implementation. But yeah, what if somehow we have a Retailer that doesn't have a respective Client? So with that in mind, let's create a test. And this is the Stock model, so we'll do a unit test for Stock. Let's implement Laravel's TestCase. Okay. And I'll say it throws an exception if a Client is not found when tracking. We may change it a little lengthy, but that's okay.

And I'll say it throws an exception if a client is not found when tracking. We may change it a little lengthy, but that's okay. So let's write it out. Given I have a Retailer with stock, if I track that stock, and if the Retailer doesn't have a Client class, then an exception should be thrown. Yeah, I think that's basically it. Okay, so let's seed our database. We have that RetailerSeeder that we created in the last episode. So that will give us a Retailer and a Product. But it's, I'm kind of hard-coding this here, it's assuming a Best Buy Retailer.

So that will give us a Retailer and a Product. But it's, I'm kind of hard-coding this here, it's assuming a Best Buy Retailer. So what I could do is just update it, or do a factory, whatever you want. I could just manually update it and say, Foo Retailer. Then if I track the stock, and again, it's the only stock in my test database, so I could do a simple query. And I called track on it, and if the Retailer doesn't have a Client class, yeah, that's sort of like right up here, then an exception should be thrown. So let's say this, expectException, and I'm going to start with the StandardException class, but we might upgrade that.

So let's say this, expect Exception, and I'm going to start with the standard Exception class, but we might upgrade that. Anyhow, let's pull in RefreshDatabase and give it a shot. Okay, it fails, yeah, it does fail. So notice it's trying to load that class, but it wasn't found, so we get an error. This is what we're trying to test against. So we come back to stock, and it sounds like we should say, well, if it doesn't exist, then throw an Exception. Client not found for the retailerName. So let's give it a run.

Client not found for the retailer name. So let's give it a run. Exception client, so that's what we wanted. Hmm, oh yeah, I'm sorry. This of course should be before you put things into motion. Has to come first. Okay, so run it again, and it passes. Okay, great. So now we have a test to ensure that if a class isn't available for the retailer, okay, it's going to fail, and we know to fix that.

So now we have a test to ensure that if a class isn't available for the retailer, okay, it's going to fail, and we know to fix that. However, since we're here, I wouldn't mind upgrading this to a ClientException. So let's add a new one here called ClientException, like so. And now we can be a little more specific. Throw a ClientException. And then if I come back, let's make sure that it throws a ClientException. So run it, and that should still pass, and it does. So now I can clean this up. And here's our test.

So now I can clean this up. And here's our test. Okay, so how are we doing? The full suite is still passing, but I think we still got more work to do. Now if you look at this, think about it. This is a factory. We are accepting some kind of value, and based upon that value, we are instantiating a class. So it is literally a factory. So with that in mind, if you want, you might extract this to a class.

Extract a client factory13:44

So it is literally a factory. So with that in mind, if you want, you might extract this to a class. So this is where I was talking about how if you only have one Client, keep it simple. You don't need to create four classes. But if you have 10, I know it seems like we're creating a number of files here, but it'll end up benefiting you in the long run. So now we're going to have a ClientFactory here where we will literally make a class based upon the Retailer or the retailer name. So let's just slowly work on this. I'll grab all of that, bring it in.

So let's just slowly work on this. I'll grab all of that, bring it in. So what data will we need? Sounds like we need the retailer. So let's accept that. And then ultimately, we're not going to return checkAvailability, yet ultimately we're going to return a new instance of one of these client classes, right? So this is where the interface becomes useful because I can make it clear this factory accepts a retailer and it returns the associated client for that retailer. Okay, so let's keep going.

a retailer and it returns the associated Client for that retailer. Okay, so let's keep going. We now have a retailer's name. We figure out the class name. We throw an Exception here, otherwise we return the class. Okay, so let's come back to Stock and it sounds like I can get rid of all of this and replace it with our ClientFactory. Then we call make, we pass the retailer, and then we call checkAvailability on the returned Client. And that'll give us our status.

client. And that'll give us our status. I don't know, let's see. Did that work? No, it didn't. Ah, string class right here. Import it. Run it. Ah, it still fails. We're getting an error through.

Ah, it still fails. We're getting an error through. Oh, there it is. Run it again. Fingers crossed. And that returns green as well. Okay, so now we have a designated class that figures out the appropriate client instance. And that's it. That's all a factory is. When I was a little bit younger in my learning, the idea of a factory sounded confusing to

That's all a factory is. When I was a little bit younger in my learning, the idea of a factory sounded confusing to me. It's not. A factory is just a class or method that creates an object. That's it. So let's come back to stock. Give this a reformat. Here's our current state. I think there's a couple other things we could do.

Here's our current state. I think there's a couple other things we could do. So if you want, we could treat this as a real-time facade. If you're not familiar with that, just search LayerCast for real-time facade, and that would allow us to interact with client factory as if it was a facade, even though it's not. So that gives us some testability benefits, and it simplifies the code a little bit. So fingers crossed. Did that work? Yes, it did. But next, I do see one thing here.

And as long as that implementation returns an instance of StockStatus, we then get the benefit of an object that knows if it's available and what the price is and whatever else we want to include. Finally, we set up a ClientFactory that accepts the retailer and then determines what the appropriate and relevant client is. So I think we've done enough for now. Everything is passing, so with that, I will see you in the next episode.

دوست دارید گاهی خبرهای Laracasts را ایمیل کنیم؟