├── .devcontainer └── devcontainer.json ├── .github └── dependabot.yml ├── .gitignore ├── .vscode └── settings.json ├── README.md ├── exercise.md ├── pom.xml └── src ├── main └── java │ └── nerdschool │ └── bar │ └── Pub.java └── test └── java └── nerdschool └── bar └── PubPricesTest.java /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the 2 | // README at: https://github.com/devcontainers/templates/tree/main/src/java 3 | { 4 | "name": "Java", 5 | // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile 6 | "image": "mcr.microsoft.com/devcontainers/java:1-21-bullseye", 7 | 8 | "features": { 9 | "ghcr.io/devcontainers/features/java:1": { 10 | "version": "none", 11 | "installMaven": "true", 12 | "installGradle": "false" 13 | } 14 | } 15 | 16 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 17 | // "forwardPorts": [], 18 | 19 | // Use 'postCreateCommand' to run commands after the container is created. 20 | // "postCreateCommand": "java -version", 21 | 22 | // Configure tool-specific properties. 23 | // "customizations": {}, 24 | 25 | // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. 26 | // "remoteUser": "root" 27 | } 28 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for more information: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | # https://containers.dev/guide/dependabot 6 | 7 | version: 2 8 | updates: 9 | - package-ecosystem: "devcontainers" 10 | directory: "/" 11 | schedule: 12 | interval: weekly 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .idea 3 | out 4 | target 5 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "java.configuration.updateBuildConfiguration": "automatic" 3 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Clean code and refactoring 2 | 3 | Somewhere along the way code goes from good to bad. It's usually a combination of many small factors that when allowed 4 | to take hold in your project, makes it hard to work with and downright frustrating. Your code starts to smell... bad... 5 | 6 | 7 | ## [Open presentation slides](https://docs.google.com/presentation/d/19rQAnOHE2CcQ-hGvmDVPpvsr5wjYGjp6WVM8caHFLF0/edit?usp=sharing) 8 | 9 | ## Focus of this lesson 10 | 11 | In this workshop you'll work together in a team. You'll get to know some of the most common code smells and how to fix them. After this workshop you'll know the basics of 12 | clean code and how to refactor your smelly code with test coverage. 13 | 14 | ## What you need 15 | 16 | - IntelliJ Community Edition: [IntelliJ Community Edition](https://www.jetbrains.com/idea/download/) 17 | - Java dev kit: [Java SE Development Kit](https://www.oracle.com/java/technologies/downloads/) 18 | 19 | ## How to get started 20 | 21 | * Start by cloning this repository into a folder on your computer. If you've never used git before, you can alternatively use the the "Download ZIP" button to the right. 22 | * Although you have this `README.md` file on your computer it's easier to read it on GitHub, so we recommend you keep this page open with the exercise tasks. 23 | 24 | ## Exercise 25 | 26 | Go to [Exercise - Fix the smelly code](exercise.md) to get started. 27 | 28 | 29 | ## Helpful resources 30 | 31 | - [IntelliJ User interface](https://www.jetbrains.com/help/idea/guided-tour-around-the-user-interface.html) 32 | - [IntelliJ interactive training course](https://www.jetbrains.com/help/idea/feature-trainer.html#start-a-lesson-from-the-welcome-screen) 33 | - [IntelliJ for Eclipse users](https://www.jetbrains.com/help/idea/migrating-from-eclipse-to-intellij-idea.html) 34 | - [IntelliJ testing](https://www.jetbrains.com/help/idea/tests-in-ide.html) 35 | 36 | 37 | ## Shortlist of code smells and ways to fix them 38 | 39 | * Duplicated code: 40 | Copy/paste is dangerous! Can be that different parts of the code do the same thing. Or different algorithms that give same result. 41 | 42 | Collect into a single place, and adhere to the Single Responsibility Principle. 43 | 44 | * Extract method. 45 | * Extract class. 46 | * Pull up method. 47 | * Form template method. 48 | 49 | * Long method: 50 | As functionality grows over time, so do methods. How do we know it's too long? 51 | Line count is one measure, number of execution paths is a better measure. 52 | 53 | Split into smaller methods. Make sure each does only one thing. Split into several classes, make sure each class has a Single Responsibility. 54 | 55 | * Extract method. 56 | * Replace temp with query. 57 | * Replace method with method object. 58 | * Decompose conditional. 59 | 60 | * Long parameter list: 61 | * Replace parameter with method. 62 | * Introduce parameter object. 63 | * Preserve whole object. 64 | 65 | * Large class/divergent change/data clumps: 66 | Data clumps is a specific type of duplication where the same or similar group of fields can be spotted in different classes. 67 | 68 | Extract the clump into a class with the methods from the different classes. 69 | 70 | * Extract class. 71 | * Extract subclass. 72 | * Extract interface. 73 | * Replace data value with object. 74 | * Introduce parameter object. 75 | * Preserve whole object. 76 | 77 | * Data class: 78 | All fields and no functionality makes the object a dull quiet thing. 79 | 80 | Look around the code for functionality that naturally belongs with the fields and move it into the class. 81 | 82 | * Move method. 83 | * Encapsulate field. 84 | * Encapsulate collection. 85 | 86 | * Shotgun surgery: 87 | Changing one small thing idea-wise ends up changing lots of similar changes all over the code. 88 | 89 | Chances are, you are missing an object. Introduce one, so that changes to the idea can be expressed in a single place. 90 | 91 | * Move method. 92 | * Move field. 93 | 94 | * Feature envy/inappropriate intimacy: 95 | A method is doing operations entirely on an object or values outside the current class. 96 | Classes are reaching into each other for values. 97 | 98 | The functionality is in the wrong place Move it to the object with the values it wants to be with. You may need to move 99 | it to a new class, or merge classes. 100 | 101 | * Move method. 102 | * Move field. 103 | * Extract method. 104 | 105 | * Primitive obsession: 106 | 107 | A programmer who thinks it’s too much overhead to use an object for just a few simple values. This is what compilers are for. 108 | 109 | * Replace data value with object. 110 | * Replace type code with class. 111 | 112 | * Switch statements: 113 | Switching on an object property to do different things often means that property has meaning, not just a simple value. 114 | 115 | Consider replacing switch statement with polymorphism – make each value of the switching property determine a new subclass. 116 | 117 | * Replace conditionals with polymorphism. 118 | * Replace type code with subclasses. 119 | * Replace parameter with explicit methods. 120 | * Introduce null object. 121 | 122 | * Speculative generality: 123 | The creation of a solution that will solve that whole class of problems, and all their varieties. 124 | 125 | You Ain't Gonna Need It. Take it out. Simple code is always better. 126 | 127 | * Collapse hierarchy. 128 | * Remove parameter. 129 | * Rename method. 130 | * Inline class. 131 | 132 | * Temporary field: 133 | * Extract class. 134 | * Introduce null object. 135 | 136 | * Comments: 137 | Comments lie. Make your code expressive enough to tell the truth instead by paying attention to good naming. 138 | 139 | * Extract method. 140 | * Introduce assertion. 141 | 142 | ### Examples of how to employ common methods: 143 | ######(google is your friend) 144 | 145 | * Pull up method: 146 | - Pull a method up into a superclass. 147 | 148 | * Form template method: 149 | - Generalise methods so that the constituents are the same, then pull up method. 150 | - Have the specialised parts in subclass methods that are abstract in the super class. 151 | 152 | * Replace conditional with polymorphism: 153 | - Move each "leg" of the conditional into an overriding method in a subclass, and make the original method abstract. 154 | 155 | * Introduce null object: 156 | - Rather than returning null, return an object (fex a subclass) representing the null condition (i.e. base case). 157 | 158 | * Encapsulate field: 159 | - Make a field private and provide getter/setter. 160 | 161 | * Encapsulate collection: 162 | - Rather than get/set for a collection, provide get, add and remove methods. 163 | 164 | * Collapse hierarchy: 165 | - Remove unneeded classes. 166 | 167 | * Replace temp with query: 168 | - Extract expression into method. 169 | 170 | * Decompose conditional: 171 | - Extract method for condition (if part), then part and else parts. 172 | 173 | * Replace parameter with method: 174 | - Remove parameter and let the receiver invoke the method. 175 | 176 | * Replace type code with subclasses: 177 | - Replace i.e. constants with sub classes. 178 | -------------------------------------------------------------------------------- /exercise.md: -------------------------------------------------------------------------------- 1 | # Fix the smelly code 2 | 3 | Form groups of about 3 people and review Pub.java in nerdschool.bar. Which code smells can you find? Refactor to remove smell(s). There is a small test suite (nerdschool.bar.PubPricesTest.java) 4 | that will help you along the way. Introduce more tests if you need them. 5 | 6 | 7 | You will learn to: 8 | 9 | - Identify bad code 10 | - Fix code smells 11 | - Refactor with tests 12 | 13 | ## Required software and tools for this exercise 14 | 15 | - IntelliJ Community Edition: [IntelliJ Community Edition](https://www.jetbrains.com/idea/download/) 16 | - Java dev kit: [Java SE Development Kit](https://www.oracle.com/java/technologies/downloads/) 17 | 18 | :question: Not familiar with IntelliJ? Take a look at the [User interface](https://www.jetbrains.com/help/idea/guided-tour-around-the-user-interface.html) docs and the [interactive training course inside IntelliJ](https://www.jetbrains.com/help/idea/feature-trainer.html#start-a-lesson-from-the-welcome-screen). 19 | 20 | ## Details 21 | 22 | :pencil2: Open `pom.xml` in IntelliJ by selecting *Open* from the Welcome Screen. You can also use File --> Open --> and then select the `pom.xml`. (See [Open an existing Maven project](https://www.jetbrains.com/help/idea/maven-support.html#maven_import_project_start) in the IntelliJ docs for more info about how to open projects.) 23 | 24 | :pencil2: Select **Open as Project**. 25 | 26 | :pencil2: Run all the tests in the `PubPricesTest` class and verify that they are all green. (See [Run tests](https://www.jetbrains.com/help/idea/performing-tests.html) in the IntelliJ docs for more info about how to run tests.) 27 | 28 | :pencil2: Examine the tests and the code in the `Pub` class. 29 | 30 | :question: What does this code do? Before you start changing the code, understand what the code does. 31 | 32 | :question: What is wrong with the code? There are naming issues, magical numbers, bad structure and more. 33 | 34 | The main part of this exercise is to refactor the code. You'll get to do much of this on your own, but here are some fairly small tasks to start with: 35 | 36 | :pencil2: Fix all the magical numbers. 37 | 38 | :pencil2: Rename existing functions so their names reflect what they do. 39 | 40 | :pencil2: Remove bad comments. 41 | 42 | :pencil2: Make functions where appropriate if there are clear bits of the code that are independent of the rest. 43 | 44 | From now on you're on your own. There are many ways to refactor this code and not necessarily one correct one. It's useful to discuss whether you see any code that should be separated into and what each class should do. 45 | 46 | - Can any of the code structures you've learned about make this code easier to read and easier to maintain? 47 | - If you add more drinks to the menu, how can you make the code support this in the best way possible? 48 | 49 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | 5 | nerdschool 6 | pub 7 | 1.0-SNAPSHOT 8 | jar 9 | 10 | uglycode 11 | http://maven.apache.org 12 | 13 | 14 | UTF-8 15 | 17 16 | 17 | 18 | 19 | 20 | 21 | org.junit.jupiter 22 | junit-jupiter-engine 23 | 5.10.2 24 | 25 | 26 | org.junit.jupiter 27 | junit-jupiter-api 28 | 5.10.2 29 | 30 | 31 | org.apache.maven.plugins 32 | maven-compiler-plugin 33 | 3.12.1 34 | 35 | 36 | 37 | ${project.artifactId} 38 | 39 | 40 | org.apache.maven.plugins 41 | maven-compiler-plugin 42 | 3.12.1 43 | 44 | UTF-8 45 | -Xlint:unchecked 46 | 47 | 48 | 49 | org.apache.maven.plugins 50 | maven-surefire-plugin 51 | 3.2.5 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /src/main/java/nerdschool/bar/Pub.java: -------------------------------------------------------------------------------- 1 | package nerdschool.bar; 2 | 3 | public class Pub { 4 | public static final String ONE_BEER = "hansa"; 5 | public static final String ONE_CIDER = "grans"; 6 | public static final String A_PROPER_CIDER = "strongbow"; 7 | public static final String GT = "gt"; 8 | public static final String BACARDI_SPECIAL = "bacardi_special"; 9 | 10 | public int computeCost(String drink, boolean student, int amount) { 11 | 12 | if (amount > 2 && (drink == GT || drink == BACARDI_SPECIAL)) { 13 | throw new RuntimeException("Too many drinks, max 2."); 14 | } 15 | int price; 16 | if (drink.equals(ONE_BEER)) { 17 | price = 74; 18 | } 19 | else if (drink.equals(ONE_CIDER)) { 20 | price = 103; 21 | } 22 | else if (drink.equals(A_PROPER_CIDER)) price = 110; 23 | else if (drink.equals(GT)) { 24 | price = ingredient6() + ingredient5() + ingredient4(); 25 | } 26 | else if (drink.equals(BACARDI_SPECIAL)) { 27 | price = ingredient6()/2 + ingredient1() + ingredient2() + ingredient3(); 28 | } 29 | else { 30 | throw new RuntimeException("No such drink exists"); 31 | } 32 | if (student && (drink == ONE_CIDER || drink == ONE_BEER || drink == A_PROPER_CIDER)) { 33 | price = price - price/10; 34 | } 35 | return price*amount; 36 | } 37 | 38 | //one unit of rum 39 | private int ingredient1() { 40 | return 65; 41 | } 42 | 43 | //one unit of grenadine 44 | private int ingredient2() { 45 | return 10; 46 | } 47 | 48 | //one unit of lime juice 49 | private int ingredient3() { 50 | return 10; 51 | } 52 | 53 | //one unit of green stuff 54 | private int ingredient4() { 55 | return 10; 56 | } 57 | 58 | //one unit of tonic water 59 | private int ingredient5() { 60 | return 20; 61 | } 62 | 63 | //one unit of gin 64 | private int ingredient6() { 65 | return 85; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/test/java/nerdschool/bar/PubPricesTest.java: -------------------------------------------------------------------------------- 1 | package nerdschool.bar; 2 | 3 | import org.junit.jupiter.api.BeforeEach; 4 | import org.junit.jupiter.api.DisplayName; 5 | import org.junit.jupiter.api.Nested; 6 | import org.junit.jupiter.api.Test; 7 | 8 | import static org.junit.jupiter.api.Assertions.assertEquals; 9 | import static org.junit.jupiter.api.Assertions.assertThrows; 10 | 11 | @DisplayName("Pub spec") 12 | public class PubPricesTest { 13 | 14 | private Pub pub; 15 | 16 | @BeforeEach 17 | public void setUp() throws Exception { 18 | pub = new Pub(); 19 | } 20 | 21 | @Test 22 | @DisplayName("When we order one beer, then the price is 74 kr.") 23 | public void oneBeerTest() { 24 | int actualPrice = pub.computeCost(Pub.ONE_BEER, false, 1); 25 | assertEquals(74, actualPrice); 26 | } 27 | 28 | @Test 29 | @DisplayName("When we order one cider, then the price is 103 kr.") 30 | public void testCidersAreCostly() throws Exception { 31 | int actualPrice = pub.computeCost(Pub.ONE_CIDER, false, 1); 32 | assertEquals(103, actualPrice); 33 | } 34 | 35 | @Test 36 | @DisplayName("When we order a proper cider, then the price is 110 kr.") 37 | public void testProperCidersAreEvenMoreExpensive() throws Exception { 38 | int actualPrice = pub.computeCost(Pub.A_PROPER_CIDER, false, 1); 39 | assertEquals(110, actualPrice); 40 | } 41 | 42 | @Test 43 | @DisplayName("When we order a gin and tonic, then the price is 115 kr.") 44 | public void testACocktail() throws Exception { 45 | int actualPrice = pub.computeCost(Pub.GT, false, 1); 46 | assertEquals(115, actualPrice); 47 | } 48 | 49 | @Test 50 | @DisplayName("When we order a bacardi special, then the price is 127 kr.") 51 | public void testBacardiSpecial() throws Exception { 52 | int actualPrice = pub.computeCost(Pub.BACARDI_SPECIAL, false, 1); 53 | assertEquals(127, actualPrice); 54 | } 55 | 56 | @Nested 57 | @DisplayName("Given a customer who is a student") 58 | class Students { 59 | @Test 60 | @DisplayName("When they order a beer, then they get a discount.") 61 | public void testStudentsGetADiscountForBeer() throws Exception { 62 | int actualPrice = pub.computeCost(Pub.ONE_BEER, true, 1); 63 | assertEquals(67, actualPrice); 64 | } 65 | 66 | @Test 67 | @DisplayName("When they order multiple beers, they also get a discount.") 68 | public void testStudentsGetDiscountsWhenOrderingMoreThanOneBeer() throws Exception { 69 | int actualPrice = pub.computeCost(Pub.ONE_BEER, true, 2); 70 | assertEquals(67 * 2, actualPrice); 71 | } 72 | 73 | @Test 74 | @DisplayName("When they order a cocktail, they do not get a discount.") 75 | public void testStudentsDoNotGetDiscountsForCocktails() throws Exception { 76 | int actualPrice = pub.computeCost(Pub.GT, true, 1); 77 | assertEquals(115, actualPrice); 78 | } 79 | } 80 | 81 | @Test 82 | @DisplayName("When they order a drink which is not on the menu, then they are refused.") 83 | public void testThatADrinkNotInTheSortimentGivesError() throws Exception { 84 | assertThrows(RuntimeException.class, () -> pub.computeCost("sanfranciscosling", false, 1)); 85 | } 86 | 87 | @Nested 88 | @DisplayName("When they order more than two drinks") 89 | class MultipleDrinks { 90 | @Test 91 | @DisplayName("and the order is for cocktails, then they are refused.") 92 | public void testCanBuyAtMostTwoDrinksInOneGo() throws Exception { 93 | assertThrows(RuntimeException.class, () -> pub.computeCost(Pub.BACARDI_SPECIAL, false, 3)); 94 | } 95 | 96 | @Test 97 | @DisplayName("and the order is for beers, then they are served.") 98 | public void testCanOrderMoreThanTwoBeers() throws Exception { 99 | pub.computeCost(Pub.ONE_BEER, false, 5); 100 | } 101 | } 102 | } 103 | --------------------------------------------------------------------------------