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

Writing initial score test1:45

So I win. And that's it. That's all we need to know here. So let's get started. Let's add a test and keep it simple. Well, actually, I know we're going to use a data provider eventually, but let's refactor toward that. So let's say it scores 0 to 0. Okay. So if I have a new TennisMatch, we'll call it match, and nobody has scored a point yet,

Okay. So if I have a new tennis match, we'll call it match, and nobody has scored a point yet, but we check the score, well, it's basically 0 to 0, right? Now we refer to that as love, love. Okay. So let's say match. If I get the score, I expect that to equal, and we can represent this however you want. I'm just going to do lowercase with a dash. And this is what we get. All right.

And this is what we get. All right. We give it a run. Of course, it fails because there's no TennisMatch class. So we create it. All right. We switch back. We will import that. And clean up. Okay.

And clean up. Okay. And run it again. Now we've changed the error. There is no score method. All right. Run it again. Now we are returning null. So let's slime it for our first test. Yay, it passes.

Adding point scoring logic2:52

So let's slime it for our first test. Yay, it passes. All right. Let's do another one. How about it scores 1 to 0? So I'm basically reproducing the rules I told you. If it's 1 to 0, we call that 15 love. However, before I can do the score, we have to represent a point being earned, right? We have to say playerOne earns 1 point. So I have an idea of what we will ultimately do, but at least to start, we might say point

We have to say playerOne earns one point. So I have an Idea of what we will ultimately do, but at least to start, we might say point to playerOne, award a point to playerOne. Yeah. And I kind of know we're going to change this, but this will at least get us rolling. So I give it a run. And of course it fails. And a little tip in PHPStorm, I can option enter on the method and add it directly to the class under test.

the class under test. Okay. So now I run it again and it fails. Okay. So it sounds like we need to track how many points each player has. So I might say playerOnePoints starts at 0 and playerTwoPoints starts at 0. Then if we award a point to playerOne, all we do is increment it. Okay. So if we run it, of course it's still going to fail because we're not taking those points into account.

So if we run it, of course it's still going to fail because we're not taking those points into account. So again, the easiest thing I can write here is if playerOne's points are greater than playerTwo's points, then we know this isn't quite right, but it'll make the test pass. And we're trying to get in that flow of the smallest amount of code to make the test pass. So if I run it now, it passes. Okay. Let's go back. Let's do another one. And then maybe we will switch to data providers.

Refactoring tests with data provider4:51

So again, you know, if we want to be a little silly here, if playerOne's points are greater than playerTwo's points by more than one, at that point we do 30 love and now everything passes. Okay. So we know we're going to change this, but before we do, I want to refactor my tests to use a data provider because already I'm seeing so much duplication. We can make this easier. So I will add a method here. This will be our dataProvider, and it's going to return an array of arrays. So for example, for each test, I need to know the points playerOne gets, the points playerTwo gets.

This will be our data provider, and it's going to return an array of arrays. So for example, for each test, I need to know the points playerOne gets, the points playerTwo gets, and then finally what we expect the score to be. Something like this. All right. So now let's use that. We'll do this original method. We'll say the data provider is scores, and then that will accept playerOne points, playerTwo points, and then finally the score. All right.

two points, and then finally the score. All right. So let's say we need to rewrite this. It scores a tennis match. We instantiate the Score class, and then ultimately we expect the score here, but we have to award those points. So we know at some point we have to call this method a certain number of times for playerOne and a certain number of times for playerTwo. So why don't we just say we could do range or simple for i. So for i equals zero, i is less than the playerOnePoints, i plus plus, and we will call

So why don't we just say we could do range or simple for i. So for i equals zero, i is less than the playerOnePoints, i plus plus, and we will call a pointToPlayerOne. So if we say I expect three points for playerOne, then three times we will call this method and then assert against the score. So have a look here. If we give it a run, it passes. Okay. So now let's do that next one. If we have one to zero, it is 15, and if I give that a run, it passes as well.

Mapping points to terms7:20

Everything is green. I know this should be better, so I will take a moment to refactor. It sounds like we need a term for a point value. So we've learned that zero is love, one is 15, two is 30, three is 40, right? So that sounds like a lookup table to me. All right. Let's try it out. Let's add a method here, and we'll call it termLookup. Maybe there's an official tennis term for what that's called. This is when it's advantageous.

