{"id":"text-ui-changing-inside","slug":"changing-the-inside","title":"Changing a class without changing its users","text":"The practice log only ever answers two questions: the total and the longest session. For that, it doesn't need to keep every session. Two `int` variables, updated at every `add`, are enough, and they take as little memory after a thousand sessions as after one. Here's `PracticeLog` rewritten that way, with `Main` and `UserInterface` exactly as they were:\n\n```java\nimport java.util.Scanner;\n\npublic class Main {\n    public static void main(String[] args) {\n        Scanner scanner = new Scanner(System.in);\n        PracticeLog log = new PracticeLog();\n        UserInterface ui = new UserInterface(scanner, log);\n        ui.start();\n    }\n}\n// ==== PracticeLog.java ====\npublic class PracticeLog {\n    private int totalMinutes;\n    private int longestSession;\n\n    public PracticeLog() {\n        this.totalMinutes = 0;\n        this.longestSession = 0;\n    }\n\n    public void add(int minutes) {\n        this.totalMinutes = this.totalMinutes + minutes;\n        if (minutes > this.longestSession) {\n            this.longestSession = minutes;\n        }\n    }\n\n    public int total() {\n        return this.totalMinutes;\n    }\n\n    public int longest() {\n        return this.longestSession;\n    }\n}\n// ==== UserInterface.java ====\nimport java.util.Scanner;\n\npublic class UserInterface {\n    private Scanner scanner;\n    private PracticeLog log;\n\n    public UserInterface(Scanner scanner, PracticeLog log) {\n        this.scanner = scanner;\n        this.log = log;\n    }\n\n    public void start() {\n        while (true) {\n            System.out.print(\"> \");\n            String command = this.scanner.nextLine();\n            if (command.equals(\"quit\")) {\n                System.out.println(\"Keep practicing!\");\n                break;\n            }\n            if (command.equals(\"log\")) {\n                int minutes = Integer.valueOf(this.scanner.nextLine());\n                this.log.add(minutes);\n                System.out.println(\"Logged \" + minutes + \" minutes\");\n            } else if (command.equals(\"total\")) {\n                System.out.println(\"Total: \" + this.log.total() + \" minutes\");\n            } else if (command.equals(\"longest\")) {\n                System.out.println(\"Longest: \" + this.log.longest() + \" minutes\");\n            } else {\n                System.out.println(\"Unknown command: \" + command);\n            }\n        }\n    }\n}\n```\n\n```input\nlog\n25\nlog\n40\ntotal\nlog\n15\nlongest\nquit\n```\n\n```output\n> Logged 25 minutes\n> Logged 40 minutes\n> Total: 65 minutes\n> Logged 15 minutes\n> Longest: 40 minutes\n> Keep practicing!\n```\n\n- `PracticeLog` has no list any more. `add` adds the minutes to the total, and when the new session is longer than the longest so far, it becomes the longest.\n- Its public methods are the same as before: `add(int minutes)`, `total()` and `longest()`, with the same parameters, the same return types and the same answers. Those methods are all the user interface ever uses, so it didn't need a single change, and the program prints what it did.\n- The instance variables are `private`, so no other class could have used the old list. That's what `private` is for: whatever is private can change without breaking anything outside the class.\n\n### What must stay the same\n\nThe public methods are a promise to the rest of the program: this is what you can call, this is what it takes, and this is what it gives back. Everything inside may change: the instance variables, how an answer is worked out, the private methods. The promise breaks when a public method's name, parameters or return type change, or when its answers change meaning, for example if `total()` suddenly returned hours instead of minutes. Then every class that uses it has to change too.\n\nA change inside has to give the same answers as before in every case, the unusual ones included. The new `longestSession` starts from 0, just as the old loop did, so even a log without any sessions answers the same, 0. That's easy to get wrong, as this step's last challenge shows.\n\nAnother change of this kind makes a class safer rather than smaller. A class that keeps two lists which must stay in step, such as the names of things in one list and their weights in another, is easy to break: remove from one list and forget the other, and every name after it gets the wrong weight. One list of objects, each with a name and a weight, can't get out of step. As long as the public methods stay the same, the class's users don't need to know about that change either.\n","task":"A café's tip jar program works, but `TipJar` keeps every tip in a list, only to answer three questions. Rewrite `TipJar` without the list: keep the number of tips, their total and the biggest tip in three `int` variables, and update them in `add`. Keep the public methods as they are, so that `UserInterface.java` and `Main.java` work without a change. For the input shown, the program prints:\n\n```\n> Thank you!\n> Thank you!\n> Thank you!\n> 3 tips\n> Total: 450 cents\n> Biggest: 200 cents\n> Closed\n```\n","kind":"code","seed":"// ==== TipJar.java ====\nimport java.util.ArrayList;\n\npublic class TipJar {\n    private ArrayList<Integer> tips;\n\n    public TipJar() {\n        this.tips = new ArrayList<>();\n    }\n\n    public void add(int cents) {\n        this.tips.add(cents);\n    }\n\n    public int count() {\n        return this.tips.size();\n    }\n\n    public int total() {\n        int sum = 0;\n        for (int tip : this.tips) {\n            sum = sum + tip;\n        }\n        return sum;\n    }\n\n    public int biggest() {\n        int biggest = 0;\n        for (int tip : this.tips) {\n            if (tip > biggest) {\n                biggest = tip;\n            }\n        }\n        return biggest;\n    }\n}\n\n// ==== UserInterface.java ====\nimport java.util.Scanner;\n\npublic class UserInterface {\n    private Scanner scanner;\n    private TipJar jar;\n\n    public UserInterface(Scanner scanner, TipJar jar) {\n        this.scanner = scanner;\n        this.jar = jar;\n    }\n\n    public void start() {\n        while (true) {\n            System.out.print(\"> \");\n            String command = this.scanner.nextLine();\n            if (command.equals(\"close\")) {\n                System.out.println(\"Closed\");\n                break;\n            }\n            if (command.equals(\"tip\")) {\n                this.jar.add(Integer.valueOf(this.scanner.nextLine()));\n                System.out.println(\"Thank you!\");\n            } else if (command.equals(\"count\")) {\n                System.out.println(this.jar.count() + \" tips\");\n            } else if (command.equals(\"total\")) {\n                System.out.println(\"Total: \" + this.jar.total() + \" cents\");\n            } else if (command.equals(\"biggest\")) {\n                System.out.println(\"Biggest: \" + this.jar.biggest() + \" cents\");\n            } else {\n                System.out.println(\"Unknown command: \" + command);\n            }\n        }\n    }\n}\n\n// ==== Main.java ====\nimport java.util.Scanner;\n\npublic class Main {\n    public static void main(String[] args) {\n        Scanner scanner = new Scanner(System.in);\n        TipJar jar = new TipJar();\n        UserInterface ui = new UserInterface(scanner, jar);\n        ui.start();\n    }\n}\n","solution":"// ==== TipJar.java ====\npublic class TipJar {\n    private int count;\n    private int total;\n    private int biggest;\n\n    public TipJar() {\n        this.count = 0;\n        this.total = 0;\n        this.biggest = 0;\n    }\n\n    public void add(int cents) {\n        this.count++;\n        this.total = this.total + cents;\n        if (cents > this.biggest) {\n            this.biggest = cents;\n        }\n    }\n\n    public int count() {\n        return this.count;\n    }\n\n    public int total() {\n        return this.total;\n    }\n\n    public int biggest() {\n        return this.biggest;\n    }\n}\n\n// ==== UserInterface.java ====\nimport java.util.Scanner;\n\npublic class UserInterface {\n    private Scanner scanner;\n    private TipJar jar;\n\n    public UserInterface(Scanner scanner, TipJar jar) {\n        this.scanner = scanner;\n        this.jar = jar;\n    }\n\n    public void start() {\n        while (true) {\n            System.out.print(\"> \");\n            String command = this.scanner.nextLine();\n            if (command.equals(\"close\")) {\n                System.out.println(\"Closed\");\n                break;\n            }\n            if (command.equals(\"tip\")) {\n                this.jar.add(Integer.valueOf(this.scanner.nextLine()));\n                System.out.println(\"Thank you!\");\n            } else if (command.equals(\"count\")) {\n                System.out.println(this.jar.count() + \" tips\");\n            } else if (command.equals(\"total\")) {\n                System.out.println(\"Total: \" + this.jar.total() + \" cents\");\n            } else if (command.equals(\"biggest\")) {\n                System.out.println(\"Biggest: \" + this.jar.biggest() + \" cents\");\n            } else {\n                System.out.println(\"Unknown command: \" + command);\n            }\n        }\n    }\n}\n\n// ==== Main.java ====\nimport java.util.Scanner;\n\npublic class Main {\n    public static void main(String[] args) {\n        Scanner scanner = new Scanner(System.in);\n        TipJar jar = new TipJar();\n        UserInterface ui = new UserInterface(scanner, jar);\n        ui.start();\n    }\n}\n","hints":["Replace the list with `private int count;`, `private int total;` and `private int biggest;`, all 0 in the constructor. A variable may have the same name as a method: `this.count` is the variable, `this.count()` the method.","`add` does all the work: `this.count++;`, adds `cents` to `this.total`, and makes `cents` the new `this.biggest` when it's bigger. The three other methods only return a variable."],"tests":[{"name":"Test 1","stdin":"tip\n150\ntip\n200\ntip\n100\ncount\ntotal\nbiggest\nclose\n","expect":"> Thank you!\n> Thank you!\n> Thank you!\n> 3 tips\n> Total: 450 cents\n> Biggest: 200 cents\n> Closed","hidden":false},{"name":"Test 2","stdin":"count\ntotal\nbiggest\ntip\n5\ntips\ncount\nbiggest\nclose\n","expect":"> 0 tips\n> Total: 0 cents\n> Biggest: 0 cents\n> Thank you!\n> Unknown command: tips\n> 1 tips\n> Biggest: 5 cents\n> Closed","hidden":true},{"name":"Test 3","stdin":"","call":"TipJar jar = new TipJar();\njar.add(50);\njar.add(20);\njar.add(50);\nTipJar other = new TipJar();\nother.add(300);\nSystem.out.println(jar.count() + \" \" + jar.total() + \" \" + jar.biggest());\nSystem.out.println(other.count() + \" \" + other.total() + \" \" + other.biggest());\n","expect":"3 120 50\n1 300 300","hidden":true}],"require":[],"forbid":[{"pattern":"\\bArrayList\\s*<","message":"Keep three `int` variables instead of the list: the program shouldn't need an ArrayList any more."}],"more":[{"task":"A camping program keeps a packing list. `PackingList` keeps two lists that must stay in step: the names of the things in one, and their weights in grams in the other. Rewrite it to keep a single `ArrayList<Item>`, using the ready `Item` class. Keep its public methods, `add(String name, int grams)`, `totalGrams()` and `heaviest()`, as they are: `Main.java`, the user interface, must work without a change. When two things weigh the same, `heaviest()` returns the one packed first, as the old code did. For the input shown, the program prints:\n\n```\n> Packed tent\n> Packed stove\n> Packed sleeping bag\n> Total: 3800 grams\n> Heaviest: tent\n> Have a good trip!\n```\n","kind":"code","seed":"// ==== PackingList.java ====\nimport java.util.ArrayList;\n\npublic class PackingList {\n    private ArrayList<String> names;\n    private ArrayList<Integer> grams;\n\n    public PackingList() {\n        this.names = new ArrayList<>();\n        this.grams = new ArrayList<>();\n    }\n\n    public void add(String name, int grams) {\n        this.names.add(name);\n        this.grams.add(grams);\n    }\n\n    public int totalGrams() {\n        int total = 0;\n        for (int weight : this.grams) {\n            total = total + weight;\n        }\n        return total;\n    }\n\n    public String heaviest() {\n        if (this.names.isEmpty()) {\n            return \"nothing\";\n        }\n        int best = 0;\n        for (int i = 1; i < this.names.size(); i++) {\n            if (this.grams.get(i) > this.grams.get(best)) {\n                best = i;\n            }\n        }\n        return this.names.get(best);\n    }\n}\n\n// ==== Item.java ====\npublic class Item {\n    private String name;\n    private int grams;\n\n    public Item(String name, int grams) {\n        this.name = name;\n        this.grams = grams;\n    }\n\n    public String getName() {\n        return this.name;\n    }\n\n    public int getGrams() {\n        return this.grams;\n    }\n}\n\n// ==== Main.java ====\nimport java.util.Scanner;\n\npublic class Main {\n    public static void main(String[] args) {\n        Scanner scanner = new Scanner(System.in);\n        PackingList packing = new PackingList();\n        while (true) {\n            System.out.print(\"> \");\n            String command = scanner.nextLine();\n            if (command.equals(\"done\")) {\n                System.out.println(\"Have a good trip!\");\n                break;\n            }\n            if (command.equals(\"pack\")) {\n                String name = scanner.nextLine();\n                int grams = Integer.valueOf(scanner.nextLine());\n                packing.add(name, grams);\n                System.out.println(\"Packed \" + name);\n            } else if (command.equals(\"weight\")) {\n                System.out.println(\"Total: \" + packing.totalGrams() + \" grams\");\n            } else if (command.equals(\"heaviest\")) {\n                System.out.println(\"Heaviest: \" + packing.heaviest());\n            } else {\n                System.out.println(\"Unknown command: \" + command);\n            }\n        }\n    }\n}\n","solution":"// ==== PackingList.java ====\nimport java.util.ArrayList;\n\npublic class PackingList {\n    private ArrayList<Item> items;\n\n    public PackingList() {\n        this.items = new ArrayList<>();\n    }\n\n    public void add(String name, int grams) {\n        this.items.add(new Item(name, grams));\n    }\n\n    public int totalGrams() {\n        int total = 0;\n        for (Item item : this.items) {\n            total = total + item.getGrams();\n        }\n        return total;\n    }\n\n    public String heaviest() {\n        if (this.items.isEmpty()) {\n            return \"nothing\";\n        }\n        Item heaviest = this.items.get(0);\n        for (Item item : this.items) {\n            if (item.getGrams() > heaviest.getGrams()) {\n                heaviest = item;\n            }\n        }\n        return heaviest.getName();\n    }\n}\n\n// ==== Item.java ====\npublic class Item {\n    private String name;\n    private int grams;\n\n    public Item(String name, int grams) {\n        this.name = name;\n        this.grams = grams;\n    }\n\n    public String getName() {\n        return this.name;\n    }\n\n    public int getGrams() {\n        return this.grams;\n    }\n}\n\n// ==== Main.java ====\nimport java.util.Scanner;\n\npublic class Main {\n    public static void main(String[] args) {\n        Scanner scanner = new Scanner(System.in);\n        PackingList packing = new PackingList();\n        while (true) {\n            System.out.print(\"> \");\n            String command = scanner.nextLine();\n            if (command.equals(\"done\")) {\n                System.out.println(\"Have a good trip!\");\n                break;\n            }\n            if (command.equals(\"pack\")) {\n                String name = scanner.nextLine();\n                int grams = Integer.valueOf(scanner.nextLine());\n                packing.add(name, grams);\n                System.out.println(\"Packed \" + name);\n            } else if (command.equals(\"weight\")) {\n                System.out.println(\"Total: \" + packing.totalGrams() + \" grams\");\n            } else if (command.equals(\"heaviest\")) {\n                System.out.println(\"Heaviest: \" + packing.heaviest());\n            } else {\n                System.out.println(\"Unknown command: \" + command);\n            }\n        }\n    }\n}\n","hints":["`add` still takes a name and a weight, but now makes one object of them: `this.items.add(new Item(name, grams));`.","`totalGrams` adds up `item.getGrams()` with a for-each loop. `heaviest` starts from the first item, keeps the heaviest one found so far, and returns its `getName()`. An empty list still gives `nothing`."],"tests":[{"name":"Test 1","stdin":"pack\ntent\n2100\npack\nstove\n400\npack\nsleeping bag\n1300\nweight\nheaviest\ndone\n","expect":"> Packed tent\n> Packed stove\n> Packed sleeping bag\n> Total: 3800 grams\n> Heaviest: tent\n> Have a good trip!","hidden":false},{"name":"Test 2","stdin":"heaviest\nweight\npack\nmap\n90\nheaviest\nunpack\ndone\n","expect":"> Heaviest: nothing\n> Total: 0 grams\n> Packed map\n> Heaviest: map\n> Unknown command: unpack\n> Have a good trip!","hidden":true},{"name":"Test 3","stdin":"","call":"PackingList packing = new PackingList();\npacking.add(\"rope\", 800);\npacking.add(\"boots\", 1600);\npacking.add(\"lamp\", 1600);\npacking.add(\"cup\", 150);\nSystem.out.println(packing.totalGrams() + \" \" + packing.heaviest());\n","expect":"4150 boots","hidden":true}],"require":[{"pattern":"ArrayList\\s*<\\s*Item\\s*>","message":"Keep the things in one `ArrayList<Item>`."}],"forbid":[{"pattern":"ArrayList\\s*<\\s*(String|Integer)\\s*>","message":"Replace the two lists with the one list of Item objects."}]},{"task":"A weather station's `Thermometer` used to keep a list of every reading. Someone rewrote it to keep only the number of readings and the lowest and highest one. The user interface didn't change, but the answers did: for the input shown, the program now says the lowest reading is 0, although no reading was 0. Fix `Thermometer`, keeping its public methods, so that the program answers as it did before. For the input shown, it prints:\n\n```\n> Recorded 12\n> Recorded 17\n> Recorded 9\n> Lowest: 9\n> Highest: 17\n> Station off\n```\n","kind":"code","seed":"// ==== Thermometer.java ====\npublic class Thermometer {\n    private int count;\n    private int lowest;\n    private int highest;\n\n    public Thermometer() {\n        this.count = 0;\n        this.lowest = 0;\n        this.highest = 0;\n    }\n\n    public void record(int degrees) {\n        if (degrees < this.lowest) {\n            this.lowest = degrees;\n        }\n        if (degrees > this.highest) {\n            this.highest = degrees;\n        }\n        this.count++;\n    }\n\n    public int count() {\n        return this.count;\n    }\n\n    public int lowest() {\n        return this.lowest;\n    }\n\n    public int highest() {\n        return this.highest;\n    }\n}\n\n// ==== UserInterface.java ====\nimport java.util.Scanner;\n\npublic class UserInterface {\n    private Scanner scanner;\n    private Thermometer thermometer;\n\n    public UserInterface(Scanner scanner, Thermometer thermometer) {\n        this.scanner = scanner;\n        this.thermometer = thermometer;\n    }\n\n    public void start() {\n        while (true) {\n            System.out.print(\"> \");\n            String command = this.scanner.nextLine();\n            if (command.equals(\"quit\")) {\n                System.out.println(\"Station off\");\n                break;\n            }\n            if (command.equals(\"add\")) {\n                int degrees = Integer.valueOf(this.scanner.nextLine());\n                this.thermometer.record(degrees);\n                System.out.println(\"Recorded \" + degrees);\n            } else if (command.equals(\"low\") || command.equals(\"high\")) {\n                if (this.thermometer.count() == 0) {\n                    System.out.println(\"No readings yet\");\n                } else if (command.equals(\"low\")) {\n                    System.out.println(\"Lowest: \" + this.thermometer.lowest());\n                } else {\n                    System.out.println(\"Highest: \" + this.thermometer.highest());\n                }\n            } else {\n                System.out.println(\"Unknown command: \" + command);\n            }\n        }\n    }\n}\n\n// ==== Main.java ====\nimport java.util.Scanner;\n\npublic class Main {\n    public static void main(String[] args) {\n        Scanner scanner = new Scanner(System.in);\n        Thermometer thermometer = new Thermometer();\n        UserInterface ui = new UserInterface(scanner, thermometer);\n        ui.start();\n    }\n}\n","solution":"// ==== Thermometer.java ====\npublic class Thermometer {\n    private int count;\n    private int lowest;\n    private int highest;\n\n    public Thermometer() {\n        this.count = 0;\n        this.lowest = 0;\n        this.highest = 0;\n    }\n\n    public void record(int degrees) {\n        if (this.count == 0 || degrees < this.lowest) {\n            this.lowest = degrees;\n        }\n        if (this.count == 0 || degrees > this.highest) {\n            this.highest = degrees;\n        }\n        this.count++;\n    }\n\n    public int count() {\n        return this.count;\n    }\n\n    public int lowest() {\n        return this.lowest;\n    }\n\n    public int highest() {\n        return this.highest;\n    }\n}\n\n// ==== UserInterface.java ====\nimport java.util.Scanner;\n\npublic class UserInterface {\n    private Scanner scanner;\n    private Thermometer thermometer;\n\n    public UserInterface(Scanner scanner, Thermometer thermometer) {\n        this.scanner = scanner;\n        this.thermometer = thermometer;\n    }\n\n    public void start() {\n        while (true) {\n            System.out.print(\"> \");\n            String command = this.scanner.nextLine();\n            if (command.equals(\"quit\")) {\n                System.out.println(\"Station off\");\n                break;\n            }\n            if (command.equals(\"add\")) {\n                int degrees = Integer.valueOf(this.scanner.nextLine());\n                this.thermometer.record(degrees);\n                System.out.println(\"Recorded \" + degrees);\n            } else if (command.equals(\"low\") || command.equals(\"high\")) {\n                if (this.thermometer.count() == 0) {\n                    System.out.println(\"No readings yet\");\n                } else if (command.equals(\"low\")) {\n                    System.out.println(\"Lowest: \" + this.thermometer.lowest());\n                } else {\n                    System.out.println(\"Highest: \" + this.thermometer.highest());\n                }\n            } else {\n                System.out.println(\"Unknown command: \" + command);\n            }\n        }\n    }\n}\n\n// ==== Main.java ====\nimport java.util.Scanner;\n\npublic class Main {\n    public static void main(String[] args) {\n        Scanner scanner = new Scanner(System.in);\n        Thermometer thermometer = new Thermometer();\n        UserInterface ui = new UserInterface(scanner, thermometer);\n        ui.start();\n    }\n}\n","hints":["The lowest reading starts at 0, and a reading of 9 isn't lower than 0, so it's never kept. The same goes for the highest when every reading is below 0. Whatever the start value, the first reading must become both the lowest and the highest.","In `record`, before `this.count++`, the first reading is the one that comes when `this.count == 0`. Add that to both conditions: `if (this.count == 0 || degrees < this.lowest)`."],"tests":[{"name":"Test 1","stdin":"add\n12\nadd\n17\nadd\n9\nlow\nhigh\nquit\n","expect":"> Recorded 12\n> Recorded 17\n> Recorded 9\n> Lowest: 9\n> Highest: 17\n> Station off","hidden":false},{"name":"Test 2","stdin":"low\nadd\n-4\nadd\n-9\nadd\n-2\nhigh\nlow\nquit\n","expect":"> No readings yet\n> Recorded -4\n> Recorded -9\n> Recorded -2\n> Highest: -2\n> Lowest: -9\n> Station off","hidden":true},{"name":"Test 3","stdin":"","call":"Thermometer t = new Thermometer();\nt.record(5);\nSystem.out.println(t.lowest() + \" \" + t.highest() + \" \" + t.count());\nt.record(-3);\nt.record(0);\nt.record(8);\nSystem.out.println(t.lowest() + \" \" + t.highest() + \" \" + t.count());\n","expect":"5 5 1\n-3 8 4","hidden":true},{"name":"Test 4","stdin":"","call":"Thermometer t = new Thermometer();\nt.record(-6);\nt.record(-1);\nSystem.out.println(t.lowest() + \" \" + t.highest());\n","expect":"-6 -1","hidden":true}],"require":[],"forbid":[]}]}