'Is there a way I can break up into more than 2 classes
I am wondering how/if I could possibly break this up into multiple classes but keep the values across objects to pass between both. I am unable to get things working including using a constructor - the values from my first object won't go over to my second. I've tried to copy them to the other class object using the copy method to no avail. Everything I find online uses only 2 classes, but I'd like to break it up into at least 3-4 (If possible separating the WantToCook and CookAgain methods into their own class). I know there are all sorts of other improvements I could make, but due to time constraints they can no longer be a priority for me. Please help if it's even possible.
My project inputs ingredients and tells the user what recipes can be made with them. The user "cooks" a recipe and then the ingredients are deducted from their initial counts. The user ideally keeps doing this until no ingredients remain, but as you will see the values go into the negative. That's one of the many things I wanted to fix in addition to this but it's no longer a priority for me.
Here is my problem child eyesore class:
import java.util.Scanner;
//object class
public class ShowRecipes {
Scanner kbd = new Scanner(System.in);
//ingredient names array
public String ingredientsNames[] = {"Apples", "Cherry", "Carrot", "Flour ", "Sugar "};
//ingredients array - setters will set
public int[] ingredients = new int[5];
//Basic constructor
public ShowRecipes() {
ingredients[0] = 1; //apple
ingredients[1] = 1; //cherry
ingredients[2] = 1;//carrot
ingredients[3] = 0;//flour
ingredients[4] = 0;//sugar
}
//overloaded constructor that covers each field
public ShowRecipes(int apples, int cherries, int carrots, int flour, int sugar) {
System.out.println("overloaded");
setApples(apples);
setCherries(cherries);
setCarrots(carrots);
setFlour(flour);
setSugar(sugar);
}
public void input() { // user inputs ingredient counts
//Asks user for ingredients on-hand
System.out.println("Please enter how many of each ingredient you have:");
System.out.print(ingredientsNames[0]);
int apples = kbd.nextInt();
setApples(apples);
System.out.print(ingredientsNames[1]);
int cherries = kbd.nextInt();
setCherries(cherries);
System.out.print(ingredientsNames[2]);
int carrots = kbd.nextInt();
setCarrots(carrots);
System.out.print(ingredientsNames[3]);
int flour = kbd.nextInt();
setFlour(flour);
System.out.print(ingredientsNames[4]);
int sugar = kbd.nextInt();
setSugar(sugar);
}//end user ingredient input
public void IngredientCount() { //Lists ingredient input counts
System.out.println("According to your input, you have:");
System.out.println("Ingredient\tValue");
for(int counter=0;counter<ingredients.length;counter++) {
System.out.println(ingredientsNames[counter] + "\t\t" + ingredients[counter]);
}//end for loop
}//end ingredient listing
public void Selection(){ //Can access every method in class except CookAgain
System.out.println("What would you like to do now? Enter a number to select. \n1. Cook something \n2. See ingredients \n3. See available recipes \n4. Modify ingredient amounts");
//1. Cook something -- WantToCook()
//2. See ingredient list -- IngredientCount()
//3. See available recipes -- compareRecipe()
//4. Modify total ingredient amounts -- input()
//maybe put this in ShowRecipes, create object for each to put in own classes
int selection = kbd.nextInt();
switch (selection) {
case 1:
WantToCook();
break;
case 2:
IngredientCount();
break;
case 3:
compareRecipe();
break;
case 4:
input();
break;
default:
System.out.println("Invalid");
return;
}}
//Setters - set from input()
//Apple setter
public void setApples(int apples){
ingredients[0] = apples;
}
//Cherry setter
public void setCherries(int cherries){
ingredients[1] = cherries;
}
//Carrot setter
public void setCarrots(int carrots){
ingredients[2]= carrots;
}
//Flour setter
public void setFlour(int flour){
ingredients[3] = flour;
}
//Sugar setter
public void setSugar(int sugar){
ingredients[4] = sugar;
}
//Getters
//Apple getter
public int getApples(){
return ingredients[0];
}
//Cherry getter
public int getCherries(){
return ingredients[1];
}
//Carrot getter
public int getCarrots(){
return ingredients[2];
}
//Flour getter
public int getFlour(){
return ingredients[3];
}
//Sugar getter
public int getSugar(){
return ingredients[4];
}
public void compareRecipe() {
System.out.println("According to the input, you can make:");
//Apples
if (ingredients[0] >= 3) {
System.out.println(RecipeNamesA[0]); //Apple Jam
}
if (ingredients[0] >= 2) {
System.out.println(RecipeNamesA[1]); //Apple Jelly
System.out.println(RecipeNamesA[2]); //Apple Smoothie
}
if (ingredients[0] >= 1 && ingredients[3] >= 1){
System.out.println(RecipeNamesA[3]); //Apple Tart
}
if (ingredients[0] >= 2 && ingredients[4] >= 2 && ingredients[3] >= 1){
System.out.println(RecipeNamesA[4]); //Apple Pie
}
if (ingredients[0] == 0) {
System.out.println("No apple recipes");
}
//Cherries
if (ingredients[1] >= 3) {
System.out.println(RecipeNamesCh[0]); //Cherry Jam
}
if (ingredients[1] >= 2) {
System.out.println(RecipeNamesCh[1]); //Cherry Jelly
}
if (ingredients[1] >= 2 && ingredients[3] >= 3 && ingredients[4] >= 2) {
System.out.println(RecipeNamesCh[2]); //Cherry Pie
}
if (ingredients[1] >= 1 && ingredients[3] <= 1 && ingredients[4] >= 1) {
System.out.println(RecipeNamesCh[3]); //Cherry Tart
}
if (ingredients[1] == 0) {
System.out.println("No cherry recipes");
}
//Carrots
if (ingredients[2] >= 2) {
System.out.println(RecipeNamesCa[0]); //Carrot juice
}
if (ingredients[2] >= 2 && ingredients[3] >= 1) {
System.out.println(RecipeNamesCa[2]); //Carrot potage
}
if (ingredients[2] >= 1 && ingredients[3] >= 1 && ingredients[4] >= 1){
System.out.println(RecipeNamesCa[1]); //Carrot cake
}
if (ingredients[2] == 0) {
System.out.println("No carrot recipes");
}
}
private String RecipeNamesA[] = {"1. Apple Jam", "2. Apple Jelly", "3. Apple Smoothie", "4. Apple Tart", "5. Apple Pie"}; //Apple recipes
private String RecipeNamesCh[] = {"6. Cherry Jam", "7. Cherry Jelly", "8. Cherry Pie", "9. Cherry Tart"}; //Cherry recipes
private String RecipeNamesCa[] = {"10. Carrot Juice", "11. Carrot Cake","12. Carrot Potage"}; //Carrot recipes
public void WantToCook() { //Enter recipe number - deducts ingredients
System.out.println("What would you like to cook? Please enter the recipe number.");
int recipeNum = kbd.nextInt();
switch(recipeNum) {
case 1: //Apple jam
setApples(getApples()-3);
System.out.println(RecipeNamesA[0]);
break;
case 2: //Apple jelly
System.out.println(RecipeNamesA[1]);
setApples(getApples()-2);
break;
case 3://Apple Smoothie
System.out.println(RecipeNamesA[2]);
setApples(getApples()-2);
break;
case 4: //Apple Tart
System.out.println(RecipeNamesA[3]);
setApples(getApples()-1);
setFlour(getFlour()-1);
setSugar(getSugar()-1);
break;
case 5: //Apple Pie
System.out.println(RecipeNamesA[4]);
setApples(getApples()-2);
setFlour(getFlour()-3);
setSugar(getSugar()-2);
break;
case 6: //Cherry Jam
System.out.println(RecipeNamesCh[0]);
setCherries(getCherries()-3);
break;
case 7: //Cherry Jelly
System.out.println(RecipeNamesCh[1]);
setCherries(getCherries()-2);
break;
case 8: //Cherry Pie
System.out.println(RecipeNamesCh[2]);
setCherries(getCherries()-2);
setFlour(getFlour()-3);
setSugar(getSugar()-2);
break;
case 9: //Cherry Tart
System.out.println(RecipeNamesCh[3]);
setCherries(getCherries()-1);
setFlour(getFlour()-1);
setSugar(getSugar()-1);
break;
case 10: //Carrot juice
System.out.println(RecipeNamesCa[0]);
setCarrots(getCarrots()-2);
break;
case 11: //Carrot cake
System.out.println(RecipeNamesCa[1]);
setCarrots(getCarrots()-1);
setFlour(getFlour()-1);
setSugar(getSugar()-1);
break;
case 12: //Carrot potage
System.out.println((RecipeNamesCa[2]));
setCarrots(getCarrots()-2);
setFlour(getFlour()-1);
break;
default:
System.out.println("Invalid entry.");
}//End recipeNum switch
System.out.println("You have cooked a thing! Good job!");
CookAgain(); //cook again? prompt
} //end WantToCook method
public void CookAgain() {
System.out.println("Would you like to cook again? Please type yes or no");
kbd.nextLine();
String answer = kbd.nextLine();
switch (answer) {
case "yes":
if (ingredients[0] >= 1 || ingredients[1] >= 1 || ingredients[2] >= 1) { //no apples, cherries, or carrots
System.out.println("You've used some ingredients. Let's see what you have now.");
IngredientCount();
compareRecipe();
WantToCook();
break;
}
else {
System.out.println("You don't have enough ingredients left to cook anything.");
break;
}
case "no":
System.out.println("Ok, done cooking. Enjoy!");
break;
default:
System.out.println("Invalid answer please try again!");
}// End answer switch
kbd.close(); // close scanner
}
}
Here is by itty bitty driver:
import java.util.Scanner;
public class DoTheThing {
public static void main (String [] args){
final ShowRecipes first = new ShowRecipes();
System.out.println("FIRST");
first.input();
first.compareRecipe();
first.WantToCook();
first.Selection();
}
}
Solution 1:[1]
PART 1 -- this answer is too long, so splitting it up amongst multiple answers
I'll start off by saying well done - this is a well designed program. It's certainly not perfect, but it definitely performs most of the basic functionality it needs to just fine.
That said, there's a few specific reasons why you are having trouble separating this into different classes.
This program is tightly coupled. @MadProgrammer actually hinted towards this in the comments earlier, but basically, tightly coupled means that each subcomponent of your program is trying to do too many different things at once. As a result, if you try and change a subcomponent, it breaks other subcomponents further downstream. That makes refactoring painful and difficult. Keep your code loosely coupled, and make it so that each class only does what it absolutely needs to. The rest can be delegated to another class or method or whatever.
You use a lot of global state. This isn't necessarily a bad thing, but it does make it harder to keep track of who edits what. Too much global state means breaking apart the class tends to cause logic errors. We'll be minimizing that in the examples below.
Your methods are quite large and complex. This is kind of a repeat of what was said earlier, but I want to highlight this specifically - if a method gets too cumbersome (either through size, lack of readability, or complexity), it becomes exponentially more difficult to maintain. Once a method reaches some arbitrary difficulty, doing any maintenance (such as breaking apart into classes) you will find that you have dug yourself into a ditch that is quite difficult to climb out of - without doing a full rewrite of your code.
This specific problem you are writing code for is practically begging to use Java Enums. Java (+ JVM languages) has the most powerful implementation of enums of any language. I'm not saying enums can't be overused, but it's a really useful tool that you should work hard to get comfortable with, especially if you plan to code in Java or some JVM language in the future. And doubly so if you ever solve a problem that has a very small and specific number of entities that won't change very often. For example, your ingredients - Apple, Carrot, etc. That really should be an enum. And your recipes, those too.
I will apply each step incrementally, so that you can see the solution evolve over time.
First, here is your unchanged class.
import java.util.Scanner;
public interface Round0
{
//object class
public class ShowRecipes {
public static void main (String [] args){
final ShowRecipes first = new ShowRecipes();
System.out.println("FIRST");
first.input();
first.compareRecipe();
first.WantToCook();
first.Selection();
}
Scanner kbd = new Scanner(System.in);
//ingredient names array
public String ingredientsNames[] = {"Apples", "Cherry", "Carrot", "Flour ", "Sugar "};
//ingredients array - setters will set
public int[] ingredients = new int[5];
//Basic constructor
public ShowRecipes() {
ingredients[0] = 1; //apple
ingredients[1] = 1; //cherry
ingredients[2] = 1;//carrot
ingredients[3] = 0;//flour
ingredients[4] = 0;//sugar
}
//overloaded constructor that covers each field
public ShowRecipes(int apples, int cherries, int carrots, int flour, int sugar) {
System.out.println("overloaded");
setApples(apples);
setCherries(cherries);
setCarrots(carrots);
setFlour(flour);
setSugar(sugar);
}
public void input() { // user inputs ingredient counts
//Asks user for ingredients on-hand
System.out.println("Please enter how many of each ingredient you have:");
System.out.print(ingredientsNames[0]);
int apples = kbd.nextInt();
setApples(apples);
System.out.print(ingredientsNames[1]);
int cherries = kbd.nextInt();
setCherries(cherries);
System.out.print(ingredientsNames[2]);
int carrots = kbd.nextInt();
setCarrots(carrots);
System.out.print(ingredientsNames[3]);
int flour = kbd.nextInt();
setFlour(flour);
System.out.print(ingredientsNames[4]);
int sugar = kbd.nextInt();
setSugar(sugar);
}//end user ingredient input
public void IngredientCount() { //Lists ingredient input counts
System.out.println("According to your input, you have:");
System.out.println("Ingredient\tValue");
for(int counter=0;counter<ingredients.length;counter++) {
System.out.println(ingredientsNames[counter] + "\t\t" + ingredients[counter]);
}//end for loop
}//end ingredient listing
public void Selection(){ //Can access every method in class except CookAgain
System.out.println("What would you like to do now? Enter a number to select. \n1. Cook something \n2. See ingredients \n3. See available recipes \n4. Modify ingredient amounts");
//1. Cook something -- WantToCook()
//2. See ingredient list -- IngredientCount()
//3. See available recipes -- compareRecipe()
//4. Modify total ingredient amounts -- input()
//maybe put this in ShowRecipes, create object for each to put in own classes
int selection = kbd.nextInt();
switch (selection) {
case 1:
WantToCook();
break;
case 2:
IngredientCount();
break;
case 3:
compareRecipe();
break;
case 4:
input();
break;
default:
System.out.println("Invalid");
return;
}}
//Setters - set from input()
//Apple setter
public void setApples(int apples){
ingredients[0] = apples;
}
//Cherry setter
public void setCherries(int cherries){
ingredients[1] = cherries;
}
//Carrot setter
public void setCarrots(int carrots){
ingredients[2]= carrots;
}
//Flour setter
public void setFlour(int flour){
ingredients[3] = flour;
}
//Sugar setter
public void setSugar(int sugar){
ingredients[4] = sugar;
}
//Getters
//Apple getter
public int getApples(){
return ingredients[0];
}
//Cherry getter
public int getCherries(){
return ingredients[1];
}
//Carrot getter
public int getCarrots(){
return ingredients[2];
}
//Flour getter
public int getFlour(){
return ingredients[3];
}
//Sugar getter
public int getSugar(){
return ingredients[4];
}
public void compareRecipe() {
System.out.println("According to the input, you can make:");
//Apples
if (ingredients[0] >= 3) {
System.out.println(RecipeNamesA[0]); //Apple Jam
}
if (ingredients[0] >= 2) {
System.out.println(RecipeNamesA[1]); //Apple Jelly
System.out.println(RecipeNamesA[2]); //Apple Smoothie
}
if (ingredients[0] >= 1 && ingredients[3] >= 1){
System.out.println(RecipeNamesA[3]); //Apple Tart
}
if (ingredients[0] >= 2 && ingredients[4] >= 2 && ingredients[3] >= 1){
System.out.println(RecipeNamesA[4]); //Apple Pie
}
if (ingredients[0] == 0) {
System.out.println("No apple recipes");
}
//Cherries
if (ingredients[1] >= 3) {
System.out.println(RecipeNamesCh[0]); //Cherry Jam
}
if (ingredients[1] >= 2) {
System.out.println(RecipeNamesCh[1]); //Cherry Jelly
}
if (ingredients[1] >= 2 && ingredients[3] >= 3 && ingredients[4] >= 2) {
System.out.println(RecipeNamesCh[2]); //Cherry Pie
}
if (ingredients[1] >= 1 && ingredients[3] <= 1 && ingredients[4] >= 1) {
System.out.println(RecipeNamesCh[3]); //Cherry Tart
}
if (ingredients[1] == 0) {
System.out.println("No cherry recipes");
}
//Carrots
if (ingredients[2] >= 2) {
System.out.println(RecipeNamesCa[0]); //Carrot juice
}
if (ingredients[2] >= 2 && ingredients[3] >= 1) {
System.out.println(RecipeNamesCa[2]); //Carrot potage
}
if (ingredients[2] >= 1 && ingredients[3] >= 1 && ingredients[4] >= 1){
System.out.println(RecipeNamesCa[1]); //Carrot cake
}
if (ingredients[2] == 0) {
System.out.println("No carrot recipes");
}
}
private String RecipeNamesA[] = {"1. Apple Jam", "2. Apple Jelly", "3. Apple Smoothie", "4. Apple Tart", "5. Apple Pie"}; //Apple recipes
private String RecipeNamesCh[] = {"6. Cherry Jam", "7. Cherry Jelly", "8. Cherry Pie", "9. Cherry Tart"}; //Cherry recipes
private String RecipeNamesCa[] = {"10. Carrot Juice", "11. Carrot Cake","12. Carrot Potage"}; //Carrot recipes
public void WantToCook() { //Enter recipe number - deducts ingredients
System.out.println("What would you like to cook? Please enter the recipe number.");
int recipeNum = kbd.nextInt();
switch(recipeNum) {
case 1: //Apple jam
setApples(getApples()-3);
System.out.println(RecipeNamesA[0]);
break;
case 2: //Apple jelly
System.out.println(RecipeNamesA[1]);
setApples(getApples()-2);
break;
case 3://Apple Smoothie
System.out.println(RecipeNamesA[2]);
setApples(getApples()-2);
break;
case 4: //Apple Tart
System.out.println(RecipeNamesA[3]);
setApples(getApples()-1);
setFlour(getFlour()-1);
setSugar(getSugar()-1);
break;
case 5: //Apple Pie
System.out.println(RecipeNamesA[4]);
setApples(getApples()-2);
setFlour(getFlour()-3);
setSugar(getSugar()-2);
break;
case 6: //Cherry Jam
System.out.println(RecipeNamesCh[0]);
setCherries(getCherries()-3);
break;
case 7: //Cherry Jelly
System.out.println(RecipeNamesCh[1]);
setCherries(getCherries()-2);
break;
case 8: //Cherry Pie
System.out.println(RecipeNamesCh[2]);
setCherries(getCherries()-2);
setFlour(getFlour()-3);
setSugar(getSugar()-2);
break;
case 9: //Cherry Tart
System.out.println(RecipeNamesCh[3]);
setCherries(getCherries()-1);
setFlour(getFlour()-1);
setSugar(getSugar()-1);
break;
case 10: //Carrot juice
System.out.println(RecipeNamesCa[0]);
setCarrots(getCarrots()-2);
break;
case 11: //Carrot cake
System.out.println(RecipeNamesCa[1]);
setCarrots(getCarrots()-1);
setFlour(getFlour()-1);
setSugar(getSugar()-1);
break;
case 12: //Carrot potage
System.out.println((RecipeNamesCa[2]));
setCarrots(getCarrots()-2);
setFlour(getFlour()-1);
break;
default:
System.out.println("Invalid entry.");
}//End recipeNum switch
System.out.println("You have cooked a thing! Good job!");
CookAgain(); //cook again? prompt
} //end WantToCook method
public void CookAgain() {
System.out.println("Would you like to cook again? Please type yes or no");
kbd.nextLine();
String answer = kbd.nextLine();
switch (answer) {
case "yes":
if (ingredients[0] >= 1 || ingredients[1] >= 1 || ingredients[2] >= 1) { //no apples, cherries, or carrots
System.out.println("You've used some ingredients. Let's see what you have now.");
IngredientCount();
compareRecipe();
WantToCook();
break;
}
else {
System.out.println("You don't have enough ingredients left to cook anything.");
break;
}
case "no":
System.out.println("Ok, done cooking. Enjoy!");
break;
default:
System.out.println("Invalid answer please try again!");
}// End answer switch
kbd.close(); // close scanner
}
}
}
Let's start by fixing #1
#1 Tight Coupling
Right now, your ShowRecipes
class literally does it all.
- A - It communicates with your user
- B - It handles the business logic, which is the logic decides what should change and when.
- C - It manipulates the internal state -- the act of actually changing stuff, as opposed to deciding when to change. For example, recognizing that you need to subtract 5 apples, as opposed to actually doing the subtraction
That is a lot for one class to handle. So the first thing I would do is decouple each of its methods into multiple smaller ones. Then after that, we can start grouping methods into different classes. Let's look through your methods.
public ShowRecipes()
- This is nice and simple, almost exactly what we are looking for. All it does is change state. This method shouldn't be modified (yet). And while I'm at it, I feel similarly aboutpublic ShowRecipes(int apples, int cherries, int carrots, int flour, int sugar)
public void input()
- This isn't bad. As is, your method does 3 things - prints a message to a user, reads their response, then changes state to equal the users response. However, the part that makes this method (and thus, the class) tightly coupled is that it hardcodes the implementation inside your method. For example, you are usingSystem.out.println()
to print all your stuff to the commandline. Let's replace all of those with calls ininput()
with a new class that handles the talking with the user -UserPrompt
. This class will ask a question, then return the answer the user gave.
public class UserPrompt
{
final Scanner kbd = new Scanner(System.in);
public int askForNumber(String prompt)
{
System.out.print(prompt);
return kbd.nextInt();
}
}
Then, we will rework your input method to use the UserPrompt
, but this time, will we make it LOOSELY coupled by passing in an instance of UserPrompt
through the parameter of input()
.
public void input(UserPrompt prompt) { // user inputs ingredient counts
//Asks user for ingredients on-hand
System.out.println("Please enter how many of each ingredient you have:");
setApples(prompt.askForNumber(ingredientsNames[0]));
setCherries(prompt.askForNumber(ingredientsNames[1]));
setCarrots(prompt.askForNumber(ingredientsNames[2]));
setFlour(prompt.askForNumber(ingredientsNames[3]));
setSugar(prompt.askForNumber(ingredientsNames[4]));
}//end user ingredient input
And for your main method, change it to be this.
public static void main (String [] args){
final ShowRecipes first = new ShowRecipes();
System.out.println("FIRST");
final UserPrompt prompt = new UserPrompt();
first.input(prompt);
first.compareRecipe();
first.WantToCook();
first.Selection();
}
Now, if you try compiling this, you might get an error in your Selection
method. I am going to jump ahead just a little bit and modify Selection
as well.
public void Selection(UserPrompt prompt){
//I left out some lines that used to be here, just to keep things simple
//Those lines we don't care about right now
//just leave them alone, and only change the parameter line above
//and the input line below
case 4:
input(prompt);
break;
default:
System.out.println("Invalid");
return;
}}
Then, go back to your main method, and change only the first.Selection()
line.
public static void main (String [] args){
//ignore the other lines, only change this line below
first.Selection(prompt);
}
If you compile your code now, there should be no errors. And if you run it, it should work exactly the same way it used to.
Now, what benefit did this give us? Well for starters, now we have simplified the logic in our input()
method. It may be a bit hard to read because there's 3 methods in one line, so that's a negative. But on the plus side, we are more loosely coupled. That helps us in a big way.
For example, your program has a minor formatting error. In the beginning, when you ask your user how many of each ingredient they want, your prompt comes out looking like this.
Please enter how many of each ingredient you have:
Apples1
Cherry2
Carrot3
Flour 4
Sugar 5
The number is smushed together with the word. If you wanted to clean that up using your old way, you would have to punch in a whole bunch of spaces like this.
public void input() { // user inputs ingredient counts
//Asks user for ingredients on-hand
System.out.println("Please enter how many of each ingredient you have:");
System.out.print(ingredientsNames[0] + " - "); //cleaned up now
int apples = kbd.nextInt();
setApples(apples);
System.out.print(ingredientsNames[1] + " - "); //but you must add it
int cherries = kbd.nextInt();
setCherries(cherries);
System.out.print(ingredientsNames[2] + " - "); //to each one of these
int carrots = kbd.nextInt();
setCarrots(carrots);
System.out.print(ingredientsNames[3] + " - "); //lines, which makes
int flour = kbd.nextInt();
setFlour(flour);
System.out.print(ingredientsNames[4] + " - "); //minor changes tedious
int sugar = kbd.nextInt();
setSugar(sugar);
}//end user ingredient input
Your output looks nice and pretty now -- Apples - 5
, but you needed to change 5 things. And if you ever want to use an =
instead of a -
, you'll need to make another 5 changes. And if you prefer spaces instead of a symbol, another 5 changes. And 5 is only because you have 5 ingredients. What if you had 50? That would be boring, tedious, and error-prone - making many repetitive changes is a great to accidentally mistype and cause a bug in your code.
But using the new way, you only need to make one tiny change in your UserPrompt
class.
public class UserPrompt
{
final Scanner kbd = new Scanner(System.in);
public int askForNumber(String prompt)
{
//Print your prompt to the command line -- I added a little extra at the end for simple reading
System.out.print(prompt + " - ");
return kbd.nextInt();
}
Now, all of your output looks clean, and if you ever want to change the way the output looks, you only need to make one change in UserPrompt
. And to be clear, the reason why this code is now loosely coupled is because you used a parameter to decide how the reading and writing goes, then used that parameter for the rest of your method. Since you are plugging in your reader/writer in as a parameter and using it in the rest of your method, that means you only need to change the parameter in order to change the whole method. And that is what loose coupling means - your components are plug-and-play, allowing you to change things quickly, without requiring you to make many changes elsewhere. All you had to do was plug in a parameter into the method, then define it in your main (and Selection).
That was a difficult topic, but it's a massive part of Software Design. And this is likely the benefits that @MadProgrammer was hinting towards when suggesting this idea to you in the comments. On to the next method.
public void IngredientCount()
- So as we mentioned earlier, tight coupling means limiting each class to doing as little as possible, and using plug and play to handle any extra functionality we may need. We printed and read stuff from the screen usingUserPrompt
, so it's not too much of a stretch to giveUserPrompt
the ability to just print. Like this.
public class UserPrompt
{
//ignore all the lines from the askForNumber method
//just add this method below that method, but still in UserPrompt
public void print(String message)
{
System.out.println(message);
}
}
Then, change the original method.
public void IngredientCount(UserPrompt prompt) { //Lists ingredient input counts
prompt.print("According to your input, you have:");
prompt.print("Ingredient\tValue");
for(int counter=0;counter<ingredients.length;counter++) {
prompt.print(ingredientsNames[counter] + "\t\t" + ingredients[counter]);
}//end for loop
}//end ingredient listing
Now, instead of using System.out.println()
to print, we can use UserPrompt
and use it's print()
method to print. As you can see, you use a lot of System.out.println()
in your program. I'll take the liberty of going ahead and replacing all of those with the print()
method in UserPrompt
. I will also replace all of your prompts (how many apples?) with the askForNumber()
method in UserPrompt
.
This is a very long, repetitive, and tedious change to make - and not something a beginner should waste their time doing. Frankly, you probably understand the concept at this point, so I will just do it for you. Furthermore, we are going to do this for the rest of the methods, so let me just knock them all out now in one fell swoop.
UNFORTUNATELY, I HAVE RUN OUT OF CHARACTERS, SO THIS WILL BE PART 1 of multiple.
Solution 2:[2]
PART 2
Ok, I have now replaced all System.out.println()
in your program to use the print()
method from UserPrompt
. Here it is.
import java.util.Scanner;
public interface Round1
{
public static void main (String [] args){
final ShowRecipes first = new ShowRecipes();
System.out.println("FIRST");
final UserPrompt prompt = new UserPrompt();
first.input(prompt);
first.compareRecipe(prompt);
first.WantToCook(prompt);
first.Selection(prompt);
}
public class UserPrompt
{
final Scanner kbd = new Scanner(System.in);
public int askForNumber(String prompt)
{
//Print your prompt to the command line -- I added a little extra at the end for simple reading
System.out.print(prompt + " - ");
return kbd.nextInt();
}
public void print(String message)
{
System.out.println(message);
}
}
//object class
public class ShowRecipes {
Scanner kbd = new Scanner(System.in);
//ingredient names array
public String ingredientsNames[] = {"Apples", "Cherry", "Carrot", "Flour ", "Sugar "};
//ingredients array - setters will set
public int[] ingredients = new int[5];
//Basic constructor
public ShowRecipes() {
ingredients[0] = 1; //apple
ingredients[1] = 1; //cherry
ingredients[2] = 1;//carrot
ingredients[3] = 0;//flour
ingredients[4] = 0;//sugar
}
//overloaded constructor that covers each field
public ShowRecipes(int apples, int cherries, int carrots, int flour, int sugar) {
System.out.println("overloaded");
setApples(apples);
setCherries(cherries);
setCarrots(carrots);
setFlour(flour);
setSugar(sugar);
}
public void input(UserPrompt prompt) { // user inputs ingredient counts
//Asks user for ingredients on-hand
prompt.print("Please enter how many of each ingredient you have:");
setApples(prompt.askForNumber(ingredientsNames[0]));
setCherries(prompt.askForNumber(ingredientsNames[1]));
setCarrots(prompt.askForNumber(ingredientsNames[2]));
setFlour(prompt.askForNumber(ingredientsNames[3]));
setSugar(prompt.askForNumber(ingredientsNames[4]));
}//end user ingredient input
public void IngredientCount(UserPrompt prompt) { //Lists ingredient input counts
prompt.print("According to your input, you have:");
prompt.print("Ingredient\tValue");
for(int counter=0;counter<ingredients.length;counter++) {
prompt.print(ingredientsNames[counter] + "\t\t" + ingredients[counter]);
}//end for loop
}//end ingredient listing
public void Selection(UserPrompt prompt){ //Can access every method in class except CookAgain
//1. Cook something -- WantToCook()
//2. See ingredient list -- IngredientCount()
//3. See available recipes -- compareRecipe()
//4. Modify total ingredient amounts -- input()
//maybe put this in ShowRecipes, create object for each to put in own classes
int userChoice =
prompt.askForNumber(
"What would you like to do now? Enter a number to select. \n1. Cook something \n2. See ingredients \n3. See available recipes \n4. Modify ingredient amounts"
);
switch (userChoice) {
case 1:
WantToCook(prompt);
break;
case 2:
IngredientCount(prompt);
break;
case 3:
compareRecipe(prompt);
break;
case 4:
input(prompt);
break;
default:
System.out.println("Invalid");
return;
}}
//Setters - set from input()
//Apple setter
public void setApples(int apples){
ingredients[0] = apples;
}
//Cherry setter
public void setCherries(int cherries){
ingredients[1] = cherries;
}
//Carrot setter
public void setCarrots(int carrots){
ingredients[2]= carrots;
}
//Flour setter
public void setFlour(int flour){
ingredients[3] = flour;
}
//Sugar setter
public void setSugar(int sugar){
ingredients[4] = sugar;
}
//Getters
//Apple getter
public int getApples(){
return ingredients[0];
}
//Cherry getter
public int getCherries(){
return ingredients[1];
}
//Carrot getter
public int getCarrots(){
return ingredients[2];
}
//Flour getter
public int getFlour(){
return ingredients[3];
}
//Sugar getter
public int getSugar(){
return ingredients[4];
}
public void compareRecipe(UserPrompt prompt) {
prompt.print("According to the input, you can make:");
//Apples
if (ingredients[0] >= 3) {
prompt.print(RecipeNamesA[0]); //Apple Jam
}
if (ingredients[0] >= 2) {
prompt.print(RecipeNamesA[1]); //Apple Jelly
prompt.print(RecipeNamesA[2]); //Apple Smoothie
}
if (ingredients[0] >= 1 && ingredients[3] >= 1){
prompt.print(RecipeNamesA[3]); //Apple Tart
}
if (ingredients[0] >= 2 && ingredients[4] >= 2 && ingredients[3] >= 1){
prompt.print(RecipeNamesA[4]); //Apple Pie
}
if (ingredients[0] == 0) {
prompt.print("No apple recipes");
}
//Cherries
if (ingredients[1] >= 3) {
prompt.print(RecipeNamesCh[0]); //Cherry Jam
}
if (ingredients[1] >= 2) {
prompt.print(RecipeNamesCh[1]); //Cherry Jelly
}
if (ingredients[1] >= 2 && ingredients[3] >= 3 && ingredients[4] >= 2) {
prompt.print(RecipeNamesCh[2]); //Cherry Pie
}
if (ingredients[1] >= 1 && ingredients[3] <= 1 && ingredients[4] >= 1) {
prompt.print(RecipeNamesCh[3]); //Cherry Tart
}
if (ingredients[1] == 0) {
prompt.print("No cherry recipes");
}
//Carrots
if (ingredients[2] >= 2) {
prompt.print(RecipeNamesCa[0]); //Carrot juice
}
if (ingredients[2] >= 2 && ingredients[3] >= 1) {
prompt.print(RecipeNamesCa[2]); //Carrot potage
}
if (ingredients[2] >= 1 && ingredients[3] >= 1 && ingredients[4] >= 1){
prompt.print(RecipeNamesCa[1]); //Carrot cake
}
if (ingredients[2] == 0) {
prompt.print("No carrot recipes");
}
}
private String RecipeNamesA[] = {"1. Apple Jam", "2. Apple Jelly", "3. Apple Smoothie", "4. Apple Tart", "5. Apple Pie"}; //Apple recipes
private String RecipeNamesCh[] = {"6. Cherry Jam", "7. Cherry Jelly", "8. Cherry Pie", "9. Cherry Tart"}; //Cherry recipes
private String RecipeNamesCa[] = {"10. Carrot Juice", "11. Carrot Cake","12. Carrot Potage"}; //Carrot recipes
public void WantToCook(UserPrompt prompt) { //Enter recipe number - deducts ingredients
int recipeNum = prompt.askForNumber("What would you like to cook? Please enter the recipe number.");
switch(recipeNum) {
case 1: //Apple jam
setApples(getApples()-3);
prompt.print(RecipeNamesA[0]);
break;
case 2: //Apple jelly
prompt.print(RecipeNamesA[1]);
setApples(getApples()-2);
break;
case 3://Apple Smoothie
prompt.print(RecipeNamesA[2]);
setApples(getApples()-2);
break;
case 4: //Apple Tart
prompt.print(RecipeNamesA[3]);
setApples(getApples()-1);
setFlour(getFlour()-1);
setSugar(getSugar()-1);
break;
case 5: //Apple Pie
prompt.print(RecipeNamesA[4]);
setApples(getApples()-2);
setFlour(getFlour()-3);
setSugar(getSugar()-2);
break;
case 6: //Cherry Jam
prompt.print(RecipeNamesCh[0]);
setCherries(getCherries()-3);
break;
case 7: //Cherry Jelly
prompt.print(RecipeNamesCh[1]);
setCherries(getCherries()-2);
break;
case 8: //Cherry Pie
prompt.print(RecipeNamesCh[2]);
setCherries(getCherries()-2);
setFlour(getFlour()-3);
setSugar(getSugar()-2);
break;
case 9: //Cherry Tart
prompt.print(RecipeNamesCh[3]);
setCherries(getCherries()-1);
setFlour(getFlour()-1);
setSugar(getSugar()-1);
break;
case 10: //Carrot juice
prompt.print(RecipeNamesCa[0]);
setCarrots(getCarrots()-2);
break;
case 11: //Carrot cake
prompt.print(RecipeNamesCa[1]);
setCarrots(getCarrots()-1);
setFlour(getFlour()-1);
setSugar(getSugar()-1);
break;
case 12: //Carrot potage
prompt.print((RecipeNamesCa[2]));
setCarrots(getCarrots()-2);
setFlour(getFlour()-1);
break;
default:
prompt.print("Invalid entry.");
}//End recipeNum switch
prompt.print("You have cooked a thing! Good job!");
CookAgain(prompt); //cook again? prompt
} //end WantToCook method
public void CookAgain(UserPrompt prompt) {
System.out.print("Would you like to cook again? Please type yes or no - ");
String answer = kbd.next();
switch (answer) {
case "yes":
if (ingredients[0] >= 1 || ingredients[1] >= 1 || ingredients[2] >= 1) { //no apples, cherries, or carrots
prompt.print("You've used some ingredients. Let's see what you have now.");
IngredientCount(prompt);
compareRecipe(prompt);
WantToCook(prompt);
break;
}
else {
prompt.print("You don't have enough ingredients left to cook anything.");
break;
}
case "no":
prompt.print("Ok, done cooking. Enjoy!");
break;
default:
prompt.print("Invalid answer please try again!");
}// End answer switch
kbd.close(); // close scanner
}
}
}
Now, what does this actually do for us? That was a lot of change, but what is the benefit?
Well, for starters, we have removed all of the hardcoded print statements, and wrapped them all under a single method print()
. Same for askForNumber()
.
Now, I could explain why that is powerful, or I could show it to you. Try running this new class above like normal. Then, completely replace the UserPrompt
class above with this new own below.
//this time, replace the whole UserPrompt class with what is here
public class UserPrompt
{
//final Scanner kbd = new Scanner(System.in);
public int askForNumber(String prompt)
{
//System.out.print(prompt + " - ");
//
//return kbd.nextInt();
String message = javax.swing.JOptionPane.showInputDialog(null, prompt + " - ");
return Integer.parseInt(message);
}
public void print(String message)
{
//System.out.println(message);
javax.swing.JOptionPane.showMessageDialog(null, message);
}
}
Now try running the class. You should see something very cool. Now, your application has windows and popups and buttons. It may not look very pretty, but hopefully this shows you how making your code loosely coupled instead of tightly coupled can allow some powerful abilities.
Now, change back the UserPrompt
to the boring command line version, and let's continue. Last we checked, we were looking at IngredientCount
and decided to replace all of the print statements with the print()
method in UserPrompt
. So now, let's look at the next method, Selection()
.
Selection()
- Obviously, we have removed coupling with print and read, the prompt at the beginning of this method is considered a loose coupling. As for the rest of this method, it actually only does one simple thing - decides what to do based on what the user requests. On the one hand, this method does not need to be broken up at all. It is exactly as large as it needs to be and does only one thing on its own. On the other hand, this method seems like it should be in a different class based on the methods we've looked at previously. For example, in thepublic ShowRecipes()
method, it would take in inputs and store them into state. That was purely data manipulation/changing state.
Whereas for this method, we are using the input to make branching decisions. One could say that this sounds more like a Driver class or a BusinessLogic class. I will make a class called Driver
, and put this method into there.
public class Driver
{
public void selection(UserPrompt prompt){ //Can access every method in class except CookAgain
//1. Cook something -- WantToCook()
//2. See ingredient list -- IngredientCount()
//3. See available recipes -- compareRecipe()
//4. Modify total ingredient amounts -- input()
//maybe put this in ShowRecipes, create object for each to put in own classes
int userChoice =
prompt.askForNumber(
"What would you like to do now? Enter a number to select. \n1. Cook something \n2. See ingredients \n3. See available recipes \n4. Modify ingredient amounts"
);
switch (userChoice) {
case 1:
WantToCook(prompt);
break;
case 2:
IngredientCount(prompt);
break;
case 3:
compareRecipe(prompt);
break;
case 4:
input(prompt);
break;
default:
System.out.println("Invalid");
return;
}
}
}
Now, immediately, several things break. The class is now trying to find all these methods that belong to ShowRecipes
, but can't find them because we are now in the Driver
class. To make things simple, we are actually going to do something rather drastic - we are going to copy all of those methods over into the Driver
class. And the reason we are doing this is because all of these methods actually have the same sort of style of decide what to do based upon user input. In fact, let's take a quick look at each.
WantToCook()
- depending on what the user says, we manipulate state, then print to the screen our response. So this does 3 things - decides what to do, then manipulates state, then prints to the screen.IngredientCount()
- This one reads the state, then prints it out the state to the user. So this one does 2 things - reads state, then prints it out to the user.compareRecipe()
- This one reads the state, then prints it out to the user. So this does 2 things - reads state, then prints it out to the user.input()
- depending on what the user says, we populate state. So this does 2 things - decides what to do, then populates state.
So, looking at these, we see 2 classes of methods - one that decides then does something, and another that reads then does something. So far, the Driver class definitely falls under the decides then does something mindset, and so does selection()
, so we will bring WantToCook()
and input()
over to the Driver
class as well.
Now your Driver
class should look like this.
public class Driver
{
public void selection(UserPrompt prompt){ //Can access every method in class except CookAgain
//1. Cook something -- WantToCook()
//2. See ingredient list -- IngredientCount()
//3. See available recipes -- compareRecipe()
//4. Modify total ingredient amounts -- input()
//maybe put this in ShowRecipes, create object for each to put in own classes
int userChoice =
prompt.askForNumber(
"What would you like to do now? Enter a number to select. \n1. Cook something \n2. See ingredients \n3. See available recipes \n4. Modify ingredient amounts"
);
switch (userChoice) {
case 1:
WantToCook(prompt);
break;
case 2:
IngredientCount(prompt);
break;
case 3:
compareRecipe(prompt);
break;
case 4:
input(prompt);
break;
default:
System.out.println("Invalid");
return;
}
}
public void WantToCook(UserPrompt prompt) { //Enter recipe number - deducts ingredients
int recipeNum = prompt.askForNumber("What would you like to cook? Please enter the recipe number.");
switch(recipeNum) {
case 1: //Apple jam
setApples(getApples()-3);
prompt.print(RecipeNamesA[0]);
break;
case 2: //Apple jelly
prompt.print(RecipeNamesA[1]);
setApples(getApples()-2);
break;
case 3://Apple Smoothie
prompt.print(RecipeNamesA[2]);
setApples(getApples()-2);
break;
case 4: //Apple Tart
prompt.print(RecipeNamesA[3]);
setApples(getApples()-1);
setFlour(getFlour()-1);
setSugar(getSugar()-1);
break;
case 5: //Apple Pie
prompt.print(RecipeNamesA[4]);
setApples(getApples()-2);
setFlour(getFlour()-3);
setSugar(getSugar()-2);
break;
case 6: //Cherry Jam
prompt.print(RecipeNamesCh[0]);
setCherries(getCherries()-3);
break;
case 7: //Cherry Jelly
prompt.print(RecipeNamesCh[1]);
setCherries(getCherries()-2);
break;
case 8: //Cherry Pie
prompt.print(RecipeNamesCh[2]);
setCherries(getCherries()-2);
setFlour(getFlour()-3);
setSugar(getSugar()-2);
break;
case 9: //Cherry Tart
prompt.print(RecipeNamesCh[3]);
setCherries(getCherries()-1);
setFlour(getFlour()-1);
setSugar(getSugar()-1);
break;
case 10: //Carrot juice
prompt.print(RecipeNamesCa[0]);
setCarrots(getCarrots()-2);
break;
case 11: //Carrot cake
prompt.print(RecipeNamesCa[1]);
setCarrots(getCarrots()-1);
setFlour(getFlour()-1);
setSugar(getSugar()-1);
break;
case 12: //Carrot potage
prompt.print((RecipeNamesCa[2]));
setCarrots(getCarrots()-2);
setFlour(getFlour()-1);
break;
default:
prompt.print("Invalid entry.");
}//End recipeNum switch
prompt.print("You have cooked a thing! Good job!");
CookAgain(prompt); //cook again? prompt
} //end WantToCook method
public void input(UserPrompt prompt) { // user inputs ingredient counts
//Asks user for ingredients on-hand
prompt.print("Please enter how many of each ingredient you have:");
setApples(prompt.askForNumber(ingredientsNames[0]));
setCherries(prompt.askForNumber(ingredientsNames[1]));
setCarrots(prompt.askForNumber(ingredientsNames[2]));
setFlour(prompt.askForNumber(ingredientsNames[3]));
setSugar(prompt.askForNumber(ingredientsNames[4]));
}//end user ingredient input
}
Now, if you try compiling at this point, you will see a scary list of errors. That is because we have not finished making our changes, so it is not yet a reason to be concerned.
Now, actually looking at the errors, the first one we will see is for the main method.
Round1.java:13: error: cannot find symbol
first.input(prompt);
^
symbol: method input(UserPrompt)
location: variable first of type ShowRecipes
It's complaining that it can't find the method input()
inside of ShowRecipes()
. Well, that is because we just took that method out and placed it into the Driver
class. So, we just need to update the main method to call input()
on the driver
instance of the Driver
class.
//replace the whole method
public static void main (String [] args){
final ShowRecipes first = new ShowRecipes();
System.out.println("FIRST");
final UserPrompt prompt = new UserPrompt();
final Driver driver = new Driver();
driver.input(prompt);
first.compareRecipe(prompt);
first.WantToCook(prompt);
driver.selection(prompt);
}
Ok, that should have removed that one error. We will see a very similar one for the WantToCook()
method. We can do the same, resulting in the method below.
public static void main (String [] args){
final ShowRecipes first = new ShowRecipes();
System.out.println("FIRST");
final UserPrompt prompt = new UserPrompt();
final Driver driver = new Driver();
driver.input(prompt);
first.compareRecipe(prompt);
driver.WantToCook(prompt);
driver.selection(prompt);
}
Now, there should be no more errors in our main method. Of course, there are still plenty to work through. Let's look at the next error.
Round1.java:67: error: cannot find symbol
IngredientCount(prompt);
^
symbol: method IngredientCount(UserPrompt)
location: class Driver
This error says that it can't find the IngredientCount()
method inside of Driver
. That is because the IngredientCount()
method is in ShowRecipes
. And spoiler alert - there's going to be more errors saying almost the exact same thing, but for methods with different names. So, to make our lives easier, let's tackle all of these errors at once. To do this, we are going to plug in our instance of ShowRecipes
into the Driver
class using a constructor. Again, this ties back to what @MadProgrammer was saying earlier, but this time, it is going to be done through a constructor instead of a method.
First let's modify the Driver
class to include a new constructor and an instance field of ShowRecipes
.
public class Driver
{
ShowRecipes sr;
public Driver(ShowRecipes parameter)
{
sr = parameter;
}
//ignore all the lines after this
}
By doing this, we have added a small amount of state to the Driver
class. Now, this might seem counter productive - didn't we create the Driver
class because it wasn't supposed to make decisions and handle state? Well, yes, but there is a big difference this time. All of the state changes are going to be handled by the new ShowRecipes
that we added inside of Driver
. So Driver
won't actually be doing any of the state changes itself, it will simply have whatever state it needs to know about be plugged in, then the Driver
class can tell it to manipulate itself. Again, the keywords here a plug-in and delegate.
Now, the first error that comes up when we compile is back in the main method. This is easy to fix, just change it to be like this.
public static void main (String [] args){
//ignore the lines before this one
final Driver driver = new Driver(first);
//ignore the lines after this one
}
Once we do this, then driver
has first
plugged into it, allowing Driver
to delegate all state changes to ShowRecipes
. Now, let's start reworking some of the methods in Driver
to use it.
For example, in the selection()
method earlier, it couldn't find IngredientCount()
and compareRecipe()
. But now that we have plugged in ShowRecipes
, let's change selection()
to use ShowRecipes
for those methods.
public void selection(UserPrompt prompt){
//ignore the lines before this one
case 2:
sr.IngredientCount(prompt);
break;
case 3:
sr.compareRecipe(prompt);
break;
//ignore the lines after this one
}
RAN OUT OF CHARACTERS, SO CONTINUED IN PART 3
Solution 3:[3]
PART 3
No more errors in selection()
. Now, the next method with errors is WantToCook()
. Let's do the same there. Any method that currently belongs to ShowRecipes
, we will put an sr
in front of it, in order to delegate that method to the ShowRecipes
instance that the Driver
class has inside of it.
public void WantToCook(UserPrompt prompt) { //Enter recipe number - deducts ingredients
int recipeNum = prompt.askForNumber("What would you like to cook? Please enter the recipe number.");
switch(recipeNum) {
case 1: //Apple jam
sr.setApples(sr.getApples()-3);
prompt.print(sr.RecipeNamesA[0]);
break;
case 2: //Apple jelly
prompt.print(sr.RecipeNamesA[1]);
sr.setApples(sr.getApples()-2);
break;
case 3://Apple Smoothie
prompt.print(sr.RecipeNamesA[2]);
sr.setApples(sr.getApples()-2);
break;
case 4: //Apple Tart
prompt.print(sr.RecipeNamesA[3]);
sr.setApples(sr.getApples()-1);
sr.setFlour(sr.getFlour()-1);
sr.setSugar(sr.getSugar()-1);
break;
case 5: //Apple Pie
prompt.print(sr.RecipeNamesA[4]);
sr.setApples(sr.getApples()-2);
sr.setFlour(sr.getFlour()-3);
sr.setSugar(sr.getSugar()-2);
break;
case 6: //Cherry Jam
prompt.print(sr.RecipeNamesCh[0]);
sr.setCherries(sr.getCherries()-3);
break;
case 7: //Cherry Jelly
prompt.print(sr.RecipeNamesCh[1]);
sr.setCherries(sr.getCherries()-2);
break;
case 8: //Cherry Pie
prompt.print(sr.RecipeNamesCh[2]);
sr.setCherries(sr.getCherries()-2);
sr.setFlour(sr.getFlour()-3);
sr.setSugar(sr.getSugar()-2);
break;
case 9: //Cherry Tart
prompt.print(sr.RecipeNamesCh[3]);
sr.setCherries(sr.getCherries()-1);
sr.setFlour(sr.getFlour()-1);
sr.setSugar(sr.getSugar()-1);
break;
case 10: //Carrot juice
prompt.print(sr.RecipeNamesCa[0]);
sr.setCarrots(sr.getCarrots()-2);
break;
case 11: //Carrot cake
prompt.print(sr.RecipeNamesCa[1]);
sr.setCarrots(sr.getCarrots()-1);
sr.setFlour(sr.getFlour()-1);
sr.setSugar(sr.getSugar()-1);
break;
case 12: //Carrot potage
prompt.print((sr.RecipeNamesCa[2]));
sr.setCarrots(sr.getCarrots()-2);
sr.setFlour(sr.getFlour()-1);
break;
default:
prompt.print("Invalid entry.");
}//End recipeNum switch
prompt.print("You have cooked a thing! Good job!");
sr.CookAgain(prompt); //cook again? prompt
} //end WantToCook method
Pretty long winded, but this eliminates all the errors in the WantToCook()
method.
Now the next error is in input()
. It is the exact same type of problem, and thus, we solve it the same way.
public void input(UserPrompt prompt) { // user inputs ingredient counts
//Asks user for ingredients on-hand
prompt.print("Please enter how many of each ingredient you have:");
sr.setApples(prompt.askForNumber(sr.ingredientsNames[0]));
sr.setCherries(prompt.askForNumber(sr.ingredientsNames[1]));
sr.setCarrots(prompt.askForNumber(sr.ingredientsNames[2]));
sr.setFlour(prompt.askForNumber(sr.ingredientsNames[3]));
sr.setSugar(prompt.askForNumber(sr.ingredientsNames[4]));
}//end user ingredient input
And now, all the errors in Driver
are gone. But if we compile, now we have some errors in ShowRecipes
.
Round1.java:345: error: cannot find symbol
WantToCook(prompt);
^
symbol: method WantToCook(UserPrompt)
location: class ShowRecipes
This error is saying that it cannot find WantToCook()
inside of the ShowRecipes
method called CookAgain()
.
Now, WantToCook()
exists in Driver
. We just moved it there because we felt that it was handling a Driver
-like, decision-making problem rather than the other methods in ShowRecipes
. However, because of that, ShowRecipes
is broken because it can't find it.
You might be tempted to reference Driver
inside of ShowRecipes
, which is the vice versa of what we just did - putting ShowRecipes
inside of Driver
. However, that would not be the correct move here. And the reason why is because CookAgain()
is actually a decision-making method too. We didn't move it before only because we hadn't gotten that far yet. But now that we are here, this method needs to go over to Driver
because it is a decision-making based method.
I just cut and pasted the method with no change into Driver
. However, once we do that, we actually need to undo a tiny bit of work that we did before in the WantToCook()
. Here is the change.
public void WantToCook(UserPrompt prompt) {
//ignore the lines before this one
CookAgain(prompt); //cook again? prompt
} //end WantToCook method
Now that we have moved CookAgain
into Driver
, that means it no longer belongs to ShowRecipes
, and by extension, sr
. So, we removed the sr
from this line only.
Now that that is done, we have one more error in the Driver
code. Basically, it looks like there is one point in your program where, instead of asking for a number, you asked for a yes or no. For right now, just to get things working, I am going to change it to use numbers. We can change that later after things are all cleaned up.
public void CookAgain(UserPrompt prompt) {
int answer = prompt.askForNumber("Would you like to cook again? 1 = yes, 2 = no - ");
switch (answer) {
case 1: //which means yes
if (ingredients[0] >= 1 || ingredients[1] >= 1 || ingredients[2] >= 1) { //no apples, cherries, or carrots
prompt.print("You've used some ingredients. Let's see what you have now.");
IngredientCount(prompt);
compareRecipe(prompt);
WantToCook(prompt);
break;
}
else {
prompt.print("You don't have enough ingredients left to cook anything.");
break;
}
case 2: //which means no
prompt.print("Ok, done cooking. Enjoy!");
break;
default:
prompt.print("Invalid answer please try again!");
}// End answer switch
}
Now that that is done, we can go back to what we were doing before - delegating methods to sr
that belong to sr
. Here are those changes.
public void CookAgain(UserPrompt prompt) {
int answer = prompt.askForNumber("Would you like to cook again? 1 = yes, 2 = no - ");
switch (answer) {
case 1: //which means yes
if (sr.ingredients[0] >= 1 || sr.ingredients[1] >= 1 || sr.ingredients[2] >= 1) { //no apples, cherries, or carrots
prompt.print("You've used some ingredients. Let's see what you have now.");
sr.IngredientCount(prompt);
sr.compareRecipe(prompt);
WantToCook(prompt);
break;
}
else {
prompt.print("You don't have enough ingredients left to cook anything.");
break;
}
case 2: //which means no
prompt.print("Ok, done cooking. Enjoy!");
break;
default:
prompt.print("Invalid answer please try again!");
}// End answer switch
}
And finally, no more errors. You can run your program again.
If you lost track of things, here are the changes made thus far.
import java.util.Scanner;
public interface Round1
{
public static void main (String [] args){
final ShowRecipes first = new ShowRecipes();
System.out.println("FIRST");
final UserPrompt prompt = new UserPrompt();
final Driver driver = new Driver(first);
driver.input(prompt);
first.compareRecipe(prompt);
driver.WantToCook(prompt);
driver.selection(prompt);
}
public class UserPrompt
{
final Scanner kbd = new Scanner(System.in);
public int askForNumber(String prompt)
{
//Print your prompt to the command line -- I added a little extra at the end for simple reading
System.out.print(prompt + " - ");
return kbd.nextInt();
}
public void print(String message)
{
System.out.println(message);
}
}
public class Driver
{
ShowRecipes sr;
public Driver(ShowRecipes parameter)
{
sr = parameter;
}
public void selection(UserPrompt prompt){ //Can access every method in class except CookAgain
//1. Cook something -- WantToCook()
//2. See ingredient list -- IngredientCount()
//3. See available recipes -- compareRecipe()
//4. Modify total ingredient amounts -- input()
//maybe put this in ShowRecipes, create object for each to put in own classes
int userChoice =
prompt.askForNumber(
"What would you like to do now? Enter a number to select. \n1. Cook something \n2. See ingredients \n3. See available recipes \n4. Modify ingredient amounts"
);
switch (userChoice) {
case 1:
WantToCook(prompt);
break;
case 2:
sr.IngredientCount(prompt);
break;
case 3:
sr.compareRecipe(prompt);
break;
case 4:
input(prompt);
break;
default:
System.out.println("Invalid");
return;
}
}
public void WantToCook(UserPrompt prompt) { //Enter recipe number - deducts ingredients
int recipeNum = prompt.askForNumber("What would you like to cook? Please enter the recipe number.");
switch(recipeNum) {
case 1: //Apple jam
sr.setApples(sr.getApples()-3);
prompt.print(sr.RecipeNamesA[0]);
break;
case 2: //Apple jelly
prompt.print(sr.RecipeNamesA[1]);
sr.setApples(sr.getApples()-2);
break;
case 3://Apple Smoothie
prompt.print(sr.RecipeNamesA[2]);
sr.setApples(sr.getApples()-2);
break;
case 4: //Apple Tart
prompt.print(sr.RecipeNamesA[3]);
sr.setApples(sr.getApples()-1);
sr.setFlour(sr.getFlour()-1);
sr.setSugar(sr.getSugar()-1);
break;
case 5: //Apple Pie
prompt.print(sr.RecipeNamesA[4]);
sr.setApples(sr.getApples()-2);
sr.setFlour(sr.getFlour()-3);
sr.setSugar(sr.getSugar()-2);
break;
case 6: //Cherry Jam
prompt.print(sr.RecipeNamesCh[0]);
sr.setCherries(sr.getCherries()-3);
break;
case 7: //Cherry Jelly
prompt.print(sr.RecipeNamesCh[1]);
sr.setCherries(sr.getCherries()-2);
break;
case 8: //Cherry Pie
prompt.print(sr.RecipeNamesCh[2]);
sr.setCherries(sr.getCherries()-2);
sr.setFlour(sr.getFlour()-3);
sr.setSugar(sr.getSugar()-2);
break;
case 9: //Cherry Tart
prompt.print(sr.RecipeNamesCh[3]);
sr.setCherries(sr.getCherries()-1);
sr.setFlour(sr.getFlour()-1);
sr.setSugar(sr.getSugar()-1);
break;
case 10: //Carrot juice
prompt.print(sr.RecipeNamesCa[0]);
sr.setCarrots(sr.getCarrots()-2);
break;
case 11: //Carrot cake
prompt.print(sr.RecipeNamesCa[1]);
sr.setCarrots(sr.getCarrots()-1);
sr.setFlour(sr.getFlour()-1);
sr.setSugar(sr.getSugar()-1);
break;
case 12: //Carrot potage
prompt.print((sr.RecipeNamesCa[2]));
sr.setCarrots(sr.getCarrots()-2);
sr.setFlour(sr.getFlour()-1);
break;
default:
prompt.print("Invalid entry.");
}//End recipeNum switch
prompt.print("You have cooked a thing! Good job!");
CookAgain(prompt); //cook again? prompt
} //end WantToCook method
public void input(UserPrompt prompt) { // user inputs ingredient counts
//Asks user for ingredients on-hand
prompt.print("Please enter how many of each ingredient you have:");
sr.setApples(prompt.askForNumber(sr.ingredientsNames[0]));
sr.setCherries(prompt.askForNumber(sr.ingredientsNames[1]));
sr.setCarrots(prompt.askForNumber(sr.ingredientsNames[2]));
sr.setFlour(prompt.askForNumber(sr.ingredientsNames[3]));
sr.setSugar(prompt.askForNumber(sr.ingredientsNames[4]));
}//end user ingredient input
public void CookAgain(UserPrompt prompt) {
int answer = prompt.askForNumber("Would you like to cook again? 1 = yes, 2 = no - ");
switch (answer) {
case 1: //which means yes
if (sr.ingredients[0] >= 1 || sr.ingredients[1] >= 1 || sr.ingredients[2] >= 1) { //no apples, cherries, or carrots
prompt.print("You've used some ingredients. Let's see what you have now.");
sr.IngredientCount(prompt);
sr.compareRecipe(prompt);
WantToCook(prompt);
break;
}
else {
prompt.print("You don't have enough ingredients left to cook anything.");
break;
}
case 2: //which means no
prompt.print("Ok, done cooking. Enjoy!");
break;
default:
prompt.print("Invalid answer please try again!");
}// End answer switch
}
}
//object class
public class ShowRecipes {
Scanner kbd = new Scanner(System.in);
//ingredient names array
public String ingredientsNames[] = {"Apples", "Cherry", "Carrot", "Flour ", "Sugar "};
//ingredients array - setters will set
public int[] ingredients = new int[5];
//Basic constructor
public ShowRecipes() {
ingredients[0] = 1; //apple
ingredients[1] = 1; //cherry
ingredients[2] = 1;//carrot
ingredients[3] = 0;//flour
ingredients[4] = 0;//sugar
}
//overloaded constructor that covers each field
public ShowRecipes(int apples, int cherries, int carrots, int flour, int sugar) {
System.out.println("overloaded");
setApples(apples);
setCherries(cherries);
setCarrots(carrots);
setFlour(flour);
setSugar(sugar);
}
public void IngredientCount(UserPrompt prompt) { //Lists ingredient input counts
prompt.print("According to your input, you have:");
prompt.print("Ingredient\tValue");
for(int counter=0;counter<ingredients.length;counter++) {
prompt.print(ingredientsNames[counter] + "\t\t" + ingredients[counter]);
}//end for loop
}//end ingredient listing
//Setters - set from input()
//Apple setter
public void setApples(int apples){
ingredients[0] = apples;
}
//Cherry setter
public void setCherries(int cherries){
ingredients[1] = cherries;
}
//Carrot setter
public void setCarrots(int carrots){
ingredients[2]= carrots;
}
//Flour setter
public void setFlour(int flour){
ingredients[3] = flour;
}
//Sugar setter
public void setSugar(int sugar){
ingredients[4] = sugar;
}
//Getters
//Apple getter
public int getApples(){
return ingredients[0];
}
//Cherry getter
public int getCherries(){
return ingredients[1];
}
//Carrot getter
public int getCarrots(){
return ingredients[2];
}
//Flour getter
public int getFlour(){
return ingredients[3];
}
//Sugar getter
public int getSugar(){
return ingredients[4];
}
public void compareRecipe(UserPrompt prompt) {
prompt.print("According to the input, you can make:");
//Apples
if (ingredients[0] >= 3) {
prompt.print(RecipeNamesA[0]); //Apple Jam
}
if (ingredients[0] >= 2) {
prompt.print(RecipeNamesA[1]); //Apple Jelly
prompt.print(RecipeNamesA[2]); //Apple Smoothie
}
if (ingredients[0] >= 1 && ingredients[3] >= 1){
prompt.print(RecipeNamesA[3]); //Apple Tart
}
if (ingredients[0] >= 2 && ingredients[4] >= 2 && ingredients[3] >= 1){
prompt.print(RecipeNamesA[4]); //Apple Pie
}
if (ingredients[0] == 0) {
prompt.print("No apple recipes");
}
//Cherries
if (ingredients[1] >= 3) {
prompt.print(RecipeNamesCh[0]); //Cherry Jam
}
if (ingredients[1] >= 2) {
prompt.print(RecipeNamesCh[1]); //Cherry Jelly
}
if (ingredients[1] >= 2 && ingredients[3] >= 3 && ingredients[4] >= 2) {
prompt.print(RecipeNamesCh[2]); //Cherry Pie
}
if (ingredients[1] >= 1 && ingredients[3] <= 1 && ingredients[4] >= 1) {
prompt.print(RecipeNamesCh[3]); //Cherry Tart
}
if (ingredients[1] == 0) {
prompt.print("No cherry recipes");
}
//Carrots
if (ingredients[2] >= 2) {
prompt.print(RecipeNamesCa[0]); //Carrot juice
}
if (ingredients[2] >= 2 && ingredients[3] >= 1) {
prompt.print(RecipeNamesCa[2]); //Carrot potage
}
if (ingredients[2] >= 1 && ingredients[3] >= 1 && ingredients[4] >= 1){
prompt.print(RecipeNamesCa[1]); //Carrot cake
}
if (ingredients[2] == 0) {
prompt.print("No carrot recipes");
}
}
private String RecipeNamesA[] = {"1. Apple Jam", "2. Apple Jelly", "3. Apple Smoothie", "4. Apple Tart", "5. Apple Pie"}; //Apple recipes
private String RecipeNamesCh[] = {"6. Cherry Jam", "7. Cherry Jelly", "8. Cherry Pie", "9. Cherry Tart"}; //Cherry recipes
private String RecipeNamesCa[] = {"10. Carrot Juice", "11. Carrot Cake","12. Carrot Potage"}; //Carrot recipes
}
}
You can copy and paste the above and run it, and you should see that the program runs.
Now that that is done, let's finalize cleaning up the tight coupling (as best as we can), then we can move onto the remaining 3 points (global state, complex methods, and enums).
So far, we have taken care of the following methods.
ShowRecipes()
input()
IngredientCount()
selection()
Now, I know we addressed other methods, but we only moved them around, and made the modifications necessary to work. We didn't actually focus on making them loosely coupled, so let's continue with our list.
Now, the next bunch of methods are very similar, so I will handle them as a whole. These next batch of methods are your setters and getters. Now, these methods do literally nothing more than manipulate state, so the methods themselves aren't wrong. That said, seeing these methods lends us to one interesting observation - in some points of your code, you use getters and setters to read/modify state, but in other parts of your code, you just modify the instance fields directly.
The act of modifying instance fields directly is a great example of tight-coupling. What happens if you decide to one day use a List<T>
instead of an int[]
to hold your ingredients? Well, any method that uses setters and getters will be fine, since you can just update the setters and getters alone. But, the code that directly manipulates state will be in trouble, because now they all have to change to handle a List<T>
instead of an int[]
.
Now, to keep ourselves honest, I am actually going to put all of these classes into separate files from now on. As you might have noticed, I have wrapped all of this into a giant interface called Round1
. This helps keep things together in one bundle, but it also allows us to cheat and just edit anything contained within the interface, even private variables.
So, I will put the Driver
class into a Driver.java
file, ShowRecipes
class into ShowRecipes.java
, and then the UserPrompt
class into UserPrompt.java
. And to keep things even simpler, I will add the main method inside of the Driver
class. Also, to prevent errors or file naming problems, I will rename ShowRecipes
to be ShowRecipesNew
. I have updated all of the code to reflect this change.
Your code base should now look like this.
Driver.java
public class Driver
{
ShowRecipesNew sr;
public Driver(ShowRecipesNew parameter)
{
sr = parameter;
}
public static void main (String [] args){
final ShowRecipesNew first = new ShowRecipesNew();
System.out.println("FIRST");
final UserPrompt prompt = new UserPrompt();
final Driver driver = new Driver(first);
driver.input(prompt);
first.compareRecipe(prompt);
driver.WantToCook(prompt);
driver.selection(prompt);
}
public void selection(UserPrompt prompt){ //Can access every method in class except CookAgain
//1. Cook something -- WantToCook()
//2. See ingredient list -- IngredientCount()
//3. See available recipes -- compareRecipe()
//4. Modify total ingredient amounts -- input()
//maybe put this in ShowRecipes, create object for each to put in own classes
int userChoice =
prompt.askForNumber(
"What would you like to do now? Enter a number to select. \n1. Cook something \n2. See ingredients \n3. See available recipes \n4. Modify ingredient amounts"
);
switch (userChoice) {
case 1:
WantToCook(prompt);
break;
case 2:
sr.IngredientCount(prompt);
break;
case 3:
sr.compareRecipe(prompt);
break;
case 4:
input(prompt);
break;
default:
System.out.println("Invalid");
return;
}
}
public void WantToCook(UserPrompt prompt) { //Enter recipe number - deducts ingredients
int recipeNum = prompt.askForNumber("What would you like to cook? Please enter the recipe number.");
switch(recipeNum) {
case 1: //Apple jam
sr.setApples(sr.getApples()-3);
prompt.print(sr.RecipeNamesA[0]);
break;
case 2: //Apple jelly
prompt.print(sr.RecipeNamesA[1]);
sr.setApples(sr.getApples()-2);
break;
case 3://Apple Smoothie
prompt.print(sr.RecipeNamesA[2]);
sr.setApples(sr.getApples()-2);
break;
case 4: //Apple Tart
prompt.print(sr.RecipeNamesA[3]);
sr.setApples(sr.getApples()-1);
sr.setFlour(sr.getFlour()-1);
sr.setSugar(sr.getSugar()-1);
break;
case 5: //Apple Pie
prompt.print(sr.RecipeNamesA[4]);
sr.setApples(sr.getApples()-2);
sr.setFlour(sr.getFlour()-3);
sr.setSugar(sr.getSugar()-2);
break;
case 6: //Cherry Jam
prompt.print(sr.RecipeNamesCh[0]);
sr.setCherries(sr.getCherries()-3);
break;
case 7: //Cherry Jelly
prompt.print(sr.RecipeNamesCh[1]);
sr.setCherries(sr.getCherries()-2);
break;
case 8: //Cherry Pie
prompt.print(sr.RecipeNamesCh[2]);
sr.setCherries(sr.getCherries()-2);
sr.setFlour(sr.getFlour()-3);
sr.setSugar(sr.getSugar()-2);
break;
case 9: //Cherry Tart
prompt.print(sr.RecipeNamesCh[3]);
sr.setCherries(sr.getCherries()-1);
sr.setFlour(sr.getFlour()-1);
sr.setSugar(sr.getSugar()-1);
break;
case 10: //Carrot juice
prompt.print(sr.RecipeNamesCa[0]);
sr.setCarrots(sr.getCarrots()-2);
break;
case 11: //Carrot cake
prompt.print(sr.RecipeNamesCa[1]);
sr.setCarrots(sr.getCarrots()-1);
sr.setFlour(sr.getFlour()-1);
sr.setSugar(sr.getSugar()-1);
break;
case 12: //Carrot potage
prompt.print((sr.RecipeNamesCa[2]));
sr.setCarrots(sr.getCarrots()-2);
sr.setFlour(sr.getFlour()-1);
break;
default:
prompt.print("Invalid entry.");
}//End recipeNum switch
prompt.print("You have cooked a thing! Good job!");
CookAgain(prompt); //cook again? prompt
} //end WantToCook method
public void input(UserPrompt prompt) { // user inputs ingredient counts
//Asks user for ingredients on-hand
prompt.print("Please enter how many of each ingredient you have:");
sr.setApples(prompt.askForNumber(sr.ingredientsNames[0]));
sr.setCherries(prompt.askForNumber(sr.ingredientsNames[1]));
sr.setCarrots(prompt.askForNumber(sr.ingredientsNames[2]));
sr.setFlour(prompt.askForNumber(sr.ingredientsNames[3]));
sr.setSugar(prompt.askForNumber(sr.ingredientsNames[4]));
}//end user ingredient input
public void CookAgain(UserPrompt prompt) {
int answer = prompt.askForNumber("Would you like to cook again? 1 = yes, 2 = no - ");
switch (answer) {
case 1: //which means yes
if (sr.ingredients[0] >= 1 || sr.ingredients[1] >= 1 || sr.ingredients[2] >= 1) { //no apples, cherries, or carrots
prompt.print("You've used some ingredients. Let's see what you have now.");
sr.IngredientCount(prompt);
sr.compareRecipe(prompt);
WantToCook(prompt);
break;
}
else {
prompt.print("You don't have enough ingredients left to cook anything.");
break;
}
case 2: //which means no
prompt.print("Ok, done cooking. Enjoy!");
break;
default:
prompt.print("Invalid answer please try again!");
}// End answer switch
}
}
UserPrompt.java
import java.util.Scanner;
public class UserPrompt
{
final Scanner kbd = new Scanner(System.in);
public int askForNumber(String prompt)
{
//Print your prompt to the command line -- I added a little extra at the end for simple reading
System.out.print(prompt + " - ");
return kbd.nextInt();
}
public void print(String message)
{
System.out.println(message);
}
}
CONTINUING IN PART 4
Solution 4:[4]
PART 4
ShowRecipesNew.java
import java.util.Scanner;
//object class
public class ShowRecipesNew {
Scanner kbd = new Scanner(System.in);
//ingredient names array
public String ingredientsNames[] = {"Apples", "Cherry", "Carrot", "Flour ", "Sugar "};
//ingredients array - setters will set
public int[] ingredients = new int[5];
//Basic constructor
public ShowRecipesNew() {
ingredients[0] = 1; //apple
ingredients[1] = 1; //cherry
ingredients[2] = 1;//carrot
ingredients[3] = 0;//flour
ingredients[4] = 0;//sugar
}
//overloaded constructor that covers each field
public ShowRecipesNew(int apples, int cherries, int carrots, int flour, int sugar) {
System.out.println("overloaded");
setApples(apples);
setCherries(cherries);
setCarrots(carrots);
setFlour(flour);
setSugar(sugar);
}
public void IngredientCount(UserPrompt prompt) { //Lists ingredient input counts
prompt.print("According to your input, you have:");
prompt.print("Ingredient\tValue");
for(int counter=0;counter<ingredients.length;counter++) {
prompt.print(ingredientsNames[counter] + "\t\t" + ingredients[counter]);
}//end for loop
}//end ingredient listing
//Setters - set from input()
//Apple setter
public void setApples(int apples){
ingredients[0] = apples;
}
//Cherry setter
public void setCherries(int cherries){
ingredients[1] = cherries;
}
//Carrot setter
public void setCarrots(int carrots){
ingredients[2]= carrots;
}
//Flour setter
public void setFlour(int flour){
ingredients[3] = flour;
}
//Sugar setter
public void setSugar(int sugar){
ingredients[4] = sugar;
}
//Getters
//Apple getter
public int getApples(){
return ingredients[0];
}
//Cherry getter
public int getCherries(){
return ingredients[1];
}
//Carrot getter
public int getCarrots(){
return ingredients[2];
}
//Flour getter
public int getFlour(){
return ingredients[3];
}
//Sugar getter
public int getSugar(){
return ingredients[4];
}
public void compareRecipe(UserPrompt prompt) {
prompt.print("According to the input, you can make:");
//Apples
if (ingredients[0] >= 3) {
prompt.print(RecipeNamesA[0]); //Apple Jam
}
if (ingredients[0] >= 2) {
prompt.print(RecipeNamesA[1]); //Apple Jelly
prompt.print(RecipeNamesA[2]); //Apple Smoothie
}
if (ingredients[0] >= 1 && ingredients[3] >= 1){
prompt.print(RecipeNamesA[3]); //Apple Tart
}
if (ingredients[0] >= 2 && ingredients[4] >= 2 && ingredients[3] >= 1){
prompt.print(RecipeNamesA[4]); //Apple Pie
}
if (ingredients[0] == 0) {
prompt.print("No apple recipes");
}
//Cherries
if (ingredients[1] >= 3) {
prompt.print(RecipeNamesCh[0]); //Cherry Jam
}
if (ingredients[1] >= 2) {
prompt.print(RecipeNamesCh[1]); //Cherry Jelly
}
if (ingredients[1] >= 2 && ingredients[3] >= 3 && ingredients[4] >= 2) {
prompt.print(RecipeNamesCh[2]); //Cherry Pie
}
if (ingredients[1] >= 1 && ingredients[3] <= 1 && ingredients[4] >= 1) {
prompt.print(RecipeNamesCh[3]); //Cherry Tart
}
if (ingredients[1] == 0) {
prompt.print("No cherry recipes");
}
//Carrots
if (ingredients[2] >= 2) {
prompt.print(RecipeNamesCa[0]); //Carrot juice
}
if (ingredients[2] >= 2 && ingredients[3] >= 1) {
prompt.print(RecipeNamesCa[2]); //Carrot potage
}
if (ingredients[2] >= 1 && ingredients[3] >= 1 && ingredients[4] >= 1){
prompt.print(RecipeNamesCa[1]); //Carrot cake
}
if (ingredients[2] == 0) {
prompt.print("No carrot recipes");
}
}
private String RecipeNamesA[] = {"1. Apple Jam", "2. Apple Jelly", "3. Apple Smoothie", "4. Apple Tart", "5. Apple Pie"}; //Apple recipes
private String RecipeNamesCh[] = {"6. Cherry Jam", "7. Cherry Jelly", "8. Cherry Pie", "9. Cherry Tart"}; //Cherry recipes
private String RecipeNamesCa[] = {"10. Carrot Juice", "11. Carrot Cake","12. Carrot Potage"}; //Carrot recipes
}
Now, if you try to compile these 3 files, we'll see that UserPrompt
and ShowRecipesNew
have no errors, but Driver
has a whole bunch. Here is one.
Driver.java:69: error: RecipeNamesA has private access in ShowRecipesNew
prompt.print(sr.RecipeNamesA[0]);
^
So now, the compiler has pointed out all of the situations where we have made direct interaction with private instance fields. As mentioned before, this another form of tight coupling, and in fact, it starts to slip over into the second point I was talking about - global state. We are going to fixing most of the global state issues here, so there may not be much left to correct once we finish up.
Anyways, the way to resolve those errors is by using setters and/or getters. You had the right idea going with the setters and getters you added already, but let's add some more for the rest of the private instance fields in ShowRecipe
. In this case, we only need to use getters, since the private fields are only being read, not set.
public class ShowRecipesNew
{
//ignore the lines that come before this one
private String RecipeNamesA[] = {"1. Apple Jam", "2. Apple Jelly", "3. Apple Smoothie", "4. Apple Tart", "5. Apple Pie"}; //Apple recipes
private String RecipeNamesCh[] = {"6. Cherry Jam", "7. Cherry Jelly", "8. Cherry Pie", "9. Cherry Tart"}; //Cherry recipes
private String RecipeNamesCa[] = {"10. Carrot Juice", "11. Carrot Cake","12. Carrot Potage"}; //Carrot recipes
public String[] getAppleRecipeNames()
{
return RecipeNamesA;
}
public String[] getCherryRecipeNames()
{
return RecipeNamesCh;
}
public String[] getCarrotRecipeNames()
{
return RecipeNamesCa;
}
}
Ok, now that we have some getters, let's replace the failing lines in Driver
with those new getters. Seems like they were all in the WantToCook()
method.
public void WantToCook(UserPrompt prompt) { //Enter recipe number - deducts ingredients
int recipeNum = prompt.askForNumber("What would you like to cook? Please enter the recipe number.");
switch(recipeNum) {
case 1: //Apple jam
sr.setApples(sr.getApples()-3);
prompt.print(sr.getAppleRecipeNames()[0]);
break;
case 2: //Apple jelly
prompt.print(sr.getAppleRecipeNames()[1]);
sr.setApples(sr.getApples()-2);
break;
case 3://Apple Smoothie
prompt.print(sr.getAppleRecipeNames()[2]);
sr.setApples(sr.getApples()-2);
break;
case 4: //Apple Tart
prompt.print(sr.getAppleRecipeNames()[3]);
sr.setApples(sr.getApples()-1);
sr.setFlour(sr.getFlour()-1);
sr.setSugar(sr.getSugar()-1);
break;
case 5: //Apple Pie
prompt.print(sr.getAppleRecipeNames()[4]);
sr.setApples(sr.getApples()-2);
sr.setFlour(sr.getFlour()-3);
sr.setSugar(sr.getSugar()-2);
break;
case 6: //Cherry Jam
prompt.print(sr.getCherryRecipeNames()[0]);
sr.setCherries(sr.getCherries()-3);
break;
case 7: //Cherry Jelly
prompt.print(sr.getCherryRecipeNames()[1]);
sr.setCherries(sr.getCherries()-2);
break;
case 8: //Cherry Pie
prompt.print(sr.getCherryRecipeNames()[2]);
sr.setCherries(sr.getCherries()-2);
sr.setFlour(sr.getFlour()-3);
sr.setSugar(sr.getSugar()-2);
break;
case 9: //Cherry Tart
prompt.print(sr.getCherryRecipeNames()[3]);
sr.setCherries(sr.getCherries()-1);
sr.setFlour(sr.getFlour()-1);
sr.setSugar(sr.getSugar()-1);
break;
case 10: //Carrot juice
prompt.print(sr.getCarrotRecipeNames()[0]);
sr.setCarrots(sr.getCarrots()-2);
break;
case 11: //Carrot cake
prompt.print(sr.getCarrotRecipeNames()[1]);
sr.setCarrots(sr.getCarrots()-1);
sr.setFlour(sr.getFlour()-1);
sr.setSugar(sr.getSugar()-1);
break;
case 12: //Carrot potage
prompt.print((sr.getCarrotRecipeNames()[2]));
sr.setCarrots(sr.getCarrots()-2);
sr.setFlour(sr.getFlour()-1);
break;
default:
prompt.print("Invalid entry.");
}//End recipeNum switch
prompt.print("You have cooked a thing! Good job!");
CookAgain(prompt); //cook again? prompt
} //end WantToCook method
Now, if we compile, we get no more errors. And more importantly, the code runs too.
At this point, you could technically stop and say that the original goal has completed. If so, feel free to stop reading now. However, I will continue performing all the tasks I agreed to.
Moving along, the list of methods that we have made loosely coupled is as follows.
ShowRecipes()
input()
IngredientCount()
selection()
- The getters and setters
On to the next method, we have compareRecipe()
in ShowRecipes
.
Now, this one is a little bit unclear on how best to move forward. In one perspective, you could say that this method just prints out the state of ShowRecipes
. But on the other hand, you could say that it decides what to print based on the state of ShowRecipes
.
Me personally, the thing that pushes it just over the edge for me is the fact that it uses what we like to call Magic Numbers. What if we add a new recipe or a new ingredient? Sure, they likely wouldn't cause any compilation errors, but it definitely would cause logic errors, as mentioned previously. That said, the way that we would resolve this is by using enums actually. So, I will save this change until we reach the enum portion, just to keep things easy to follow. But yes, this method still has tight coupling to the indexes of the various int arrays. Trying to solve it without enums would just be a waste of effort since the best solution is definitely, so I will defer this change until then.
Next on the list is WantToCook()
in Driver
. We already moved this over, so the method is in the right area. And thanks to the changes we made before (plugging in UserPrompt
and ShowRecipes
), most of the tight coupling has been removed. However, just like the previous method, we are tightly coupled to the indexes. And just like before, I will solve this tight coupling to the indexes in the enums section.
But otherwise, that's all the methods for checking tight coupling. Now, let's move on to the next section.
#2 Global State and Public Instance Fields
When an object lets its internal state be public, it allows other programs/classes/etc to depend on the internal implementation of the class. As mentioned before - if you are currently using an int[]
and then decide to switch over to a List<Integer>
, then anything that was depending on the internal state will immediately break, and you will need to do some debugging and refactoring to correct the compilation errors. However, if you keep your internal state private and have clearly defined getters and setters, then you can make all the changes internally, and then just modify your getters and setters to accomodate the new internal state. For example, let's say you have getters that return int[]
and setters that accept int[]
. Well, when your internal state decides to use a list instead, your getter should still return int[]
and your setter should still accept int[]
, but the getter should convert the List<Integer>
into an int[]
array, and the setter should convert the accepted int[]
parameter into a List<Integer>
before modifying internal state.
As you might have guessed, that means that we need to turn all of the instance fields into private instance fields. Each class has at least one instance field that needs to change. Let's cycle through the classes and get them all.
Driver
- looks like our instance fieldShowRecipesNew sr
was only set to package-protected as opposed to private. All that is required is to put a private in front of it and we are done. Like this --private ShowRecipesNew sr;
UserPrompt
- looks like we did the same mistake for theScanner
inUserPrompt
. This is just as simple and everything works just fine after.private final Scanner kbd = new Scanner(System.in);
ShowRecipesNew
- Unfortunately, this is where we will need to make some heavier modifications. For starters, there are 3 instance fields that are not private.
Scanner kbd = new Scanner(System.in);
//ingredient names array
public String ingredientsNames[] = {"Apples", "Cherry", "Carrot", "Flour ", "Sugar "};
//ingredients array - setters will set
public int[] ingredients = new int[5];
Thankfully, we can eliminate one of them already. The Scanner
shouldn't be there. All of our scanning is being handled by UserPrompt
, so we can just plain delete that variable completely.
Unfortunately, the other 2 are much harder. They are actually being used up and down the Driver
class, so we will need to do some heavy rewriting. In short, anywhere that we directly accessed these instance fields, we will now do through a getter. Much like previously, this is a tedious task so I will just knock it out all at once.
Here are the getters in ShowRecipesNew
.
public String[] getIngredientNames()
{
return ingredientsNames;
}
public int[] getIngredientsBag()
{
return ingredients;
}
And here is the refactored Driver
.
public class Driver
{
private ShowRecipesNew sr;
public Driver(ShowRecipesNew parameter)
{
sr = parameter;
}
public static void main (String [] args){
final ShowRecipesNew first = new ShowRecipesNew();
System.out.println("FIRST");
final UserPrompt prompt = new UserPrompt();
final Driver driver = new Driver(first);
driver.input(prompt);
first.compareRecipe(prompt);
driver.WantToCook(prompt);
driver.selection(prompt);
}
public void selection(UserPrompt prompt){ //Can access every method in class except CookAgain
//1. Cook something -- WantToCook()
//2. See ingredient list -- IngredientCount()
//3. See available recipes -- compareRecipe()
//4. Modify total ingredient amounts -- input()
//maybe put this in ShowRecipes, create object for each to put in own classes
int userChoice =
prompt.askForNumber(
"What would you like to do now? Enter a number to select. \n1. Cook something \n2. See ingredients \n3. See available recipes \n4. Modify ingredient amounts"
);
switch (userChoice) {
case 1:
WantToCook(prompt);
break;
case 2:
sr.IngredientCount(prompt);
break;
case 3:
sr.compareRecipe(prompt);
break;
case 4:
input(prompt);
break;
default:
System.out.println("Invalid");
return;
}
}
public void WantToCook(UserPrompt prompt) { //Enter recipe number - deducts ingredients
int recipeNum = prompt.askForNumber("What would you like to cook? Please enter the recipe number.");
switch(recipeNum) {
case 1: //Apple jam
sr.setApples(sr.getApples()-3);
prompt.print(sr.getAppleRecipeNames()[0]);
break;
case 2: //Apple jelly
prompt.print(sr.getAppleRecipeNames()[1]);
sr.setApples(sr.getApples()-2);
break;
case 3://Apple Smoothie
prompt.print(sr.getAppleRecipeNames()[2]);
sr.setApples(sr.getApples()-2);
break;
case 4: //Apple Tart
prompt.print(sr.getAppleRecipeNames()[3]);
sr.setApples(sr.getApples()-1);
sr.setFlour(sr.getFlour()-1);
sr.setSugar(sr.getSugar()-1);
break;
case 5: //Apple Pie
prompt.print(sr.getAppleRecipeNames()[4]);
sr.setApples(sr.getApples()-2);
sr.setFlour(sr.getFlour()-3);
sr.setSugar(sr.getSugar()-2);
break;
case 6: //Cherry Jam
prompt.print(sr.getCherryRecipeNames()[0]);
sr.setCherries(sr.getCherries()-3);
break;
case 7: //Cherry Jelly
prompt.print(sr.getCherryRecipeNames()[1]);
sr.setCherries(sr.getCherries()-2);
break;
case 8: //Cherry Pie
prompt.print(sr.getCherryRecipeNames()[2]);
sr.setCherries(sr.getCherries()-2);
sr.setFlour(sr.getFlour()-3);
sr.setSugar(sr.getSugar()-2);
break;
case 9: //Cherry Tart
prompt.print(sr.getCherryRecipeNames()[3]);
sr.setCherries(sr.getCherries()-1);
sr.setFlour(sr.getFlour()-1);
sr.setSugar(sr.getSugar()-1);
break;
case 10: //Carrot juice
prompt.print(sr.getCarrotRecipeNames()[0]);
sr.setCarrots(sr.getCarrots()-2);
break;
case 11: //Carrot cake
prompt.print(sr.getCarrotRecipeNames()[1]);
sr.setCarrots(sr.getCarrots()-1);
sr.setFlour(sr.getFlour()-1);
sr.setSugar(sr.getSugar()-1);
break;
case 12: //Carrot potage
prompt.print((sr.getCarrotRecipeNames()[2]));
sr.setCarrots(sr.getCarrots()-2);
sr.setFlour(sr.getFlour()-1);
break;
default:
prompt.print("Invalid entry.");
}//End recipeNum switch
prompt.print("You have cooked a thing! Good job!");
CookAgain(prompt); //cook again? prompt
} //end WantToCook method
public void input(UserPrompt prompt) { // user inputs ingredient counts
//Asks user for ingredients on-hand
prompt.print("Please enter how many of each ingredient you have:");
sr.setApples(prompt.askForNumber(sr.getIngredientNames()[0]));
sr.setCherries(prompt.askForNumber(sr.getIngredientNames()[1]));
sr.setCarrots(prompt.askForNumber(sr.getIngredientNames()[2]));
sr.setFlour(prompt.askForNumber(sr.getIngredientNames()[3]));
sr.setSugar(prompt.askForNumber(sr.getIngredientNames()[4]));
}//end user ingredient input
public void CookAgain(UserPrompt prompt) {
int answer = prompt.askForNumber("Would you like to cook again? 1 = yes, 2 = no - ");
switch (answer) {
case 1: //which means yes
if (sr.getIngredientsBag()[0] >= 1 || sr.getIngredientsBag()[1] >= 1 || sr.getIngredientsBag()[2] >= 1) { //no apples, cherries, or carrots
prompt.print("You've used some ingredients. Let's see what you have now.");
sr.IngredientCount(prompt);
sr.compareRecipe(prompt);
WantToCook(prompt);
break;
}
else {
prompt.print("You don't have enough ingredients left to cook anything.");
break;
}
case 2: //which means no
prompt.print("Ok, done cooking. Enjoy!");
break;
default:
prompt.print("Invalid answer please try again!");
}// End answer switch
}
}
And now, we have fully eliminated all global access and direct access to instance fields. Now, we just need to maintain our getters/setters in order to make sure that all of our objects work well together.
On to the next point.
#3 Complex and large methods
In your defense, Java is a language that makes it very easy to write long methods. Most people even say the languages lends itself to verbosity. Regardless, methods that are too complex end up becoming too difficult to maintain. This especially becomes a problem when you want to add more functionality to your program. I brought this up before, but what if you want to add a new ingredient to your list? Well, all those complex methods that span 50+ lines will now need to be updated to handle this new ingredient as well.
Funnily enough, literally every single change I would make to shorten these methods would involve an enum, so I am going to combine the next part into this one.
#4 Java Enums
This is by far the biggest improvement we can make to this project. I said it before, and I'll say it again - this type of problem practically demands that you use Java Enums. And since Java enums are so powerful and are the best out there, getting practice using them now will be good for you.
That said, enums can be confusing. If this section doesn't make much sense and/or is hard to understand, feel free to bypass this one to focus on the previous sections.
But going back to the problem, the String[] ingredientsNames
would be better represented as a separate enum class called Ingredient
. This new Ingredient
class would go into a file called Ingredient.java
.
public enum Ingredient
{
APPLE,
CHERRY,
CARROT,
FLOUR,
SUGAR,
;
}
Great, now that we have the Ingredient
enum, let's also make one for the recipes. We can call this enum Recipe
and put it into Recipe.java
.
public enum Recipe
{
APPLE_JAM,
APPLE_JELLY,
APPLE_SMOOTHIE,
APPLE_TART,
APPLE_PIE,
CHERRY_JAM,
CHERRY_JELLY,
CHERRY_PIE,
CHERRY_TART,
CARROT_JUICE,
CARROT_CAKE,
CARROT_POTAGE,
;
}
Ok, now that that one is ready too, we can start using these enums in place of arbitrary String
values.
The first thing that should definitely change is the int[] ingredients
instance field in ShowRecipesNew
class. That should be replaced with an [EnumMap<E>][4]
. Be sure to import it using the following imports.
import java.util.EnumMap;
import java.util.Map;
And here is the change.
public class ShowRecipesNew {
//ignore the lines before this one
private EnumMap<Ingredient, Integer> ingredients = new EnumMap<>(Ingredient.class);
//ignore the lines after this one
}
This map will tell us how many of each ingredient there is. What makes this so useful though is that, if we ever decide to add a new ingredient like a banana, all we need to do is add BANANA
to Ingredient
.
Anyways, once we compile this, several things break.
First, the no argument constructor that gives default values for all of the ingredients is expecting an array. We can fix this by modifying the Ingredient
enum to hold the default values instead.
public enum Ingredient
{
APPLE(1),
CHERRY(1),
CARROT(1),
FLOUR(0),
SUGAR(0),
;
private final int defaultValue;
Ingredient(int input)
{
defaultValue = input;
}
public int defaultValue()
{
return defaultValue;
}
}
This is a useful change, because now, if we ever decide to add a new ingredient to Ingredient
, we must also give that new ingredient a default value, otherwise there will be compilation errors. The reason why this is so powerful is because it prevents you from running into logic errors by forgetting to set something. And we're about to see a lot more.
Now, let's modify the constructor to use those default values. We will be using the values()
method from Ingredient
. This method is one that is automatically included for you, though you can't see it. Methods like this that are automatically included without being seen are called intrinsic methods.
public class ShowRecipesNew {
//ignore the lines before this one
public ShowRecipesNew() {
for (Ingredient each : Ingredient.values())
{
ingredients.put(each, each.defaultValue());
}
}
//ignore the lines after this one
}
Again, this is definitely more complex, so feel free to skip over this if it gets too complicated. But in short, the values()
method in Ingredient
returns all of the enum values, so APPLE
, CHERRY
, CARROT
, etc. Then, we loop through all of those enum values using a for-each loop. Once inside the loop, we start putting values on the map. Previously, using your old version with the int[]
, we would use the index to know which ingredient we were specifying the number of. But for an EnumMap
, we don't care about index. We use a key-value pair relationship to find out how much of each Ingredient
there is. As for defining how much, we pull the defaultValue()
from the enum earlier on. Each enum value has one specified in the Ingredient
enum, so it is easy to simply pull and use it.
Once we compile this, the constructor is now fixed. But there are more errors. Now, the getter for ingredients
is broken. This is pretty easy to fix, we just change the return type. It's important to note though - I am only changing the return type because I believe it will make the program much easier to use and develop for. In general, it's better to avoid changing the return types of public methods, especially ones that other classes depend upon. Otherwise, it will cause errors.
Here is the change.
public EnumMap<Ingredient, Integer> getIngredientsBag()
{
return ingredients;
}
After compiling this, the next error we get is for the for loop in the method IngredientCount()
. This is easily dealt with by using a for each loop. Specifically, we are going to be looping over the entrySet()
method from the EnumMap
. The entrySet()
method allows us to loop through both the key and it's adjacent value. In the case of ingredients
, that means that we can fetch the Ingredient
, and the quantity of it, which is exactly what we want.
public ShowRecipesNew() {
for (Ingredient each : Ingredient.values())
{
ingredients.put(each, each.defaultValue());
}
}
Now that takes care of the errors in the constructor. Next, we get errors in the batch of getters and setters for Apple, Cherry, etc. However, this is yet another powerful feature of enums - we don't need those setters or getters anymore. We can combine all of those getters into a single getter and all those setters into a single setter. This is the power of using a [Map][7]
.
public void setIngredientCount(Ingredient ingredient, int count)
{
ingredients.put(ingredient, count);
}
MORE IN PART 5
Solution 5:[5]
PART 5
This setter can handle all of the different Ingredient
values because it takes in an Ingredient
parameter. What this means is that, if I want to set the number of Apples to be 12, all I need to do is this -- setIngredientCount(Ingredient.APPLE, 12);
. As you can see, you don't need a separate method to handle setting Cherry or Carrot. They can all be set by using this method, it just requires you to specify which ingredient you are setting in the parameter.
Now, once we replace all those setters with this new one, more stuff starts breaking that depended on the old setter. In this case, the ShowRecipesNew
constructor (the one with many parameters) is now broken, since it used to use the old setters. We just need to swap out the old setters with the new one.
public ShowRecipesNew(int apples, int cherries, int carrots, int flour, int sugar) {
System.out.println("overloaded");
this();
setIngredientCount(Ingredient.APPLE, apples);
setIngredientCount(Ingredient.CHERRY, cherries);
setIngredientCount(Ingredient.CARROT, carrots);
setIngredientCount(Ingredient.FLOUR, flour);
setIngredientCount(Ingredient.SUGAR, sugar);
}
That this()
method actually calls the ShowRecipes()
constructor first. That is helpful since it sets all values to the defaultValue
first. Then, we can later modify them using the setIngredientCount()
method. This way, we can avoid any pesky and annoying nulls.
Once that compiles, then this constructor is good. The next error comes from the getters of all the different ingredients. Just like the setter, we can combine all of this into a single method. Here it is.
public int getIngredientCount(Ingredient ingredient)
{
return ingredients.get(ingredient);
}
Once that getter is fixed, then the next method to break is the compareRecipe()
method in ShowRecipe
class.
Now this method is yet another place we can use enums to make the code both easier to understand and safer. We are actually going to uproot this method almost entirely, and place the logic into the Recipe
enum.
Let's start by adding the ability to put a map of requiredIngredients
inside of each method, using an EnumMap<K>
.
import java.util.EnumMap;
import java.util.Map;
public enum Recipe
{
APPLE_JAM,
APPLE_JELLY,
APPLE_SMOOTHIE,
APPLE_TART,
APPLE_PIE,
CHERRY_JAM,
CHERRY_JELLY,
CHERRY_PIE,
CHERRY_TART,
CARROT_JUICE,
CARROT_CAKE,
CARROT_POTAGE,
;
private final EnumMap<Ingredient, Integer> requiredIngredients;
Recipe(Map<Ingredient, Integer> parameter)
{
requiredIngredients = new EnumMap<>(parameter);
}
}
Now, we can plug in how many of each ingredient we need in order to satisfy this recipe. As for the methods I am using, I am constructing key value pairs as mentioned before, with the key being the ingredient, and the value being how much of that ingredient. I know the methods might look a bit confusing, but that is all that they are doing.
import java.util.EnumMap;
import java.util.Map;
public enum Recipe
{
APPLE_JAM(
Map.of(
Ingredient.APPLE, 3
)
),
APPLE_JELLY(
Map.of(
Ingredient.APPLE, 2
)
),
APPLE_SMOOTHIE(
Map.of(
Ingredient.APPLE, 2
)
),
APPLE_TART(
Map.of(
Ingredient.APPLE, 1,
Ingredient.FLOUR, 1
)
),
APPLE_PIE(
Map.of(
Ingredient.APPLE, 2,
Ingredient.FLOUR, 1,
Ingredient.SUGAR, 2
)
),
CHERRY_JAM(
Map.of(
Ingredient.CHERRY, 3
)
),
CHERRY_JELLY(
Map.of(
Ingredient.CHERRY, 2
)
),
CHERRY_PIE(
Map.of(
Ingredient.CHERRY, 2,
Ingredient.FLOUR, 3,
Ingredient.SUGAR, 2
)
),
CHERRY_TART(
Map.of(
Ingredient.CHERRY, 1,
Ingredient.FLOUR, 1,
Ingredient.SUGAR, 1
)
),
CARROT_JUICE(
Map.of(
Ingredient.CARROT, 2
)
),
CARROT_CAKE(
Map.of(
Ingredient.CARROT, 1,
Ingredient.FLOUR, 1,
Ingredient.SUGAR, 1
)
),
CARROT_POTAGE(
Map.of(
Ingredient.CARROT, 2,
Ingredient.FLOUR, 1
)
),
;
private final EnumMap<Ingredient, Integer> requiredIngredients;
Recipe(Map<Ingredient, Integer> parameter)
{
requiredIngredients = new EnumMap<>(parameter);
}
}
As you can see, the code is a lot more explicit now. Instead of saying we need a value of 3 in ingredients[0] to satisfy recipeNamesA[0]
, we can now say we need a value of 3 for Ingredient.APPLE to satisfy APPLE_PIE
. We are using more complex terms to do it, but once you understand those complex terms, the solution becomes much more expressive and easy to understand.
And finally, let's add a method to Recipe
in order to help us do the quantity check.
public boolean canMakeRecipeWith(Map<Ingredient, Integer> ingredientsWeHave)
{
for (Map.Entry<Ingredient, Integer> each : requiredIngredients.entrySet())
{
Ingredient ingredient = each.getKey();
int howMuchWeHave = ingredientsWeHave.get(ingredient);
int howMuchWeNeed = requiredIngredients.get(ingredient);
if (howMuchWeHave < howMuchWeNeed)
{
return false;
}
}
return true;
}
This method will allow us to take in our Map<Ingredient, Integer>
and see if we have enough ingredients to make the chosen recipe. The way we will chose the recipe is by doing something like this. Recipe.APPLE_PIE.canMakeRecipeWith(ingredients)
-- this will return true
or false
depending on whether or not ingredients
has enough of each Ingredient
to make an APPLE_PIE
Now, we have modified this enum to become powerful and expressive. Let's start using it.
Back to our original method compareRecipe()
, we can pretty much rework this method from the ground up to use our new enum functionality.
public void compareRecipe(UserPrompt prompt) {
prompt.print("According to the input, you can make:");
if (getIngredientCount(Ingredient.APPLE) == 0) {
prompt.print("No apple recipes");
}
if (getIngredientCount(Ingredient.CHERRY) == 0) {
prompt.print("No cherry recipes");
}
if (getIngredientCount(Ingredient.CARROT) == 0) {
prompt.print("No carrot recipes");
}
for (Recipe each : Recipe.values())
{
if (each.canMakeRecipeWith(ingredients))
{
prompt.print(each.ordinal() + " " + each.name());
}
}
}
This is really helpful to us because now, if we ever add a new Recipe
, we never have to touch this method again. Since we plugged in the Recipe
enum into ShowRecipesNew
, we only have to modify Recipe
in order to make sweeping changes for ShowRecipesNew
.
In fact, we can even go a step farther, and change the way we do the no ____ recipes
feature too.
public void compareRecipe(UserPrompt prompt) {
prompt.print("According to the input, you can make:");
for (Ingredient each : Ingredient.values())
{
Integer count = getIngredientCount(each);
if (count == 0)
{
prompt.print("No " + each.name().toLowerCase() + " recipes");
}
}
for (Recipe each : Recipe.values())
{
if (each.canMakeRecipeWith(ingredients))
{
prompt.print(each.ordinal() + " " + each.name());
}
}
}
Now, if you ever add a new value to Ingredient
, you don't need to add another method to notify that there aren't enough ingredients. This loop will now handle that for you.
As for what this method does, it basically does exactly what it did before, but using enums. It uses the ordinal()
method to print the number, then uses the name()
method to print the name. Each enum has an ordinal, which is just like an index in the array example we used to have before. We will be using arrays a little bit in our solution, but they will be Ingredient[]
now instead of int[]
or String[]
. That will come later on though.
Now, if we try compiling, both ShowRecipesNew
and Recipe
should be good. Of course, we still have plenty to do in ShowRecipesNew
.
First, let's start deleting the non-enum stuff. That includes the leftover Scanner
(which should have been deleted long ago), the ingredientNames
array, and all the getters and setters for the array instance fields. Once we clean up ShowRecipesNew
, here is what we have left.
ShowRecipesNew.java
import java.util.EnumMap;
import java.util.Map;
import java.util.Scanner;
//object class
public class ShowRecipesNew {
//ingredients array - setters will set
private EnumMap<Ingredient, Integer> ingredients = new EnumMap<>(Ingredient.class);
//Basic constructor
public ShowRecipesNew() {
for (Ingredient each : Ingredient.values())
{
ingredients.put(each, each.defaultValue());
}
}
//overloaded constructor that covers each field
public ShowRecipesNew(int apples, int cherries, int carrots, int flour, int sugar) {
this();
System.out.println("overloaded");
setIngredientCount(Ingredient.APPLE, apples);
setIngredientCount(Ingredient.CHERRY, cherries);
setIngredientCount(Ingredient.CARROT, carrots);
setIngredientCount(Ingredient.FLOUR, flour);
setIngredientCount(Ingredient.SUGAR, sugar);
}
public void IngredientCount(UserPrompt prompt) { //Lists ingredient input counts
prompt.print("According to your input, you have:");
prompt.print("Ingredient\tValue");
for(Map.Entry<Ingredient, Integer> each : ingredients.entrySet()) {
prompt.print(each.getKey() + "\t\t" + each.getValue());
}//end for loop
}//end ingredient listing
public void setIngredientCount(Ingredient ingredient, int count)
{
ingredients.put(ingredient, count);
}
public int getIngredientCount(Ingredient ingredient)
{
return ingredients.get(ingredient);
}
public void compareRecipe(UserPrompt prompt) {
prompt.print("According to the input, you can make:");
for (Ingredient each : Ingredient.values())
{
Integer count = getIngredientCount(each);
if (count == 0)
{
prompt.print("No " + each.name().toLowerCase() + " recipes");
}
}
for (Recipe each : Recipe.values())
{
if (each.canMakeRecipeWith(ingredients))
{
prompt.print(each.ordinal() + " " + each.name());
}
}
}
}
Much smaller and more compact. Now, there are no more references to arrays or magic numbers. Just pure values and enums. I also went ahead and removed the getter getIngredientsBag()
for ingredients
. Even though it returns an EnumMap<Ingredient, Integer>
, we can check each Ingredient
individually using getIngredientCount()
, so there's not much benefit in exposing the whole map.
Now, if we compile all of our classes, everything should be working except for Driver.java
. So let's start fixing that.
Now, we could go through each one of the errors individually, but just trust me when I say we should just go through each method and rework it to use enums instead. Java has decent error messages, but sometimes (and in this particular moment), they will send you off on a goose hunt when the real issue is elsewhere. So, let's go through the methods in Driver
one by one and switch out the int[]
and String[]
based solutions for enum ones.
public Driver(ShowRecipesNew parameter)
- nothing to change here, this is perfect.public static void main (String [] args)
- same thing here, noint[]
orString[]
, so we are all good.public void selection(UserPrompt prompt)
- this is fine. All we are doing is letting the user choose which option they want. Technically, we could turn this into an enum too, but that would be excessive with no short term benefit.public void WantToCook(UserPrompt prompt)
- Ok, here is something that we do need to change. In this method, we are manually editing the values via the old getters and setters. Obviously those are gone now, and more importantly, we have a much more efficient way of doing this. But in order to use it, we must modify ourRecipe
enum one more time.
In Recipe
, we created the method canMakeRecipeWith
, whose only job was to check if we had enough of each Ingredient
to be able to cook our recipe. However, there is no method to actually modify the ingredients counts. Let's give Recipe
the ability to do that by plugging ShowRecipesNew
into a new method in Recipe
.
public String cookRecipeWith(ShowRecipesNew sr)
{
for (Map.Entry<Ingredient, Integer> each : requiredIngredients.entrySet())
{
Ingredient ingredient = each.getKey();
int currentNum = sr.getIngredientCount(ingredient);
int requiredAmount = requiredIngredients.get(ingredient);
sr.setIngredientCount(ingredient, currentNum - requiredAmount);
}
return name();
}
Of course, it is not the job of Recipe
to do state manipulation, but we need state manipulation to occur, so we plug in ShowRecipesNew
so that we can delegate state manipulation.
Now, what this method does is go through the requiredIngredients
of the Recipe
, then removes the amount from sr
. Basically, it is doing exactly what WantToCook()
was doing, but it's using a for loop to make this code both smaller and safer.
Now, let's rework WantToCook()
to use this new functionality we created.
public void WantToCook(UserPrompt prompt) { //Enter recipe number - deducts ingredients
int recipeNum = prompt.askForNumber("What would you like to cook? Please enter the recipe number.");
if (recipeNum >= Recipe.values().length)
{
prompt.print("invalid entry");
}
else
{
final Recipe recipeToMake = Recipe.values()[recipeNum];
prompt.print(recipeToMake.cookRecipeWith(sr));
}
prompt.print("You have cooked a thing! Good job!");
CookAgain(prompt); //cook again? prompt
} //end WantToCook method
Now, all the logic is being handled by the Recipe
enum, and we don't need to think about whether we caught all the values or not - the for loop is handling that for us. Let's move on to the next method.
public void input(UserPrompt prompt)
- This one also needs to be reworked. Let's start on this one too.
public void input(UserPrompt prompt) { // user inputs ingredient counts
//Asks user for ingredients on-hand
prompt.print("Please enter how many of each ingredient you have:");
for (Ingredient each : Ingredient.values())
{
int count = prompt.askForNumber(each.name());
sr.setIngredientCount(each, count);
}
}//end user ingredient input
This is stuff we've seen a few times before, so I won't go in depth - we cycle through all possible values of Ingredient
, then use sr
to set them to the count
number that the user entered.
public void CookAgain(UserPrompt prompt)
- This one also needs to change, but we will need to undo some of the work we have done prior. It will be very minimal though, so don't worry.
First, what this method does is ask the user what they want to do. Then, if the user says they want to cook, they check if cooking can be done. If so, then reenter the cooking process again. But if not, then notify the user. If the user says they do not want to cook, then we just break out and more or less end the method.
In retrospect, this method is a little more complex than I remember it, but we can clean it up a lot with only a little effort.
First, we need to check if we have enough Ingredient
in sr
to be able to cook any of the Recipe
. In Recipe
, we have a method that allows us to check if we have enough Ingredient
, but it checks a Map
, not ShowRecipesNew
, which is what sr
is. Normally, we would just make a new method to handle checking, but since what they do is exactly the same with only a different parameter, I think it makes more sense to just have one method handle checking.
So now, let's redo our canMakeRecipeWith()
method in Recipe
.
public boolean canMakeRecipeWith(ShowRecipesNew sr)
{
for (Map.Entry<Ingredient, Integer> each : requiredIngredients.entrySet())
{
Ingredient ingredient = each.getKey();
int howMuchWeHave = sr.getIngredientCount(ingredient);
int howMuchWeNeed = requiredIngredients.get(ingredient);
if (howMuchWeHave < howMuchWeNeed)
{
return false;
}
}
return true;
}
Nice, now our cookRecipeWith
and canMakeRecipeWith
methods both have ShowRecipesNew sr
as their parameter. That hopefully makes things a bit simpler to follow along with.
However, changing this method broke ShowRecipesNew
a little, so let's fix that. Specifically, it broke the compareRecipe()
method. It's a simple fix though.
public void compareRecipe(UserPrompt prompt) {
prompt.print("According to the input, you can make:");
for (Ingredient each : Ingredient.values())
{
Integer count = getIngredientCount(each);
if (count == null || count == 0)
{
prompt.print("No " + each.name().toLowerCase() + " recipes");
}
}
for (Recipe each : Recipe.values())
{
//here is the change
if (each.canMakeRecipeWith(this))
{
prompt.print(each.ordinal() + " " + each.name());
}
}
}
The keyword this
is used in Java to represent the instance itself. It's more or less saying use myself as the parameter for this method. And that works because canMakeRecipeWith()
is looking for a ShowRecipesNew
parameter. Therefore, if ShowRecipesNew
submits itself as the parameter, everything checks out fine!
Anyways, now ShowRecipesNew
is working again, so we can go back to Driver
. However, before we go back to our method, there is one logic bug I would like to fix. In WantToCook()
, I would like to check whether the Recipe
the user selected is actually something we have enough Ingredient
to make. So, I will add a simple check to that method.
public void WantToCook(UserPrompt prompt) { //Enter recipe number - deducts ingredients
int recipeNum = prompt.askForNumber("What would you like to cook? Please enter the recipe number.");
if (recipeNum >= Recipe.values().length || recipeNum < 0)
{
prompt.print("invalid entry");
}
else
{
final Recipe recipeToMake = Recipe.values()[recipeNum];
if (recipeToMake.canMakeWith(sr))
{
prompt.print(recipeToMake.cookRecipeWith(sr));
}
else
{
prompt.print("not enough ingredients");
}
}
prompt.print("You have cooked a thing! Good job!");
CookAgain(prompt); //cook again? prompt
} //end WantToCook method
Nice, now we have fixed that logic error, we can go back to the method we were editing, CookAgain
.
For this method, we need to check if we have enough Ingredient
to make any of the Recipe
we have. To do this, let's just cycle through all of the Recipe
and see if we have enough Ingredient
to make any of them. And to keep things organized, I'll make a new method to do this cycling, and then toss it onto the Recipe
enum.
public static boolean canCookAnything(ShowRecipesNew sr)
{
for (Recipe each : Recipe.values())
{
if (each.canMakeRecipeWith(sr))
{
return true;
}
}
return false;
}
This works almost exactly the same as the other canMakeRecipeWith()
method, but the difference being, it borrows that method to check all possible values in Recipe
.
Now, let's use that new method in our CookAgain()
method.
public void CookAgain(UserPrompt prompt) {
int answer = prompt.askForNumber("Would you like to cook again? 1 = yes, 2 = no - ");
switch (answer) {
case 1: //which means yes
if (Recipe.canCookAnything(sr)) {
prompt.print("You've used some ingredients. Let's see what you have now.");
sr.IngredientCount(prompt);
sr.compareRecipe(prompt);
WantToCook(prompt);
break;
}
else {
prompt.print("You don't have enough ingredients left to cook anything.");
break;
}
case 2: //which means no
prompt.print("Ok, done cooking. Enjoy!");
break;
default:
prompt.print("Invalid answer please try again!");
}// End answer switch
}
And just like that, we have simplified this method down to check all possible values of Recipe
and Ingredient
, not just the apples, cherries, and carrots it was checking before.
And now, we have finally finished the refactor of your entire program!
COMPLETE
If you made it this far, be proud of yourself - you have proven to be a resilient programmer willing to do a deep dive to learn more. You will certainly go far in this industry, I don't doubt that.
I will post the completed program in PART FINAL, since I am running out of space here.
Solution 6:[6]
PART FINAL
As promised, here is the complete program, with everything working using all of the refactored code.
Driver.java
public class Driver
{
private ShowRecipesNew sr;
public Driver(ShowRecipesNew parameter)
{
sr = parameter;
}
public static void main (String [] args){
final ShowRecipesNew first = new ShowRecipesNew();
System.out.println("FIRST");
final UserPrompt prompt = new UserPrompt();
final Driver driver = new Driver(first);
driver.input(prompt);
first.compareRecipe(prompt);
driver.WantToCook(prompt);
driver.selection(prompt);
}
public void selection(UserPrompt prompt){ //Can access every method in class except CookAgain
//1. Cook something -- WantToCook()
//2. See ingredient list -- IngredientCount()
//3. See available recipes -- compareRecipe()
//4. Modify total ingredient amounts -- input()
//maybe put this in ShowRecipes, create object for each to put in own classes
int userChoice =
prompt.askForNumber(
"What would you like to do now? Enter a number to select. \n1. Cook something \n2. See ingredients \n3. See available recipes \n4. Modify ingredient amounts"
);
switch (userChoice) {
case 1:
WantToCook(prompt);
break;
case 2:
sr.IngredientCount(prompt);
break;
case 3:
sr.compareRecipe(prompt);
break;
case 4:
input(prompt);
break;
default:
System.out.println("Invalid");
return;
}
}
public void WantToCook(UserPrompt prompt) { //Enter recipe number - deducts ingredients
int recipeNum = prompt.askForNumber("What would you like to cook? Please enter the recipe number.");
if (recipeNum >= Recipe.values().length || recipeNum < 0)
{
prompt.print("invalid entry");
}
else
{
final Recipe recipeToMake = Recipe.values()[recipeNum];
if (recipeToMake.canMakeRecipeWith(sr))
{
prompt.print(recipeToMake.cookRecipeWith(sr));
}
else
{
prompt.print("not enough ingredients");
}
}
prompt.print("You have cooked a thing! Good job!");
CookAgain(prompt); //cook again? prompt
} //end WantToCook method
public void input(UserPrompt prompt) { // user inputs ingredient counts
//Asks user for ingredients on-hand
prompt.print("Please enter how many of each ingredient you have:");
for (Ingredient each : Ingredient.values())
{
int count = prompt.askForNumber(each.name());
sr.setIngredientCount(each, count);
}
}//end user ingredient input
public void CookAgain(UserPrompt prompt) {
int answer = prompt.askForNumber("Would you like to cook again? 1 = yes, 2 = no - ");
switch (answer) {
case 1: //which means yes
if (Recipe.canCookAnything(sr)) {
prompt.print("You've used some ingredients. Let's see what you have now.");
sr.IngredientCount(prompt);
sr.compareRecipe(prompt);
WantToCook(prompt);
break;
}
else {
prompt.print("You don't have enough ingredients left to cook anything.");
break;
}
case 2: //which means no
prompt.print("Ok, done cooking. Enjoy!");
break;
default:
prompt.print("Invalid answer please try again!");
}// End answer switch
}
}
UserPrompt.java
import java.util.Scanner;
public class UserPrompt
{
private final Scanner kbd = new Scanner(System.in);
public int askForNumber(String prompt)
{
//Print your prompt to the command line -- I added a little extra at the end for simple reading
System.out.print(prompt + " - ");
return kbd.nextInt();
}
public void print(String message)
{
System.out.println(message);
}
}
ShowRecipesNew.java
import java.util.EnumMap;
import java.util.Map;
import java.util.Scanner;
//object class
public class ShowRecipesNew {
//ingredients array - setters will set
private EnumMap<Ingredient, Integer> ingredients = new EnumMap<>(Ingredient.class);
//Basic constructor
public ShowRecipesNew() {
for (Ingredient each : Ingredient.values())
{
ingredients.put(each, each.defaultValue());
}
}
//overloaded constructor that covers each field
public ShowRecipesNew(int apples, int cherries, int carrots, int flour, int sugar) {
this();
System.out.println("overloaded");
setIngredientCount(Ingredient.APPLE, apples);
setIngredientCount(Ingredient.CHERRY, cherries);
setIngredientCount(Ingredient.CARROT, carrots);
setIngredientCount(Ingredient.FLOUR, flour);
setIngredientCount(Ingredient.SUGAR, sugar);
}
public void IngredientCount(UserPrompt prompt) { //Lists ingredient input counts
prompt.print("According to your input, you have:");
prompt.print("Ingredient\tValue");
for(Map.Entry<Ingredient, Integer> each : ingredients.entrySet()) {
prompt.print(each.getKey() + "\t\t" + each.getValue());
}//end for loop
}//end ingredient listing
public void setIngredientCount(Ingredient ingredient, int count)
{
ingredients.put(ingredient, count);
}
public int getIngredientCount(Ingredient ingredient)
{
return ingredients.get(ingredient);
}
public void compareRecipe(UserPrompt prompt) {
prompt.print("According to the input, you can make:");
for (Ingredient each : Ingredient.values())
{
Integer count = getIngredientCount(each);
if (count == null || count == 0)
{
prompt.print("No " + each.name().toLowerCase() + " recipes");
}
}
for (Recipe each : Recipe.values())
{
if (each.canMakeRecipeWith(this))
{
prompt.print(each.ordinal() + " " + each.name());
}
}
}
}
Ingredient.java
public enum Ingredient
{
APPLE(1),
CHERRY(1),
CARROT(1),
FLOUR(0),
SUGAR(0),
;
private final int defaultValue;
Ingredient(int input)
{
defaultValue = input;
}
public int defaultValue()
{
return defaultValue;
}
}
Recipe.java
import java.util.EnumMap;
import java.util.Map;
public enum Recipe
{
APPLE_JAM(
Map.of(
Ingredient.APPLE, 3
)
),
APPLE_JELLY(
Map.of(
Ingredient.APPLE, 2
)
),
APPLE_SMOOTHIE(
Map.of(
Ingredient.APPLE, 2
)
),
APPLE_TART(
Map.of(
Ingredient.APPLE, 1,
Ingredient.FLOUR, 1
)
),
APPLE_PIE(
Map.of(
Ingredient.APPLE, 2,
Ingredient.FLOUR, 1,
Ingredient.SUGAR, 2
)
),
CHERRY_JAM(
Map.of(
Ingredient.CHERRY, 3
)
),
CHERRY_JELLY(
Map.of(
Ingredient.CHERRY, 2
)
),
CHERRY_PIE(
Map.of(
Ingredient.CHERRY, 2,
Ingredient.FLOUR, 3,
Ingredient.SUGAR, 2
)
),
CHERRY_TART(
Map.of(
Ingredient.CHERRY, 1,
Ingredient.FLOUR, 1,
Ingredient.SUGAR, 1
)
),
CARROT_JUICE(
Map.of(
Ingredient.CARROT, 2
)
),
CARROT_CAKE(
Map.of(
Ingredient.CARROT, 1,
Ingredient.FLOUR, 1,
Ingredient.SUGAR, 1
)
),
CARROT_POTAGE(
Map.of(
Ingredient.CARROT, 2,
Ingredient.FLOUR, 1
)
),
;
private final EnumMap<Ingredient, Integer> requiredIngredients;
Recipe(Map<Ingredient, Integer> parameter)
{
requiredIngredients = new EnumMap<>(parameter);
}
public boolean canMakeRecipeWith(ShowRecipesNew sr)
{
for (Map.Entry<Ingredient, Integer> each : requiredIngredients.entrySet())
{
Ingredient ingredient = each.getKey();
int howMuchWeHave = sr.getIngredientCount(ingredient);
int howMuchWeNeed = requiredIngredients.get(ingredient);
if (howMuchWeHave < howMuchWeNeed)
{
return false;
}
}
return true;
}
public String cookRecipeWith(ShowRecipesNew sr)
{
for (Map.Entry<Ingredient, Integer> each : requiredIngredients.entrySet())
{
Ingredient ingredient = each.getKey();
int currentNum = sr.getIngredientCount(ingredient);
int requiredAmount = requiredIngredients.get(ingredient);
sr.setIngredientCount(ingredient, currentNum - requiredAmount);
}
return name();
}
public static boolean canCookAnything(ShowRecipesNew sr)
{
for (Recipe each : Recipe.values())
{
if (each.canMakeRecipeWith(sr))
{
return true;
}
}
return false;
}
}
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | davidalayachew |
Solution 2 | davidalayachew |
Solution 3 | davidalayachew |
Solution 4 | davidalayachew |
Solution 5 | davidalayachew |
Solution 6 | davidalayachew |