So it will work, but I think I'm going to do a switch instead. So let's do a switch on the, well, we have to accept some points, right? So let's do a switch on the points there, and then we'll just say, well, if the case is zero, then we return love, and then I'll repeat this, one, two, three, right? So if the case is one, we return 15, if the case is two, we return 30, and if it's three, we return 40. All right. So now we have a simple lookup table, and if we need to later, we'll add a default. So if I switch back, think about it. If I comment all of this out, I could just format a string.

So if I switch back, think about it. If I comment all of this out, I could just format a string. Let's do sprintf. So the format will be the term for playerOnePoints, a dash, and a term for playerTwoPoints, and then we'll just say player, actually, no, we'll need to call pointsToTerm, right? Yeah, I don't like that method. That's not good. We'll keep thinking of something better, and then we'll send through playerOnePoints, and then do the same thing for playerTwoPoints, right? So now imagine the score is two to zero, all right?

So all we have to do is duplicate this and then say, point to playerTwo. We run it. Now it fails because there is no method. So I will add that method, and I'm going to move it up, and all we're going to do is the same thing here, but in favor of playerTwo. And we give it a run, and now everything is passing. Let's keep going. So what if playerOne scores four points? We know that is winner for playerOne. So again, we can represent this however we want.

Implementing winner detection10:56

We know that isWinner for playerOne. So again, we can represent this however we want. I'm just going to say winner for playerOne, and I'll give that a run. Of course it fails because we haven't represented that. So I switch back to our class, and yeah, it sounds like right up here, check if we have a winner. That's kind of what we need, otherwise provide a default. That's kind of what's going on here. Okay. So how would I check if we have a winner?

Okay. So how would I check if we have a winner? Well, this is where we go back to our rules. A game is won if you have four points and two points more than your opponent. All right. So let's represent that in code. We'll start with playerOne. If playerOne's points is greater than 3, and playerOne's points is greater than playerTwo's points plus 2, and actually I think that should be greater than or equal to, we'll go over it, then we should have a winner.

So I'm going to leave this comment here, but remember what we've talked about before. This is a dead ringer that some refactor is in order, and that's because this conditional does not describe what we are checking, and that's why you added a comment. So in these cases, we've learned, change the code to make the comment redundant. We'll come back to that later, though. We're going to do another one. Let's say playerOne has zero points, and playerTwo has four. That would be winner playerTwo. Now if we run that, it fails. We switch back, and I'm just going to duplicate this.

Now if we run that, it fails. We switch back, and I'm just going to duplicate this. If playerTwoPoints is greater than three, and playerTwoPoints, it's basically the inverse of what we had earlier. Then we have a winner for playerTwo. Run the code, and now that works as well. All right. So now that we have our winning condition in order, yeah, we can do our refactor. So I'm going to select everything here, and extract a method, and we'll say, what are we checking for?

So I'm going to select everything here, and extract a method, and we'll say, what are we checking for? Do we have a winner? How about hasWinner? All right. Rerun the code. It still passes. Let's have a look at that method. That will now check what we had originally, but this isn't quite right. This method checks if playerOne is a winner, right?

That will now check what we had originally, but this isn't quite right. This method checks if playerOne is a winner, right? But that's not really what I care about. I want to check if we have a winner for playerOne or playerTwo. So this is where we kind of have to reproduce what we had before. So if that's the case, return true. Let's come back and grab that one. Don't worry. We'll keep refactoring, though. If playerTwo has met the requirements, then return true.

We'll keep refactoring, though. If playerTwo has met the requirements, then return true. Otherwise, we don't have a winner yet. So now take a look at this. If I run the code, it's failing because we've lost that check for playerTwo. However, we've replaced it with this call. So now it sounds like we just need to make this return string dynamic. We check the leader and either return playerOne or playerTwo. So if we wanted to write it out, we could say if playerOne's points are greater than playerTwo's points, that means playerOne is the winner at this point.

So if we wanted to write it out, we could say if playerOne's points are greater than playerTwo's points, that means playerOne is the winner at this point. So winner, playerOne. Otherwise, winner, playerTwo. All right. Let's see if that works. Run the code, and yet everything is passing. So I can clean this up further, but I want you to note that this refactor here has now made the comment redundant. So I can remove that entirely, and that's a decent refactor.

