Lesson objectives
At the end of this lesson, you will know…
- How to use Visual Studio, to help you clean up source code
- How to make your source code easier to read, which makes it easier to find bugs and make changes
There are thousands of different ways to write any program. Many of them will work as well as any other.
However, you need to remember that some other programmer may need to make changes to it in the future. And, that future programmer may be you (in six months, when you’ve forgotten exactly why you wrote your code the way you did).
In this lesson, I’ll show you some common techniques to make your code easier to understand – and how to use Visual Studio to help you do this.
None of the changes involve any new skill. They are only creating new properties, methods, and renaming a few things. However, I believe you’ll see a version of SuperAdventure that is much easier to work with.
NOTE: This refactoring changed 13 files. If you don’t want to make all these changes to your SuperAdventure program, I suggest that you still read through the lesson and compare your code with the refactored code here. This will give you an idea of how your future programs should look – with smaller functions, and improved names.
Video of me doing some of the refactoring steps
I’m using Visual Studio Community 2015 in this video. Older versions may not have the same refactoring tools, or they may work differently.
Use source control, when refactoring code
When you refactor your program, or make any large group of changes, it’s very helpful to use source control.
Before you make changes, check your code into your source code repository. After you make your changes, test your program. If it still works, check in the new version of your source code. If the changes don’t work, and you can’t fix them, you can “revert/rollback” your code to what it was before you started the changes.
If you are not familiar with source control, you can watch this video on Installing TortoiseSVN (Subversion) and VisualSVN for Visual Studio 2015 on Windows 10. These are the same source control tools I use at home, and at the office.
Step 1: At the top of many of our classes, we have several “using” statements. Some of them are added when Visual Studio creates the class file. However, we usually don’t need all of them. We will clean up our classes by removing those excess lines.
Start Visual Studio, open Player.cs, and look at the “using” statements. You should see that these two lines are light grey:
using System.Text; using System.Threading.Tasks;
They are light grey because the Player class does not use any classes from those namespaces. So, we can remove them.
If you look at most of the other classes, you’ll notice different “using” statements that are light grey, and can also be removed.
You could go through each file, and manually remove all these lines, but there is an easier way.
In Visual Studio’s menu, click on Edit -> IntelliSense -> Organize Usings -> Remove and Sort Usings. This will remove all the un-needed “using” lines, and sort the remaining ones in alphabetical order. You don’t need to do the sort, but I like to keep them organized.
After doing this, I click the “Save All” button in Visual Studio’s menu, and run the game, to make sure it still works.
It all worked, so I committed these changes to the source code repository. Remember to check in your changes after each successful refactoring change.
Step 2: Make your comparisons sound more like a natural language.
Sometimes, “if” conditions can be very complex. They might check several different values, and use complex combinations of “if”, “and”, and “not”. This can also happen when a comparison contains negatives – some value “does not” equals another value.
To make these easier to understand, you can change the variable, property, function, or series of comparisons to sound like how you would naturally speak.
For these situations, “wrap” the unnatural-sounding comparison behind a new function, or property, that has a clearer name.
For example, in the Player MoveTo() function, we have this line, to see if a location has a quest:
// Does the location have a quest if(newLocation.QuestAvailableHere != null)
The “if” statement doesn’t sound very natural when you read it aloud – at least, not to me.
So, I created a new property in the Location class that wraps that comparison. It does the exact same logic, but with a more natural name. Here’s the code for that function:
public bool HasAQuest { get { return QuestAvailableHere != null; } }
Now, we can change the code in MoveTo() to this:
if(newLocation.HasAQuest)
This line sounds much more natural, and easier to understand.
It’s so clear, I removed the comment from the line above it. This is what “self-documented code” means. The source code is so easy to read, and understand, that you need very few comments.
I made several more similar changes to SuperAdventure, creating new properties and functions, with names that sound more natural.
Step 3: “Inline” variables that are only used once, especially if they are used almost immediately after they are created.
Sometimes, we create variables that are only used once. They may serve the same purpose as the “wrapper” property we created in the previous step – to put a complex evaluation in an easy-to-understand location.
But, if we have well-named variables, properties, and functions, and we only use that variable in a few places, we can eliminate that variable (and that line of code) by placing its value “inline”.
On line 318 of the Player class, we have:
bool playerAlreadyHasQuest = HasThisQuest(newLocation.QuestAvailableHere);
We only use that variable in one place, three lines later, on line 321.
if(playerAlreadyHasQuest)
To eliminate the temporary variable, click on it and press Ctrl + . (the period/full stop key). This pulls up Visual Studio’s refactoring tool. Choose “Inline temporary variable”.
Visual Studio will show you where it will make the change, and you can click the “Apply” button to complete the change.
Another line of code is eliminated.
I did this for several more variables that were only used once (or a few times), and were used almost immediately after they were created.
NOTE: Sometimes, you may want to keep a single-use variable in your code, and not “inline” it. If you debug your program, you can set a breakpoint to see what happens when the variable is populated. That may be a little easier to follow than setting the breakpoint on the “if” statement. It’s one less thing happening on the line of code where you set the breakpoint.
Step 4: Give variables, properties, and functions more descriptive names.
If you have unclear names for properties, functions, or variables, and they don’t need to be “wrapped”, you can easily rename them in one place – and Visual Studio can update them every place they are used.
Left-click on a variable, property, or function name. It should have a light grey background.
Now, press the “F2” key. The background should turn green, and a box will pop up in the upper-right corner of the editing screen. Start typing, and Visual Studio will replace the name of your variable/property/function – every place it is used in your solution.
You can check the options in the pop-up box if you also want Visual Studio to change the name if it exists in comments or strings in your solution. If you check the “Preview Changes” box, Visual Studio will show you every place it will make the change, when you click the “Apply” button. In the preview, you can uncheck places where you don’t want the change made, before Visual Studio does the replacement.
Step 5: Break large functions into smaller functions that only do one thing.
The MoveTo function is large, does a lot of things, and difficult to understand.
When you see a function like this, look for the parts that have one purpose. Move those lines of code to their own function, and have the large function call that new, smaller function.
The smaller function will be easier to understand. It only has one purpose and has fewer lines and variables. Plus, the larger function will now have one line, where it used to have 15-30 lines. That will make the larger function easier to read.
When you see lines of code that you would like to move to a separate function, highlight them, click Ctrl + . (to bring up the refactoring menu), and click on “Extract Method”. Give the new method a name, and Visual Studio will create the function. It will also replace the lines you highlighted with a call to the new function.
Here is what the Player MoveTo function looks like, before and after all the refactoring changes.
BEFORE
public void MoveTo(Location newLocation) { //Does the location have any required items if(!HasRequiredItemToEnterThisLocation(newLocation)) { RaiseMessage("You must have a " + newLocation.ItemRequiredToEnter.Name + " to enter this location."); return; } // Update the player's current location CurrentLocation = newLocation; // Completely heal the player CurrentHitPoints = MaximumHitPoints; // Does the location have a quest? if(newLocation.QuestAvailableHere != null) { // See if the player already has the quest, and if they've completed it bool playerAlreadyHasQuest = HasThisQuest(newLocation.QuestAvailableHere); bool playerAlreadyCompletedQuest = CompletedThisQuest(newLocation.QuestAvailableHere); // See if the player already has the quest if(playerAlreadyHasQuest) { // If the player has not completed the quest yet if(!playerAlreadyCompletedQuest) { // See if the player has all the items needed to complete the quest bool playerHasAllItemsToCompleteQuest = HasAllQuestCompletionItems(newLocation.QuestAvailableHere); // The player has all items required to complete the quest if(playerHasAllItemsToCompleteQuest) { // Display message RaiseMessage(""); RaiseMessage("You complete the '" + newLocation.QuestAvailableHere.Name + "' quest."); // Remove quest items from inventory RemoveQuestCompletionItems(newLocation.QuestAvailableHere); // Give quest rewards RaiseMessage("You receive: "); RaiseMessage(newLocation.QuestAvailableHere.RewardExperiencePoints + " experience points"); RaiseMessage(newLocation.QuestAvailableHere.RewardGold + " gold"); RaiseMessage(newLocation.QuestAvailableHere.RewardItem.Name, true); AddExperiencePoints(newLocation.QuestAvailableHere.RewardExperiencePoints); Gold += newLocation.QuestAvailableHere.RewardGold; // Add the reward item to the player's inventory AddItemToInventory(newLocation.QuestAvailableHere.RewardItem); // Mark the quest as completed MarkQuestCompleted(newLocation.QuestAvailableHere); } } } else { // The player does not already have the quest // Display the messages RaiseMessage("You receive the " + newLocation.QuestAvailableHere.Name + " quest."); RaiseMessage(newLocation.QuestAvailableHere.Description); RaiseMessage("To complete it, return with:"); foreach(QuestCompletionItem qci in newLocation.QuestAvailableHere.QuestCompletionItems) { if(qci.Quantity == 1) { RaiseMessage(qci.Quantity + " " + qci.Details.Name); } else { RaiseMessage(qci.Quantity + " " + qci.Details.NamePlural); } } RaiseMessage(""); // Add the quest to the player's quest list Quests.Add(new PlayerQuest(newLocation.QuestAvailableHere)); } } // Does the location have a monster? if(newLocation.MonsterLivingHere != null) { RaiseMessage("You see a " + newLocation.MonsterLivingHere.Name); // Make a new monster, using the values from the standard monster in the World.Monster list Monster standardMonster = World.MonsterByID(newLocation.MonsterLivingHere.ID); _currentMonster = new Monster(standardMonster.ID, standardMonster.Name, standardMonster.MaximumDamage, standardMonster.RewardExperiencePoints, standardMonster.RewardGold, standardMonster.CurrentHitPoints, standardMonster.MaximumHitPoints); foreach(LootItem lootItem in standardMonster.LootTable) { _currentMonster.LootTable.Add(lootItem); } } else { _currentMonster = null; } }
AFTER
public void MoveTo(Location location) { if(PlayerDoesNotHaveTheRequiredItemToEnter(location)) { RaiseMessage("You must have a " + location.ItemRequiredToEnter.Name + " to enter this location."); return; } // The player can enter this location CurrentLocation = location; CompletelyHeal(); if(location.HasAQuest) { if(PlayerDoesNotHaveThisQuest(location.QuestAvailableHere)) { GiveQuestToPlayer(location.QuestAvailableHere); } else { if(PlayerHasNotCompleted(location.QuestAvailableHere) && PlayerHasAllQuestCompletionItemsFor(location.QuestAvailableHere)) { GivePlayerQuestRewards(location.QuestAvailableHere); } } } SetTheCurrentMonsterForTheCurrentLocation(location); }
The MoveTo function has gone from over 100 lines, to only 31 lines. The other lines are all in small functions, usually around 20 lines each.
If you want to change the SuperAdventure game – maybe add new features – it will be much easier to do with this smaller MoveTo function.
Step 6: Repeat, until all the code is easy to understand.
When you program, it’s usually a good idea to refactor often. After each change, look for anything you can clean up.
Think of it like changing the oil in a car. If you always drive (add code), and never change your oil (refactor), eventually, your engine (and program) will break down. Then, it will take much longer time to repair.
I’ve worked on programs that have been in development for years, with dozens of programmers making changes and additions. Many times, management will say that we don’t have time to clean up the code – we need to add in new features, “Now!”.
But, eventually, the code gets so bad that every change causes something new to break. When you keep your code clean, you reduce these problems.
Summary
When you create large programs, or programs that are modified long after they are written, it helps to have the code easy to read and understand.
I’ve worked on several projects that are millions of lines long, have been in development for years, and have had more than a dozen programmers work on them. When the code is not easy to understand, changes take longer, and are more likely to break something.
If you frequently do these refactoring techniques, your code will be much easier to work with.
Source code for this lesson
Next lesson: Lesson 25.1 – Select a random monster at a location
Previous lesson: Lesson 23.1 – Creating a console front-end for the game
All lessons: Learn C# by Building a Simple RPG Index
Hi Scott,
I’ve completed and read (and understand) this final lesson (but I didnt make the changes myself, I wanna become a better programmer by mastering the technical stuff first). It helped me out in getting some basic experience rather than just have a lot of theoretical knowledge in C#, so thats awesome.
Some questions that arise:
You made a static PlayerDataMapper class for reading/writing to SQL server. Why is this behaviour in a different class than Player? I understand if its to separate player logic from SQL logic, but then why isn’t the XML reading/saving part in a datamapper class as well?
In SuperAdventure we have a static World class – Should i have something similar for my movie project, a static class to contain and initialize all my components or would it not be required?
Would you mind in giving me some advice for my next project (the local/personal movie database in WPF)? I can’t really make a final decision on how the design should be (one of the things you wrote in the first lessons, about what objects there should be and what behaviour they should provide).
It seems to me it’s a much simpler application to write than your RPG with alot less components. Just a few objects: movie, datamapper (for SQL operations) and a form… Maybe some IMDb API functionality or web scraping. But I can use some techniques like events/delegates/databinding so I can get used to that more!
I’ve some questions about how to set it up and why to do it that way if you would not mind in helping me out a little.
Hi J,
I made the PlayerDataMapper class because someone asked to see an example of how to use a data mapper technique (also called a design pattern). The XML was similar to the active record design pattern (although, not exactly how you would implement it). Normally, I would choose one pattern, and use it every place in a program.
For your movie program, I probably wouldn’t use a static class like the World class. In SuperAdventure, I used the static World class as a replacement for a database. It’s our “data store”, the place where our data resides. Because SuperAdventure has very little data, we could use a static class. For more data, I’d start with a database instead – unless you use the IMDb API, and let their database become your data store. I’m guessing that your movie program will have features like: find a movie by the title, find a list of movies that an actor was in, find all actors that were in a specific movie, etc. Those would probably all be database (or web API) calls.
For you app, write a list of what you want it to do, and let me know. I’ll ask you a bunch of questions (so you can come up with the answers yourself), to help you.
Hi Scott.
Those are actually some really nice idea’s, I might just expand to implement those later. My own idea’s were actually based upon the “problems” I ran into while managing movies on my harddrive. Some movies had been on the drive for a few years and manually managing a really large list of movies (about 100+) is annoying. So one feature I’m gonna implement first is a personal rating system and delete all that is below a certain personal rating with one click, incase I wanna create free space on my harddisk. This ensures in the long run that only the movies that I personally liked, remain the longest.
So far I’ve come up with this:
LOCAL MOVIE DATABASE:
Scroll through a list in a GUI, showing movie titles that are on my hard-drive.
GUI should be populated from a database.
Must have button “more info” to show some extended data (rather than just the title).
There should be buttons to edit data, add new media (maybe after scanning media folder), delete data, etc.
I should be able to delete all media below a certain rating with one click besides deleting individual movie dirs.
Every button editing, removing, or adding any data, should make sure the database is updated as well (fire event so the engine will do the actual updating).
Change file attributes (just for practise).
Keep a log of what has changed and when and what the previous values were.
maybe a button to choose what the media directory is from the GUI and let it read the folder names inside there to determine what movies they are (like Plex does).
Objects:
Media directory or “Folder browser” object (not sure if this needs to be a class providing behaviour for reading/writing, deleting, updating, navigating or if the GUI should handle this?)
‘movie’ object:
Has: title, director, main actors, description, IMDb rating (should use imdb api or web scraping?), IMDb description, optional personal rating, optional personal comment, movie poster, information about whether the movie has been watched (bool), filepath and names of files inside that directory (for updating/deleting, etc).
datamapper (? –> for reading from/writing to database?) – Maybe combine this with folder browser object!
Besides this I just now realised that actors, directors etc, rather than just string properties in the movie object for their names, might be objects of their own, holding their data and possibly providing behaviour. Or is this overkill?
So what do you think of all this? Should I take a different approach or is this OK?
That looks like a good list of features.
I’d definitely start with building the Movie class, and getting the basics of the program working with it – using the file browser, reading from and writing to the database, etc. Than, after that works, I’d add in the next set of features (rating, or connecting to an API). Get each new feature working, before starting the next feature. If IMDb has a web API, I’d use that, instead of web scraping. Web scraping is usually difficult – the HTML may not be formatted properly/consistently, the format will probably change over time, and the website might prevent you from reading too many pages quickly.
Because a Movie has more than one Actor, an Actor can be in more than one Movie, and each movie could have a different number of actors, I’d use three tables to store that information. The Movie and Actor tables would each have an ID column, and their would be a third table that has two columns – MovieID and ActorID. So, the data might look like this:
Movie table
ID: 1 Name: Deadpool
ID: 2 Name: Serenity
Actor table
ID: 1 Name: Ryan Reynolds
ID: 2 Name: Morena Baccarin
ID: 3 Name: Nathan Fillion
ID: 4 Name: Gina Torres
MovieActor table
MovieID: 1 ActorID: 1 (for Ryan Reynolds, in Deadpool)
MovieID: 1 ActorID: 2 (for Morena Baccarin, in Deadpool)
MovieID: 2 ActorID: 2 (for Morena Baccarin, in Serenity)
MovieID: 2 ActorID: 3 (for Nathan Fillion, in Serenity)
MovieID: 2 ActorID: 4 (for Gina Torres, in Serenity)
That’s a common way of handling a many-to-many relation in a database.
Because I’ve made some personal modifications to the code as I follow your lessons, I’m going through the source for this lesson, finding the key differences, and implementing them myself as opposed to copy/paste.
I noticed that RemoveItemFromInventory() in Player.cs is different than the one introduced in Lesson 20.4.
It seems like the earlier one is superior as it will remove items even if the quantity goes negative (then correct for it), but perhaps I don’t understand everything going on.
What’s the thought process behind the change?
If we always use the RemoveItemFromInventory() function, we should ever have a time when Quantity is negative. The previous code is safer. But, it could also hide problems from us – problems created by us (the programmers) writing some of our code incorrectly/unsafely. Sometimes, you might want to your program to not catch errors that shouldn’t ever happen, so you can see problems in the source code. It would be ideal to have unit tests for this code, and have it test all possible conditions. However, you could definitely keep the negative quantity check, if you want it.
Hi Scott!
Looks like i missed a lesson when we added CurrentMonster insted of _currentMonster… :c
Hi there,
I have some weird issues with the Game. I had an issue with game yesterday, I was unable to quit it in convention way (by clicking on X in top right corner or ALT + F4), so I did that by force via task manager. When I clicked on start now, the window doesn’t appear, only diagnostic tools start working. What causing this problem and what can I do about it ?
In SuperAdventure.cs, did you delete (or comment out) these lines:
_player = PlayerDataMapper.CreateFromDatabase();
PlayerDataMapper.SaveToDatabase(_player);
You might also try using the debugger. You could set a breakpoint on the first line of the SuperAdventure constructor and the SuperAdventure_FormClosing() function. Then, use F10 and F11 to see the lines of code that run. This will let you see where the program stops working – which should be a good clue what the problem is.
Nope, I didn’t comment out or delete anything. I also rewrote all codes I have with those from GitHub, but the window doesn’t appear in any case. I’m using debugger most of the time, when I hit the Start button, right now I changed it to release, but it changed nothing.
I tried what you write and if I’m not mistaken, that lines cause the problem, it’s in Program.cs:
// Load the player
LoadGameData();
I could be wrong, but hope I’m not.. I never used any program to code something, I always wrote the raw code, so this Visual Studio makes me really hard times.
Go to the LoadGameData() function and delete (or comment out) the line:
_player = PlayerDataMapper.CreateFromDatabase();
In the SaveGameData() function, delete (or comment out) the line:
PlayerDataMapper.SaveToDatabase(_player);
Those are the lines that try to load and save the data in the database.
Hm, it didn’t make any change, but I have 0 errors, though. I tried a Console version and this one works just fine. So, it looks like the problem is not in the code, but in something I did with the Visual Studio itself. When I tried to fix the problem with vendor, the game window just froze and as I recently wrote, I close it via task manager. From that point the problem occured. I’m trying to recall what else I did, but this seems to be the only thing I did. Nothing else comes to my mind.
Can you upload your solution (including the directories under it, and all the files in those directories) to GitHub or Dropbox, so I can look at it?
Convenient: LINK REMOVED FOR PRIVACY
I hope it’s everything you did want..
I only see the SuperAdventure project there. Can you also upload the solution file (SuperAdventure.sln) and the project folder and files for the Engine project?
I hope this is it.. it’s whole folder with all files, project and solution: LINK REMOVED FOR PRIVACY
I found the problem. In the Windows Forms SuperAdventure project, the Program.cs class has the code for the SuperAdventureConsole Program.cs class.
Replace the Program.cs in the SuperAdventure project (only) with the code from here: https://gist.github.com/ScottLilly/bf55047c908b00728058d95c2e2d1381
Let me know if that doesn’t fix the problem.
Unfortunately no, it changed nothing..
Here is a copy of the complete solution that works when I run it: https://www.dropbox.com/sh/ran74ct4r7t3xki/AAAdQJK8DSJJQCCG65MKmO3La?dl=0
Hm, this solution works, the window pop up, thanks ! Is there an option to compare files each one to see difference and find where was the problem ?
You can use the WinMerge program at: http://winmerge.org/