made the comment redundant. So I can remove that entirely, and that's a decent refactor. Okay. But now here, I don't like that. I have to parse it a little bit. I really just want to say return winner : and then whoever the leader is. That's kind of what I want. Okay. Let's grab that. I will extract the method, and I'm just going to call it leader.

Let's grab that. I will extract the method, and I'm just going to call it the leader. That's what we're checking there. But if I scroll down, we'll have to tweak it a little bit. So I don't want a string. I want the player. So I think we're going to have this concept of player objects. We'll create a Player class at the end, but until then, I'm just going to do this. We'll just hard code a string to represent player one or player two, and if we rerun it, it works.

We'll just hard code a string to represent playerOne or playerTwo, and if we rerun it, it works. Okay. So now this is what our score method looks like. Let's see what else we need. Let's go back to our Scratchpad. We've handled the case of a winner. We've handled the basic terminology. Not this one, though. At least three points have been scored, and the scores are equal.

All right. We have our next step. So let's go back to our score method, and it sounds like we check for a winner. Then we will probably check for advantage, right? We haven't gotten there yet. And then we will check for deuce. So check for deuce. Okay. The rules are, again, if each player has three points, so if playerOne's points are greater than or equal to three, and playerTwo...

But I find, when I'm trying to get the test to pass, don't try to impress yourself. Just write what it takes to make the test pass, and then you can refactor. So in our case, if both players have at least three points, and they are tied, okay, well, if the points are identical, it sounds like this is deuce. So return deuce. Run the code, and it passes. Okay, but now, yet again, this doesn't signal that we're checking for deuce, and that's why we have a comment. So let's make it redundant. I extract the method.

This is just a simple explanation with no code.

Run the code, and that's still passing. Okay, so, yeah, let's see if we want to tweak this a little bit, if only for fun. This first check, what are we doing here? Well, like I told you earlier, that is like the threshold for whether we can find a winner, right? That is the threshold where now we need a winner by two, right? So if the score is three to one, you're winning by two, but there is no winner because you haven't yet reached that threshold. But once they're tied, then whoever scores the next two points in a row wins the game. So what if I refactored this?

Has reached, threshold, I don't like that either. Can be one. I don't know. I'm sure you have a better idea, but I'm going to stick with that for now. So I rerun the test and it still passes. Okay, so I'm going to leave this variable here, but I think we may need to reach for it more later. We'll come back to it. So let's go to our scratch pad. We've handled this case, we've handled this case, we've handled this case.

Handling deuce and advantage19:55

So let's go to our scratch pad. We've handled this case, we've handled this case, we've handled this case. The final step is advantage. So once we are at deuce, if a player scores a point, we would say advantage playerOne or advantage playerTwo. We need to represent that in the code. So let's say right here, if we have four to three, that means advantage playerOne. I run the code and it fails. Okay, let's make that work. So I go back to my score method and we're going to do our check here.

Okay, let's make that work. So I go back to my score method and we're going to do our check here. Check for advantage. So if, let's just write it out, if playerOne's points are greater than or equal to 3 in playerTwo. So we're kind of checking that that has reached threshold idea again, aren't we? If playerTwo's points are at least 3, and let's do playerOne first. playerOne's points is greater than playerTwo. That sounds like we have an advantage for playerOne. So I will return advantagePlayerOne.

Just whoever the leader is, advantage to that person. Okay. Let's add that method. Right down here, I usually do protected, and I'm going to paste all of that in, but now I'm not going to return a string. Yet again, I will return true. Otherwise, return false. So if I rerun the code, that passes, but again, this is looking pretty gross to me. Now, the first thing I want you to notice is this. If playerOne's points are greater than 3, and the same for playerTwo.

Now, the first thing I want you to notice is this. If playerOne's points are greater than 3, and the same for playerTwo. So let's extract a little variable there called canBeOne, hint, hint, and then notice it's the exact same thing here. Rerun the code. It still works. But now notice we've introduced duplication. So this is where I think duplication is very handy. Not to keep it, but as a signal for your next refactor. So often when you're working on these projects, you want to achieve duplication.

Not to keep it, but as a signal for your next refactor. So often when you're working on these projects, you want to achieve duplication. You want to refactor the code until you've duplicated things, because then it's clear, all right, I extract that to a method and then I can remove the duplication. It's like a little signal for what your next step should be. So let's make this a method now. Can be one. Again, I don't like that name, but it's okay for now. All right. So if I scroll down, there's the method there, but let's inline that like so.

All right. So if I scroll down, there's the method there, but let's inline that like so. Okay. Rerun the code. It still works, which means we can update this one as well. And that works. Finally, I don't need a variable anymore, so I can inline it, run it. It still works. So you see how this is useful, but let's keep going. This still seems like the same code, right?

So you see how this is useful, but let's keep going. This still seems like the same code, right? If it can be one, or how about this? If it can't be one, then immediately return false. There's no need to continue. And if we make that refactor, I can remove both these checks here, and that still works. Cool. Next, what are we checking here? We're checking if playerOne is winning or playerTwo is winning. However, we don't care who's winning in this case.

We're checking if playerOne is winning or playerTwo is winning. However, we don't care who's winning in this case. This method is simply checking, have we reached that threshold? And if so, is one player beating the other? So I'll show you this in a few steps. What if we started like this? If playerOne is beating playerTwo, or playerTwo is beating playerOne, then somebody has the advantage. Remove that, run it, it still works. But next, we have a conditional that returns true, otherwise false, which means we can

Remove that, run it, it still works. But next, we have a conditional that returns true, otherwise false, which means we can just return the result of that conditional, and I can remove that. Run it, and that works as well. But we're still not done. Think about it. At this point, we're just checking if they're not tied, or they're not in deuce. So try this. How about we return playerOne’s points does not equal playerTwo’s points. Run the code, and that's a good refactor too.

How about we return playerOnePoints does not equal playerTwoPoints. Run the code, and that's a good refactor too. But we're not done. I said, if they're not in deuce, so let's return notIsDeuce. Run it, and that works as well. You see how cool this is? One refactor leads to another. One extraction leads to another. One final thing here. We have, yet again, a variable that's referenced only one time.

One final thing here. We have, yet again, a variable that's referenced only one time. In these cases, unless it makes the code much more readable, I will always inline it. If it can't be one, return false, otherwise return if we're not in deuce, and that works. And I think that's fine. The only thing is, I sometimes don't like using the negatives so much. It's a little harder for me to think. Let's say if it can be one, let's see, do we like this better? That would work. We're just changing the order.

That means I need to switch back to my test, and we'll pass it in here. So yeah, again, I'm going to use strings for now, but I might create a Player class. So that's still going to work, because we're not really using it. However, why don't we change this to John versus Jane? And now, if I scroll down, we can say advantage to John, and then advantage to Jane. So I think that will now fail, yeah, because we have to update the code. So let's find playerOne. There we go. That will be not John. I'm not going to hard code it.

Introducing Player objects27:47

Next, this feels a little weird. I wouldn't mind instead having a method called, it seems like when you watch a tennis match, they'll say point to so-and-so. So why don't we say pointTo, and then we accept a player. Okay. So with that in mind, what if instead we start thinking in terms of a Player object? So notice I'm not writing any more tests, and that's okay, but I am introducing a new class. And that's important. One test or one test class can encompass multiple classes in certain cases.

And that's important. One test or one test class can encompass multiple classes in certain cases. So yeah, if we did that, then we could even let player store their own points. So something like that could be interesting. Hmm. Let's try it out. So I'm going to go back to my test and we'll say right up here, create a tennisMatch between two players, John and Jane. Then for John's points, we'll say, we'll do this, John, Jane. Often in my tests, I just make it crystal clear.

Point to Jane down here. Okay. So if I run the code, it's all going to fail because there is no Player class. So let's create the class. I'll return. The namespace is app. All right. It's kind of weird formatting there, PHPStorm. Get rid of that. And then finally, we need to accept the name of the player.

Get rid of that. And then finally, we need to accept the name of the player. So I will initialize that, and let's make it public. All right. Run the code. Unexpected public. Whoops. Run it again. Okay. So now, it's trying to call a score method on player.

Okay. So now, it's trying to call a score method on player. So player, we've decided we'll store their points, and if we call a score method, we'll just say points plus plus. Run the code again, and now it is failing, and that's okay. That's because, let's take a look. So now, when we say points to a player, we say player score, but we never read from that. We're still looking at these playerOne points, playerTwo. Okay. So, I'm going to get rid of that.

Okay. So, I'm going to get rid of that. Next, these will be player instances, so I will update that, and then these as well, or you can make it all dynamic if you want. I won't judge it either way. So if we keep going, yeah, we have to update all these references to playerOnePoints. Let's say, find every reference to playerOnePoints, and we'll change it to playerOne, and then the property on that object, like so. Then let's do the same thing for every reference to playerTwo. playerTwoPoints.

Then let's do the same thing for every reference to playerTwo. playerTwo points. All right. Fingers crossed, it's still failing. Return value of leader must be a string. Yeah, but now, an object is being returned. So if we go to that leader method, we're figuring out who is winning, and we return an object, but we are echoing it as if it was a string, as you see here. So it sounds like we need to say the leader's name. Run the code.

So it sounds like we need to say the leader's name. Run the code. We're getting close. Return value. So do we have another reference to leader? No. Hmm. Return value of leader must be of type string. Oh, of course. It's returning a player now.

Oh, of course. It's returning a player now. Run the code, and now it's passing. All right. So now, I think that's a decent refactor. A TennisMatch is played between two players. So we represent that. If we score it, we check, well, do we have a winner? If so, return this formatted string. Winner to the leader's name.

If so, return this formatted string. Winner to the leader's name. If we have an advantage, return advantage to the leader's name. If we have a deuce, return deuce. Otherwise, format the points. And you know what? We could even experiment. I'm not going to do this, but we could maybe even experiment with what if the Player class had a translatePoint method. Actually, let's try it.

had a method to translate their point. Actually, let's try it. What if the Player class had a method to translate the number of points to the term? So for example, to term. Then, I haven't tried this, but let's try it out. We would take that, bring it in. So now the lookup is being stored here, but we do a switch on the current player's points. What do you think of that? So now, let's see. If I remove all of this, our tests are going to fail, because we no longer have that method

So now, let's see. If I remove all of this, our tests are going to fail, because we no longer have that method pointsTo term. Let's seek it out. Okay. So now, we were calling a method and then passing in the playerOne's points, but let's just say this playerOne to term, and then playerTwo to term. So we're letting that player be responsible for whatever that term is. Run the code, and that passes as well. So again, you can tinker around with this and decide, should it be the player's responsibility?

Run the code, and that passes as well. So again, you can tinker around with this and decide, should it be the player's responsibility? Well, they're already tracking their points, so it's not crazy that there would be a method that converts the points to the proper tennis term. These are all things to think about. Anyways, next, you'll notice that we no longer call this method or this method, because we cleaned it up to this. Next, everything's still passing? Yeah. Point two, I think it's fine, but we might also consider in our test and in our API,

Further refactoring win logic33:48

So like we did earlier, we could refactor that. If playerOne has more than three points and they are winning by two, or playerTwo has more than three and is winning by two, then return true. So that will still work. This is kind of what we did earlier. We can also replace that just with a return there directly, and that works as well. And then finally, we might split this up. Check this out. What if we said max here? So let's look at playerOne's points and playerTwo's points.

What if we said max here? So let's look at playerOne's points and playerTwo's points. And let's say if the highest value in this array is less than 4, then you haven't reached the threshold to win the game. So we could immediately return false. So if we did that, I think I can remove this check and this check. Run the code. Ooh, max when only one parameter is given. It must be an array. Why did I not do that?

So it fails. So here's what we can do, though. Let's grab the absolute value there, and that will turn -2 into 2. So it will work in both cases, and I think if I give it a run, yeah, it still works. So that's kind of the fun refactor there. The only remaining step, yeah, the only thing that bugs me is this canBeWon. So I think that's confusing. We've talked about this a little. That's confusing because I have a hasWinner. That checks if somebody won the game, but then canBeWon.

That's confusing because I have a hasWinner. That checks if somebody won the game, but then can be winner. It is not clear what that means, and that's, I think, the final refactor that needs to take place. So you know what? Here's what we're going to do. For the first time, I'm going to leave this to you, and we can work together on it in the comments. I think the class looks pretty good other than that method. So what refactor, if you're working along, would you perform to make this clear?

I think the class looks pretty good other than that method. So what refactor, if you're working along, would you perform to make this clear? Because it sounds like we have too many checks. We have a check here, and we also have a check here that are pretty similar. So I'm going to leave the final piece of the puzzle to you, and if you'd like to see what I came up with, you can check the source code in the description below the video.

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