- Created by Børge Haugset, last modified by Unknown User (magnuses) on 29.05.2022
Her ligger løsningsforslaget til eksamen 2020. I kombinasjon med kildekoden vil vi beskrive hva oppgaven spurt etter, samt ofte forekommende feil og misforståelser.
På gitlab finner dere full kode til alle deler i oppgaven - lenker nedover i denne teksten refererer til filer her. Etterhvert skal løsningsforslaget legges inn som en egen gren i samme kode.
Eksamensinformasjon
Eksamenen er delt i to, med flere oppgaver i hver del:
- Del 1 inneholder separate oppgaver, som hver for seg tester ulik kunnskap i pensum. Del 1 er tenkt å dekke 25% av eksamen.
- Del 2 inneholder en større programmeringsbit, med fem deloppgaver. Disse teller 75% av eksamen.
Husk at det er lett å fokusere for mye på å oppnå korrekt, kjørende kode. Ikke bruk for mye tid på dette, husk at det vil gi flere poeng å 'nesten klare' tre oppgaver enn å klare én full ut og ikke ha tid til de to siste. Hvis du ikke skulle klare å implementere en metode i en del kan du selvfølgelig bruke denne videre som om den virket (som i tidligere 'papireksamener').
Oppgavene har en tekstbeskrivelse, men denne er ikke alltid utfyllende. De mest utfyllende kravene til en metode står i dens javadoc-beskrivelse, altså en kommentar som står før metoden selv i kildekoden.
Dere vil finne main-metoder med litt kode i enkelte klasser. Denne skal vise forventet oppførsel. Benytt deg gjerne av dette til å teste din egen kode.
Denne delen består av mindre, uavhengige oppgaver som tester spesifikke ferdigheter.
Denne klassen hjelper til med formattering av oppramsinger av typen, "en, to og tre". Poenget er at en har to måter å skille elementer på, én som brukes mellom alle elementene utenom de to siste og én som brukes mellom de to siste. Separatorene (mainSeparator og lastSeparator) kan være like, som i eksempelet "en, to, tre, fire".
- Skriv metodene som mangler i Joiner-klassen.
package stuff; import java.util.Arrays; import java.util.Iterator; public class Joiner { private final String mainSeparator; private final String lastSeparator; /** * Joins the strings together, where all elements but the two last are separated by mainSeparator, and * the two last are separated by lastSeparator. If lastSeparator is null, mainSeparator is used between all elements. * E.g. if you join the strings "one", "two", "three" with mainSeparator as ", " and lastSeparator as " and ", * you should get "one, to and three". * If strings contains only one element, that element is returned, if it contains no elements, the empty string is returned. * @param strings the strings to join * @param mainSeparator the separator used between all but the two last elements * @param lastSeparator the separator used between the last two elements * @return strings joined with the provided separators */ public static String join(final Iterator<String> strings, final String mainSeparator, final String lastSeparator) { final StringBuilder builder = new StringBuilder(); while (strings.hasNext()) { final String item = strings.next(); if (builder.length() > 0) { final String sep = (strings.hasNext() || lastSeparator == null ? mainSeparator : lastSeparator); builder.append(sep); } builder.append(item); } return builder.toString(); } /** * Initialises this Joiner with the provided separators. * @param mainSeparator the separator to use between all but the last two elements * @param lastSeparator the separator to use between the last two elements */ public Joiner(final String mainSeparator, final String lastSeparator) { this.mainSeparator = mainSeparator; this.lastSeparator = lastSeparator; } /** * Initialises this Joiner with the provided separator. * @param separator the separator to use between all elements */ public Joiner(final String separator) { this(separator, null); } /** * Joins strings with the provided mainSeparator and lastSeparator * @param strings the strings to join * @return the joined strings */ public String join(final Iterator<String> strings) { return Joiner.join(strings, mainSeparator, lastSeparator); } /** * Joins strings with the provided mainSeparator and lastSeparator * @param strings the strings to join * @return the joined strings */ public String join(final Iterable<String> strings) { return join(strings.iterator()); } /** * Joins strings with the provided mainSeparator and lastSeparator * @param strings the strings to join * @return the joined strings */ public String join(final String... strings) { return join(Arrays.asList(strings)); } }
Her skulle kandidatene vise kunnskap i manipulering av strenger, iteratorer og iterables. De ulike metodene skal samtidig vise ulike former for iterering – over Iterable, String… og Iterator. Dersom kandidaten kan alle disse (og starter øverst) er de knyttet tett sammen og en slipper å gjøre mye på nummer to og utover.
Typiske feil: Mange har ikke sett at en ved å løse første oppgave kan løse resten med enlinjerskall. De har dermed løst omtrent samme oppgave flere ganger, med gjentakende kode. En del hadde også løst at en tar inn en iterator ved å fylle opp en liste. Det er en liten omvei, men hvis all kode fungerte ga det allikevel full skår.
MedianComputer og MedianComputerTest
Følgende figur fra Wikipedia beskriver beregning av median-verdien til en samling tall.
Deklarer en compute-metode i MedianComputer-klassen som passer for formålet å beregne median.
Skriv en eller flere testmetoder i MedianComputerTest-klassen som tester at compute-metoden virker som den skal, og på en måte som avdekker det du anser som mulige feil.
Du trenger deklarasjonen av compute for å kunne skrive testene, men du trenger ikke implementere den, bare skrive nok kode til at den kompilerer. Om du velger å implementere den, f.eks. for å kunne kjøre testen-klassen, så vil vi ikke vurdere implementasjonen, kun deklarasjonen og test-logikken. Legg merke til behovet for avrunding i AssetEquals, eksempel på toppen av koden.
Et eksempel på implementasjon av MedianComputer:
package stuff; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; public class MedianComputer { public static double compute(final Collection<Double> nums) throws IllegalArgumentException { if (nums.size() < 1) { throw new IllegalArgumentException("Cannot compute median of no values"); } final List<Double> sorted = new ArrayList<>(nums); Collections.sort(sorted); final int size = sorted.size(); if (size % 2 == 0) { return (sorted.get(size / 2 - 1) + sorted.get(size / 2)) / 2; } else { return sorted.get(size / 2); } } }
Eksempel på tester for flere ulike typer feil:
package stuff; import static org.junit.Assert.fail; import java.util.List; import org.junit.Assert; import org.junit.Test; public class MedianComputerTest { // use as third argument to assertEquals method for handling round-off errors double roundErrorDelta = 0.00000001; @Test public void testComputeEvenSize() { Assert.assertEquals(6.0, MedianComputer.compute(List.of(1.0, 3.0, 3.0, 6.0, 7.0, 8.0, 9.0)), roundErrorDelta); } @Test public void testComputeOddSize() { Assert.assertEquals(4.5, MedianComputer.compute(List.of(1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 8.0, 9.0)), roundErrorDelta); } @Test public void testComputeSize1() { Assert.assertEquals(4.5, MedianComputer.compute(List.of(4.5)), roundErrorDelta); } @Test public void testComputeSize2() { Assert.assertEquals(4.5, MedianComputer.compute(List.of(4.0, 5.0)), roundErrorDelta); } @Test public void testComputeSize0() { try { MedianComputer.compute(List.of()); fail(); } catch (final Exception e) { Assert.assertTrue(e instanceof IllegalArgumentException); } } }
Her kunne de (hvis de ønsket) implementere MedianComputer, men målet var egentlig å skrive tester som viser at de kan dekke krav til en metode, spesielt grensetilfeller. Beregner den korrekt null elementer, ett, to, odde eller like antall elementer. Poeng gis etter hvor stor del av disse en tester for. Det var ikke behov for å lage en fullstendig kode for MedianComputer her – oppgaveteksten sier at en bare må ha noe som kompilerer.
Summer-klassen inneholder tre metoder med kode som virker i noen tilfeller (bl.a. de som testes av eksisterende testmetoder i SummerTest), men feiler i andre. Oppgaven går ut på å utvide SummerTest-klassen med ekstra testmetoder som avdekker disse feilene på en hensiktsmessig måte. Du skal altså ikke fikse på koden, bare lage testene.
- Skriv nødvendige metoder i SummerTest-klassen.
package stuff; import java.util.List; import org.junit.Assert; import org.junit.Test; public class SummerTest { @Test public void testSum1() { Assert.assertEquals(6, Summer.sum(List.of(1, 2, 3))); } @Test public void testSumMistake() { Assert.assertEquals(0, Summer.sum(List.of())); } @Test public void testDifference1() { Assert.assertEquals(0, Summer.difference(List.of(6, 1, 2, 3))); } @Test public void testDifferenceMistake() { try { Summer.difference(List.of()); Assert.fail("Empty list should throw IllegalArgumentException"); } catch (final Exception e) { Assert.assertTrue("Empty list should throw IllegalArgumentException", e instanceof IllegalArgumentException); } } }
Her er det altså SummerTest.java som skrives av kandidaten, ikke Summer.java. Målet er at de skal forstå koden til Summer.java, og klarer å skrive to tester som feiler (suksess her er gjerne 1 error og en failure, hvis testene gjør det de skal.)
- testSumMistake() skal forvente at Summer.sum() med en tom liste blir 0. Det blir den jo ikke..
- testDifferenceMistake() skal teste at difference.of() med en tom liste ikke utløser en IllegalArgumentException, slik javadoc beskriver.
Le Petite Chef
(Hvis du ønsker å lese denne filen med pent utseende i Eclipse, husk at du kan trykke på 'Preview'-fanen nede til venstre i md-editoren).
Le Petite Chef er en liten familierestaurant. Du har fått i oppdrag å lage et system for å holde styr på oppskrifter, et lager av matvarer (ingredienser) og innkjøp. Kokken skal bl.a. kunne bestemme hvilke oppskrifter som skal brukes neste uke (med antall), og dette skal kunne brukes ved innkjøp av matvarer.
Dette eksamenssettet bygger på at du skal utvide et eksisterende rammeverk. De fleste klassene eksisterer allerede. Dersom du har behov for å lage noen ekstra, for eksempel hjelpeklasser, så står du fritt til det. Hvis du mener at det kan det være lurt å beskrive valgene, se avsnitt om filen Oppgavekommentarer.md under.
Sentrale klasser
Sentralt i oppgavene står håndtering av matvarer/ingredienser. F.eks. inneholder både oppskrifter og matlageret en mengde/antall av et sett med ingredienser. En ingrediens representeres med en String og mengden/antallet av ingrediensen med et desimaltall (double).
Dette er et grensesnitt med metoder for å lese ut og gå gjennom et sett ingredienser og deres mengder/antall og metoder knyttet til sammenligning av sett med ingredienser. Den primære implementasjonen av Ingredients er IngredientsContainer.
Denne klassen er en konkret implementasjon av Ingredients, med ekstra metoder for å endre (legge til og fjerne) ingredienser. Denne kan brukes av andre klasser som trenger å håndtere ingredienser.
Recipe-klassen representerer en oppskrift med et sett tilhørende ingredienser for et visst antall porsjoner. Oppskrifter kan leses inn fra fil vha. RecipeReader-klassen.
Kjøkkenet har en oversikt over alle kjente oppskrifter og de oppskriftene som skal brukes i ukemenyen. I tillegg har den et lager av matvarer, som må etterfylles når en ny ukesmeny legges inn.
Oppgavestruktur
Le Petit Chef er delt i 5 oppgaver, som er naturlige å gjøre i rekkefølge. Du bør likevel tenke på det som en helhet, og det er sluttresultatet som skal leveres inn. Klassene er allerede opprettet og inneholder (noen av de nødvendige) metodene med javadoc og // TODO-kommentarer som indikerer hva som må implementeres.
Her beskrives trinnene skissemessig, detaljer finnes i oppgavebeskrivelsen. Lenkene under går til toppen av oppgavebeskrivelsen, ikke til de respektive oppgavene. (Tekstene i oppgavebeskrivelsen er nå kopiert inn i hver enkelt underpunkt, før javakoden.)
Oppgave 1 handler om IngredientContainer, som implementerer Ingredients, Recipe og RecipeReader.
Oppgave 2 handler om Kitchen. Denne inneholder et sett med oppskrifter, et lager med ingredienser og neste ukes meny.
Oppgave 3 handler om delegering, der du skal implementere en ny metode i Recipe
Oppgave 4 handler om observerbarhet, der noen skal få beskjed når kokken har laget ferdig neste ukes meny.
Oppgave 5 handler om en JavaFX-app der kokken kan legge inn neste ukes meny.
Denne delen handler om IngredientContainer, Recipe og RecipeReader.
IngredientContainer holder oversikt over antall/mengde av et sett med ingredienser og er den primære implementasjonen av Ingredients. Siden Ingredients bare inneholder metoder for å lese ut informasjon, må IngredientContainer naturlig nok legge til metoder for å endre antallet/mengden som finnes av hver ingrediens.
Recipe (oppskrift) representerer ingrediensene som trengs for en viss mengde porsjoner av en matrett (eller drikk). Hjelpeklassen RecipeReader håndterer innlesing av oppskrifter fra en kilde, f.eks. en fil.
Oppgaven
Implementer nødvendige metoder (og felt og hjelpeklasser og -metoder) i
Dersom du ikke klarer å lese oppskrifter fra fil, så kan du senere benytte deg av den statiske metoden Recipe.createSampleRecipes(). Denne vil returnere en samling av oppskrifter som du kan bruke. Du finner et eksempel på dette i KitchenController.java som brukes i oppgave 5.
package food; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.Map; /** * Generic container of ingredients. * Uses a Map for managing relations between ingredient (name) and amount */ public class IngredientContainer implements Ingredients { private Map<String, Double> ingredientMap; // package private for testing purposes IngredientContainer(Map<String, Double> ingredientMap) { this.ingredientMap = ingredientMap; } public IngredientContainer() { this(new HashMap<>()); } /** * Creates a new IngredientContainer. * * @param ingredients A map containing the ingredients currently in the container * @param isEditable Controls whether this container is editable or not */ public IngredientContainer(Ingredients ingredients) { this(); addIngredients(ingredients); } /** * Add `amount` of `ingredient` to the container. * * @param ingredient The name of the ingredient to add * @param amount The amount of the ingredient to add * Wrongly added: @throws (fitting subclass of) RuntimeException if amount cannot be removed from this */ public void addIngredient(String ingredient, double amount) { ingredientMap.put(ingredient, getIngredientAmount(ingredient) + amount); } // Own helper method. private static void removeIngredientHelper(Map<String, Double> ingredientMap, String ingredient, double amount) { double totalAmount = ingredientMap.getOrDefault(ingredient, 0.0); if (totalAmount < amount) { throw new IllegalArgumentException("Not enough left of ingredient '" + ingredient + "' in container."); } if (totalAmount - amount == 0) { ingredientMap.remove(ingredient); } else { ingredientMap.put(ingredient, totalAmount - amount); } } /** * Remove `amount` of `ingredient` to the container. * * If the resulting amount of the ingredient is 0, its name should be removed * * @param ingredient The name of the ingredient to add * @param amount The amount of the ingredient to remove * @throws IllegalArgumentException if amount cannot be removed */ public void removeIngredient(String ingredient, double amount) { removeIngredientHelper(ingredientMap, ingredient, amount); } /** * @return An Iterable giving the names of all the ingredients */ @Override public Iterable<String> ingredientNames() { return ingredientMap.keySet(); } /** * @return A collection containing the names of all the ingredients */ @Override public Collection<String> getIngredientNames() { return new ArrayList<>(ingredientMap.keySet()); } /** * @param ingredient The ingredient to get the amount of * If the ingredient does not exist, the double 0.0 should be returned. * @return The amount of ingredient */ @Override public double getIngredientAmount(String ingredient) { return ingredientMap.getOrDefault(ingredient, 0.0); } /** * Get a string containing the ingredients with amounts in the format given below: * * ingredientName1: amount1 * ingredientName2: amount2 * ingredientName3: amount3 * ... * * @return A string on the format above */ @Override public String toString() { StringBuilder sb = new StringBuilder(); for (String ingredient : ingredientMap.keySet()) { sb.append(String.format("%s: %.1f\n", ingredient, ingredientMap.get(ingredient))); } return sb.toString(); } /** * Add all ingredients in other into this. * * @param ingredients the ingredients to add */ public void addIngredients(Ingredients ingredients) { for (String ingredient : ingredients.ingredientNames()) { addIngredient(ingredient, ingredients.getIngredientAmount(ingredient)); } } /** * Removes all ingredients in other from this. * * @param ingredients the ingredients to add * @throws IllegalArgumentException if this does not contain enough (without changing this) */ public void removeIngredients(Ingredients ingredients) { Map<String, Double> newIngredientMap = new HashMap<>(ingredientMap); for (String ingredient : ingredients.ingredientNames()) { removeIngredientHelper(newIngredientMap, ingredient, ingredients.getIngredientAmount(ingredient)); } this.ingredientMap = newIngredientMap; } /** * Checks if the all the ingredients in other is contained in this * @param other * @return true of there is at least the same or larger amount of ingredients in this than in other, false otherwise */ @Override public boolean containsIngredients(Ingredients other) { for (String ingredient : other.ingredientNames()) { if (other.getIngredientAmount(ingredient) > getIngredientAmount(ingredient)) { return false; } } return true; } @Override public Ingredients missingIngredients(Ingredients other) { Map<String, Double> newIngredientMap = new HashMap<>(); for (String ingredient : ingredientNames()) { double diff = getIngredientAmount(ingredient) - other.getIngredientAmount(ingredient); if (diff > 0) { newIngredientMap.put(ingredient, diff); } } return new IngredientContainer(newIngredientMap); } @Override public Ingredients scaleIngredients(double scale) { Map<String, Double> newIngredientMap = new HashMap<>(); for (String ingredient : ingredientNames()) { newIngredientMap.put(ingredient, getIngredientAmount(ingredient) * scale); } return new IngredientContainer(newIngredientMap); } }
En av de største oppgavene. Her er det mange ulike tema som dekkes: Klarer kandidaten på egenhånd å avgjøre hvordan man skal lagre ingredienser og mengden av dem? (map eller to lister? Vi gir poeng for begge, såfremt de virker, men det er viktig at de ikke åpner opp implementasjonsløsningen ut over grensesnittet som er definert).
- Får kandidaten opprettet en tom IngredientContainer, sikrer de at interne maps/lister er initialisert? Det finnes en del .add til ikke-eksisterende samlinger her...
- IngredientContainer(Ingredients): Bruker kandidaten this.addIngredients(Ingredients) her, eller lager hen lik kode flere steder?
- addIngredient(ingredient, amount): Håndterer kandidaten basismanipulasjon av en map eller to lister? Er variablene initialisert på forhånd? Fungerer koden med eksisterende ingrediens, eller at det er tomt?
- removeIngredient(ingredient, amount): Nok en basismanipulasjon av intern tilstand, men denne skulle også teste om de klarer å utløse unntak (og da ikke endre noe som helst i tilstanden)
- IngredientNames(): Klarer kandidaten å gjøre om den interne tilstanden av ingredienser til en iterator<String>? Basis konvertering, avhengig av egen løsning.
- getIngredientNames(): nok en (enkel) overgang fra intern tilstand til ekstern representasjon. Har kandidaten husket på å sende inn en kopi av ingrediensene, og ikke originalen? (gjelder også i andre metoder)
- getIngredientAmount(ingredient): Viktigste spesielle her er at kandidaten behersker retur av 0.0 hvis ingrediens ikke finnes, så løsning avhenger av implementasjon.
- toString(): Basis strenghåndtering – klarer kandidaten å hente ut informasjon fra tilstand og få det til å se riktig ut.
- addIngredients(Ingredients): Hvordan klarer kandidaten å forholde seg til å få inn sitt eget grensesnitt? Denne viser om kandidaten klarer å se sammenhengen mellom eksempelvis getIngredientNames(), addIngredient(ing, amt) og getIngredientAmount()
- removeIngredient(Ingredient): Denne er litt mer komplisert, og tester spesielt om kandidaten klarer å utløse unntak. Viktig her er at unntaket skal ende opp med at en ikke endrer intern tilstand i det hele tatt, ikke for noen ingredienser. Hvordan løser kandidaten denne floken? Klarer kandidaten å bruke eksisterende kode for iterasjon gjennom Ingredient?
- containsIngredients(Ingredients): Ingredients har en drøss med ingredienser, og en mengde av dem. Inneholder this nok av alle? Avhengig av implementasjonsvalg må en løpe igjennom alle ingredienser og sjekke mengde. Er én av this under i mengde, returner false, etter loopen true. Basisforståelse av løkker knyttet til egne valg. Krever dog forståelse av Ingredients-grensesnittet.
- missingIngredients(Ingredients): Samme som forrige, men her går det på å regne ut forskjellen og legge ingrediensene en manglet noe av inn i en ny Ingredients (via IngredientsContainer, siden det er implementasjonen). Metoden tester evne til å holde styr på koblingen mellom grensesnittet Ingredients og implementasjonen (og utvidelsen) IngredientContainer.
- scaleIngredients(scale): Klarer kandidaten å skalere this? Her må en altså holde styr på alle ingredienser, multiplisere mengden av dem med parameteren, og legge koblingen mellom alle disse inn i en ny Ingredients (via IngredientsContainer). Viktige ting her er om de skalerer korrekt, og om det returneres et nytt IC-objekt (det blir feil dersom en endrer mengden av ingredienser i dette objektet, this brukes kun som referanse her)
package food; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Map; public class Recipe { private final String name; private final String category; private final int nPortions; private final Ingredients ingredients; public Recipe( final String name, final String category, final int nPortions, final Ingredients ingredients ) { this.name = name; this.category = category; this.nPortions = nPortions; this.ingredients = ingredients; } public String getName() { return name; } public String getCategory() { return category; } public int getNPortions() { return nPortions; } public Ingredients getIngredients() { return ingredients; } @Override public String toString() { return "Recipe [name=" + name + ", category=" + category + ", nPortions=" + nPortions + ", ingredients=" + ingredients + "]"; } /** * Create a copy of this recipe, except change the number of portions it creates * * @param n The number of portions to make food for * @return The new recipe */ public Recipe createNPortions(final int n) { final double scale = ((double) n) / nPortions; return new Recipe(name, category, n, ingredients.scaleIngredients(scale)); } public static Collection<Recipe> createSampleRecipes() { Collection<Recipe> recipes = new ArrayList<>(); IngredientContainer ig; ig = new IngredientContainer(); ig.addIngredient("Egg Yolks", 3.00); ig.addIngredient("Lime", 4.00); ig.addIngredient("Double Cream", 300.00); ig.addIngredient("Condensed Milk", 400.00); ig.addIngredient("Butter", 150.00); ig.addIngredient("Digestive Biscuits", 300.00); ig.addIngredient("Icing Sugar", 1.00); recipes.add(new Recipe("Key Lime Pie", "Dessert", 4, ig)); ig = new IngredientContainer(); ig.addIngredient("Water", 1.00); ig.addIngredient("Garlic Clove", 3.00); ig.addIngredient("Sesame Seed Oil", 5.00); ig.addIngredient("Carrots", 3.00); ig.addIngredient("Wonton Skin", 1.00); ig.addIngredient("Oil", 1.00); ig.addIngredient("Celery", 3.00); ig.addIngredient("Soy Sauce", 15.00); ig.addIngredient("Ginger", 5.00); ig.addIngredient("Spring Onions", 6.00); ig.addIngredient("Pork", 1.00); recipes.add(new Recipe("Wontons", "Pork", 4, ig)); ig = new IngredientContainer(); ig.addIngredient("Strawberries", 300.00); ig.addIngredient("Caster Sugar", 175.00); ig.addIngredient("Raspberries", 500.00); ig.addIngredient("Blackberries", 250.00); ig.addIngredient("Redcurrants", 100.00); ig.addIngredient("Bread", 7.00); recipes.add(new Recipe("Summer Pudding", "Dessert", 4, ig)); ig = new IngredientContainer(); ig.addIngredient("Chicken Stock", 1.00); ig.addIngredient("Honey", 1.00); ig.addIngredient("Broccoli", 1.00); ig.addIngredient("Balsamic Vinegar", 1.00); ig.addIngredient("Potatoes", 5.00); ig.addIngredient("Butter", 15.00); ig.addIngredient("Garlic", 2.00); ig.addIngredient("Vegetable Oil", 15.00); ig.addIngredient("Olive Oil", 15.00); ig.addIngredient("Chicken Breast", 2.00); recipes.add(new Recipe("Honey Balsamic Chicken with Crispy Broccoli & Potatoes", "Chicken", 4, ig)); ig = new IngredientContainer(); ig.addIngredient("Water", 1.00); ig.addIngredient("Salt", 1.00); ig.addIngredient("Egg", 1.00); ig.addIngredient("Starch", 10.00); ig.addIngredient("Coriander", 1.00); ig.addIngredient("Soy Sauce", 10.00); ig.addIngredient("Tomato Puree", 30.00); ig.addIngredient("Vinegar", 10.00); ig.addIngredient("Pork", 200.00); ig.addIngredient("Sugar", 5.00); recipes.add(new Recipe("Sweet and Sour Pork", "Pork", 4, ig)); ig = new IngredientContainer(); ig.addIngredient("basil", 1.00); ig.addIngredient("oregano", 1.00); ig.addIngredient("allspice", 1.00); ig.addIngredient("Flour", 2.00); ig.addIngredient("Egg White", 1.00); ig.addIngredient("black pepper", 5.00); ig.addIngredient("celery salt", 1.00); ig.addIngredient("Salt", 1.00); ig.addIngredient("onion salt", 2.00); ig.addIngredient("garlic powder", 1.00); ig.addIngredient("Oil", 2.00); ig.addIngredient("paprika", 1.00); ig.addIngredient("marjoram", 1.00); ig.addIngredient("chili powder", 5.00); ig.addIngredient("sage", 1.00); ig.addIngredient("Chicken", 1.00); ig.addIngredient("Brown Sugar", 1.00); recipes.add(new Recipe("Kentucky Fried Chicken", "Chicken", 4, ig)); ig = new IngredientContainer(); ig.addIngredient("Hotsauce", 1.00); ig.addIngredient("Potatoes", 2.00); ig.addIngredient("Pepper", 1.00); ig.addIngredient("Red Onions", 1.00); ig.addIngredient("Vegetable Oil", 1.00); ig.addIngredient("Bread", 2.00); ig.addIngredient("Lime", 1.00); ig.addIngredient("Salt", 1.00); ig.addIngredient("Barbeque Sauce", 1.00); ig.addIngredient("Garlic", 2.00); ig.addIngredient("Tomato Ketchup", 1.00); ig.addIngredient("Pork", 1.00); ig.addIngredient("Sugar", 1.00); recipes.add(new Recipe("BBQ Pork Sloppy Joes with Pickled Onion & Sweet Potato Wedges", "Pork", 4, ig)); ig = new IngredientContainer(); ig.addIngredient("Vanilla Extract", 1.00); ig.addIngredient("Oil", 5.00); ig.addIngredient("Raspberries", 125.00); ig.addIngredient("Pecan Nuts", 25.00); ig.addIngredient("Eggs", 2.00); ig.addIngredient("Baking Powder", 1.00); ig.addIngredient("Banana", 1.00); recipes.add(new Recipe("Banana Pancakes", "Dessert", 4, ig)); ig = new IngredientContainer(); ig.addIngredient("Garlic Clove", 4.00); ig.addIngredient("Plain Flour", 30.00); ig.addIngredient("Gruyère", 140.00); ig.addIngredient("Dry White Wine", 250.00); ig.addIngredient("Onion", 1000.00); ig.addIngredient("Butter", 50.00); ig.addIngredient("Olive Oil", 15.00); ig.addIngredient("Sugar", 5.00); ig.addIngredient("Beef Stock", 1.00); ig.addIngredient("Bread", 4.00); recipes.add(new Recipe("French Onion Soup", "Side", 4, ig)); ig = new IngredientContainer(); ig.addIngredient("garlic", 3.00); ig.addIngredient("bowtie pasta", 16.00); ig.addIngredient("Pepper", 1.00); ig.addIngredient("onion", 1.00); ig.addIngredient("Butter", 2.00); ig.addIngredient("milk", 1.00); ig.addIngredient("Olive Oil", 3.00); ig.addIngredient("Squash", 1.00); ig.addIngredient("Parmesan cheese", 1.00); ig.addIngredient("mushrooms", 8.00); ig.addIngredient("Salt", 5.00); ig.addIngredient("Broccoli", 1.00); ig.addIngredient("white wine", 1.00); ig.addIngredient("red pepper flakes", 1.00); ig.addIngredient("heavy cream", 1.00); ig.addIngredient("Chicken", 5.00); ig.addIngredient("Parsley", 1.00); recipes.add(new Recipe("Chicken Alfredo Primavera", "Chicken", 4, ig)); return recipes; } }
Denne klassen skulle dekke to ulike ting:
- Konstruktøren: En helt basis konstruktør, som handler om å knytte parametre til riktige interne variable.
- createNPortions(n): Recipe inneholder et Ingredients-objekt samt et tall som beskriver hvor mange porsjoner denne oppskriften er beregnet for. Metoden tester om kandidaten kan lage en ny Recipe som er skalert etter n porsjoner. Her testes evnen til å se at Ingredients allerede har en metode for skalering av mengder, denne bør brukes i stedet for en ny implementasjon her. I tillegg er det viktig at det returneres et nytt Recipe-objekt. En typisk feil her er å glemme cast fra int til double før en deler n på this.nPortions – da blir resultatet feil.
package food; import java.io.BufferedReader; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.util.ArrayList; import java.util.List; public class RecipeReader { private final String columnSeparatorRegex = "\\$"; /** * Read recipes from an InputStream with the given format: * * name$category$nPortions$ingredient1;ingredient2;...;...$amount1;amount2;...;... * * As you see from the format, each recipe is a single line, with fields separated by `$` and * elements in lists separated by `;`. * * Regarding ingredients and amounts, the two lists are sorted in the same order, so `ingredient1` * should have `amount1`, and so forth. All amounts can be read as doubles, while nPortions is an integer. * * Note that the first line of the stream is the header, and so should not be used. * If a line (i.e. a single recipe) fails to be parsed correctly, that recipe is to be skipped. * * @param input The source to read from * @throws IOException if input (InputStream) throws IOException */ public List<Recipe> readRecipes(final InputStream input) throws IOException { final List<Recipe> recipes = new ArrayList<>(); try (BufferedReader reader = new BufferedReader(new InputStreamReader(input))) { String line = null; reader.readLine(); while ((line = reader.readLine()) != null) { try { final String[] data = line.split(columnSeparatorRegex, -1); final String name = data[0]; final String category = data[1]; final int port = Integer.parseInt(data[2]); final IngredientContainer ingredients = new IngredientContainer(); final String[] ingredientNames = data[3].split(";", -1); final String[] ingredientAmounts = data[4].split(";", -1); for (int i = 0; i < ingredientNames.length; i++) { ingredients.addIngredient(ingredientNames[i], Double.parseDouble(ingredientAmounts[i])); } recipes.add(new Recipe(name, category, port, ingredients)); } catch (final RuntimeException e) { System.out.println(e.toString()); } } } catch (final NullPointerException e) { throw new IOException(); } return recipes; } public static void main(final String[] args) throws IOException { System.out.println(new RecipeReader().readRecipes(RecipeReader.class.getResourceAsStream("sample-recipes.txt"))); } }
Her skal kandidaten først og fremst vise at hen får lest informasjon fra fil, og håndtert separatorer. I tillegg til dette er det lagt inn en del ekstra små detaljer som krever oppmerksomhet: Den første linjen inneholder navn på feltene som kommer under. Denne må behandles spesielt. Den kan ikke være del av den store løkken, men må også tas vare på. Vi ønsker gjerne at den skal håndtere linjer som ikke følger malen. Til slutt skal metodekallet ReadRecipes returnere IOException hvis input (InputStream) utløser det en IOException (den skal med andre ord sende unntaket videre, så her skal kandidaten vise at hen behersker ‘throws IOException’.
Det var valgfritt for kandidatene hvordan de håndterte innlesing, men det var definitivt enklest å basere seg på en linjebasert lesing.
Denne delen handler om Kitchen (uten observatør, de kommer i oppgave 4).
Kjøkkenet har et sett med oppskrifter (Recipe, som angis i konstruktøren). Noen metoder (getRecipe og filterRecipes) håndterer spørring om disse.
Kjøkkenet har et matlager (getStorage), som må være fylt med nok matvarer til å kunne lage rettene. Derfor har klassen metoder fokusert på sammenheng mellom oppskrifter og matlageret (canCreateRecipe, getRecipesThatCanBeCreated, getRecipiesContainingIngredient).
Etter at restauranten er stengt for uken planlegger kokken neste ukes meny (vha. metodene addRecipeToWeekly, removeRecipeFromWeekly og clearWeekly). Denne består av et sett med eksisterende oppskrifter og et visst antall porsjoner av dem. Når alle oppskriftene er lagt inn kalles registerWeekly, der man f.eks. kunne tenke seg at en handleliste skal lages. Du trenger ikke å implementere funksjonalitet for en slik handleliste eller annen bruk av ukesmenyen.
Oppgaven
Implementer nødvendige metoder (og felt og hjelpeklasser og -metoder) i
package food; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.HashSet; import java.util.function.Predicate; import java.util.stream.Collectors; /* * Denne klassen bruker aktivt 'final' på variable - dette er ikke noe krav. */ public class Kitchen { private final IngredientContainer storage = new IngredientContainer(); private final Collection<Recipe> allRecipes = new ArrayList<>(); private final Collection<Recipe> weeklyRecipes = new ArrayList<>(); private final Collection<KitchenObserver> observers = new HashSet<>(); /** * Create a new kitchen with the given recipes * @param recipes The recipes the kitchen knows */ public Kitchen(final Recipe...recipes) { super(); allRecipes.addAll(Arrays.asList(recipes)); } /** * @return The kitchen's ingredient storage */ public IngredientContainer getStorage() { return storage; } /** * Lookup a recipe in the kitchen's known recipes by name. * * If no recipe with the given name is found, `null` is returned. * * @param name The name of the recipe * @return The recipe with the given name, or `null` if no such recipe exists */ public Recipe getRecipe(final String name) { return allRecipes.stream().filter(recipe -> recipe.getName().equals(name)).findFirst().orElse(null); } /** * @return A collection of all the recipes the kitchen knows */ public Collection<Recipe> getAllRecipes() { return new ArrayList<>(allRecipes); } /** * Generic filter method on a predicate, can be used for more specific filtering * * @param predicate The predicate to filter on * @return The filtered recipes */ public Collection<Recipe> filterRecipes(final Predicate<Recipe> predicate) { return allRecipes.stream().filter(predicate).collect(Collectors.toList()); } /** * Add a recipe to the recipes of the week * @param recipe The recipe to add */ public void addRecipeToWeekly(final Recipe recipe) { weeklyRecipes.add(recipe); } /** * Remove a recipe from the recipes of the week * @param recipe The recipe to remove */ public void removeRecipeFromWeekly(final Recipe recipe) { weeklyRecipes.remove(recipe); } /** * Clear the recipes of the week. Useful for selecting next weeks * recipes. */ public void clearWeekly() { weeklyRecipes.clear(); } /** * This method should be called when the chef has finished setting * all recipes for the next week, and can be used to trigger * fitting behavior. */ public void registerWeekly() { notifyObservers(); } /** * @return The list of recipes the kitchen will create this week */ public Collection<Recipe> getWeeklyRecipes() { return new ArrayList<>(weeklyRecipes); } /** * Check if this kitchen has the ingredients needed to create the given recipe * * @param recipe The recipe to check for * @return true if the kitchen has enough ingredients */ public boolean canCreateRecipe(final Recipe recipe) { return storage.containsIngredients(recipe.getIngredients()); } /** * Create the given recipe, i.e. remove the amount of ingredients from this kitchen's storage. * * @param recipe The recipe to create * @throws an appropriate RuntimeException if there's not enough ingredients to create the given recipe. */ public void createRecipe(final Recipe recipe) { storage.removeIngredients(recipe.getIngredients()); notifyObservers(); } /** * Get all recipes that can be created with the current ingredient store of this kitchen. * * @return The filtered recipes */ public Collection<Recipe> getRecipesICanCreate() { return filterRecipes(this::canCreateRecipe); } /** * Another possible filtering method (probably no point having both this and previous, and previous is likely more interesting) * * @param ingredient The ingredient to search for * @return The filtered recipes */ public Collection<Recipe> getRecipiesContainingIngredient(final String ingredient) { return filterRecipes(recipe -> recipe.getIngredients().getIngredientNames().contains(ingredient)); } // From this point on: Observable stuff public void addObserver(final KitchenObserver obs) { observers.add(obs); } public void removeObserver(final KitchenObserver obs) { observers.remove(obs); } public void notifyObservers() { observers.forEach(obs -> obs.update(this)); } }
En mengde metoder, men de flest er svært korte hvis en forstår hva som skal gjøres. Kitchen bruker alle klassene som er introdusert til nå. Her tester vi litt mer intrikat manipulasjon, inkludert funksjonelle grensesnitt. Videre må besvarelsen ha definert (og initialisert) et passende sett med variable.
- Kitchen(Recipe…): Hvordan klarer kandidaten å lage Kitchen med 0, 1 eller mange Recipe som parametre? (for informasjon om ‘…’ se https://docs.oracle.com/javase/tutorial/java/javaOO/arguments.html#varargs.
- getStorage(): Enkel getter, basert på egen implementasjon.
- getRecipe(String): En oppgave som tester evne til filtrering. Her passet det perfekt å bruke Collection sin stream(), inkludert en .filter() som inneholder en lambda. Men det går an å gjøre det med en vanlig for-løkke også.
- getAllRecipes(): hente ut (en kopi) av all oppskrifter i Kitchen. Her står det ikke klart definert at det skal være en kopi, men vanlig innkapslingstankegang er at en ikke skal tillate endring av et objekts tilstand utenfra. Av det følger det at en må returnere en kopi av oppskriftene.
- filterRecipes(Predicate): Som getRecipe over, men her presses en til å bruke et oppgitt predikat. getRecipe kunne løses uten å kunne funksjonelle grensesnitt, denne ikke. En trenger derimot ikke kunne streams for å løse oppgaven, det er bare mye mer effektivt akkurat her.
- canCreateRecipe(Recipe): Kjøkkenet har et lager implementert ved IC. Ser kandidaten at denne metoden løses ved et kall til this.<storagevariablename>.containsIngredients()?
- createRecipe(): Tunga rett i munnen: Hvis det er nok ingredienser til å lage retten skal denne lages (gjør ikke noe i denne oppgaven) og fjerne brukte ingredienser fra lageret. Og så skal den selvsagt utløse RunTimeException hvis matretten ikke kan lages fordi det mangler ingredienser. Og det gjør jo IC.removeIngredients (som må/bør brukes) allerede. Her er det viktig at man ikke begynner å fjerne ingredienser før en sjekker om en faktisk har alt!
- recipesICanCreate(): Her skal en gå igjennom alle oppskrifter som er kjent i Kitchen, og oppdatere en samling med oppskrifter en kan lage gitt ingrediensene en har tilgjengelig. Det er mange måter å skrive dette på, men den enkleste er nok å benytte seg av at Kitchen.canCreateRecipe(Recipe) returnerer en boolsk operator. Hvor passer det perfekt å bruke denne? Jo, i Kitchen.filterRecipes(Predicate)
- getRecipesContainingIngredients(String): En litt mer avansert filtreringsoppgave, der en må filtrere alle oppskriftene som inneholder én ingrediens. Kan løses med en litt intrikat forløkke, eller en stream().(…).
- addRecipeToWeekly, removeRecipeFromWeekly, clearWeekly: Har kandidaten initiert en passende variabel for å løse disse oppgavene? Ellers basiskode.
- getWeeklyRecipes(): Returnere en kopi av intern tilstand.
- registerWeekly(): Denne er til observatør-observert i oppgave 4.
En skalert oppskrift, som den som returneres av Recipe sin createNPortions-metode, kan ha nytte av en egen implementasjon av Ingredients implementert vha. delegering.
Oppgaven
Implementer en skalert oppskrift vha. delegering og bruk denne. Du skal ikke endre metoden createNPortions som du lagde i oppgave 1, i stedet skal du lage en ekstra metode createNPortionsUsingDelegation. Merk: Du skal ikke bruke IngredientContainer.scaleIngredients(), klassen som får ansvaret delegert skal også implementere skaleringen.
package food; import java.util.Collection; public class ScaledIngredients implements Ingredients { private final Ingredients ingredients; private final double scale; public ScaledIngredients(final Ingredients ingredients, final double scale) { if (scale <= 0.0) { throw new IllegalArgumentException("Scale must be positive, but was " + scale); } this.ingredients = ingredients; this.scale = scale; } @Override public Iterable<String> ingredientNames() { return ingredients.ingredientNames(); } @Override public Collection<String> getIngredientNames() { return ingredients.getIngredientNames(); } @Override public double getIngredientAmount(final String ingredient) { return ingredients.getIngredientAmount(ingredient) * scale; } @Override public boolean containsIngredients(final Ingredients other) { for (final String ingredient : other.ingredientNames()) { if (other.getIngredientAmount(ingredient) > getIngredientAmount(ingredient)) { return false; } } return true; } @Override public Ingredients missingIngredients(final Ingredients other) { final IngredientContainer ingredients = new IngredientContainer(); for (final String ingredient : ingredientNames()) { final double diff = getIngredientAmount(ingredient) - other.getIngredientAmount(ingredient); if (diff > 0) { ingredients.addIngredient(ingredient, diff); } } return ingredients; } @Override public Ingredients scaleIngredients(final double scale) { return new ScaledIngredients(this, scale); } }
Her skulle kandidatene vise at de behersket delegering (og lesing av kravspesifikasjon). Mens IC tidligere hadde skalering selv, så skulle dette arbeidet overføres til en egen klasse ScaledIngredients. Det stod spesifisert at en ikke skulle benytte seg av IC.scaledIngredients(), en skulle altså late som om denne metoden ikke eksisterte. Poenget med å beskrive det på denne måten er at en da måtte implementere metodene fra Ingredients her. Noen av dem måtte gjøres på litt andre måter enn i IC, spesielt er containsIngredients og missingIngredients metoder som krever litt mer. Det gir litt, men dårlig uttelling å kalle bare dra ut ScaledIngredients, og så kalle alt av metoder fra IC. Det er ikke sånn delegering skal fungere.
Typiske feil: Mange valgte å hoppe over denne oppgaven, og den er også oppgaven med dårligst snitt. En typisk feil var, som beskrevet over, at en bare lar ScaledIngredients kalle relevante metoder fra IC. Denne løsningen er helt i strid med oppgaveteksten som sa at en ikke skulle gjøre dette, og ble dermed heller ikke premiert veldig godt.
Restauranten er stengt på søndager. Da setter kokken seg ned og lager neste ukes meny.
- Menyen for den inneværende uken slettes.
- Kokken velger ut hvilke retter som skal serveres, og hvor mange porsjoner av hver det skal kjøpes inn til.
- Når denne jobben er ferdig skal det være mulig for eksterne enheter å få beskjed om dette. Eksempler på slike enheter kan være en innkjøpsansvarlig, eller en illustratør for ukesmenyen.
- Du skal ikke implementere noen av de eksterne lytterne, men lage kode som støtter et slikt mønster.
Oppgaven
Deklarer nødvendige grensesnitt og/eller klasser som støtter observerbarhet og implementer nødvendige metoder (og felt og hjelpeklasser og -metoder) for oppførselen beskrevet overfor. Bruk det eksisterende grensesnittet KitchenObserver.
- Kitchen
- Du skal ikke implementere en lytter her, men koden din skal støtte en slik.
Implementasjon av observatør-observert krevde følgende endring (eller liknende) i Kitchen.java (en plass å legge til og fjerne lyttere, en metode som kaller alle lyttere med metoden definert i grensesnittet, og så et kall til denne metoden fra delen der observatørene bør informeres.
private final Collection<KitchenObserver> observers = new HashSet<>(); // ... public void createRecipe(final Recipe recipe) { storage.removeIngredients(recipe.getIngredients()); notifyObservers(); // Denne linjen } // ... public void addObserver(final KitchenObserver obs) { observers.add(obs); } public void removeObserver(final KitchenObserver obs) { observers.remove(obs); } public void notifyObservers() { observers.forEach(obs -> obs.update(this)); }
Kandidaten skulle selv vurdere hvordan metoden(e) i grensesnittet skulle se ut.
package food; public interface KitchenObserver { void update(Kitchen kitchen); }
Her er målet å bruke det eksisterende grensesnittet KitchenObserver som et grensesnitt som observatører av Kitchen skal følge. I Kitchen.java skal en så definere en passende variabel for samling av observatører, og konstruere passende metoder for å legge inn og fjerne observatører. Til slutt skal man lage en metode som går igjennom alle observatører og kaller metoden definert i KitchenObserver. Basert på oppgaven var det beskrevet at observatører skulle kunne handle inn, da må de i så fall ha tilgang på Kitchen sin variabel som holdt rede på mengden av varer til neste uke. Det stod også at observatører skulle kunne lage (illustrere) neste ukes meny, det vil si at de har behov for de faktiske oppskriftene som er valgt. Siden det er ulike ting observatørene kan ha behov for er det like greit å sende over seg selv, this.
Det som gjenstår er å se hvor det er naturlig å kalle opp observatørene. Her er det naturlig å legge inn kall i i metoden registerWeekly. En kan også tenke seg at det er naturlig å oppdatere observatørene i createRecipe (når en matrett er laget ferdig), men det var ikke definert og ikke et krav. I createRecipe måtte en også huske på å slette sist ukes meny.
Vanlige feil: Grensesnittet KitchenObserver sin metode må inneholde en parameter som er i stand til å overføre all informasjon som en lytter ønsker å ha. Her gjaldt det både mengden ingredienser som skulle kjøpes inn til neste uke, og oppskriftene som skal lages. For å få til dette kan en sende over et sett med Recipe (disse har jo mengder inkludert, så en kan regne seg frem til begge) eller en kan sende med kitchen (altså this). Det var mange som bare sendte over en IngredientContainer - det holdt ikke. I tillegg var det en del som implementerte add- og remove- metoder i grensesnittet. Disse skulle ligge i Kitchen. Noen laget også grensesnitt med en metode uten parametre - hvordan kan da lytteren få noe informasjon. Men generelt ble vi imponerte over kunnskapen hos folk!
Det er laget en app som skal hjelpe kokken med å planlegge neste ukes meny. Filen Kitchen.fxml er knyttet til Controllerklassen KitchenController.java. Grensesnittet inneholder:
- En nedtrekksmeny der alle oppskrifter som finnes er lagt inn. Du behøver ikke å vite mye om hvordan denne nedtrekksmenyen fungerer, i oppgaven blir du kun bedt om å lese hvilken oppskrift som er valgt når en knapp trykkes inn.
- Et tekstfelt der kokken kan legge inn hvor mange porsjoner av retten som skal kunne lages neste uke.
- En knapp som, når den er trykket inn, legger inn oppskrift og antall porsjoner i neste ukes meny.
- Et tekstfelt der alle rettene som til nå er valgt er vist, sammen med antallet porsjoner av retten. Det skal oppdateres med metodekallet updateChosen()
- En knapp som, når den trykkes inn, bekrefter til systemet at neste ukes meny er ferdig.
Du kan starte appen ved å kjøre KitchenApp.
Oppgaven
- Forhold deg til den eksisterende koden Kitchen.fxml og KitchenController.java
- Ferdigstill kontrolleren slik at den følger kravene slik de er beskrevet overfor.
@FXML private void addRecipeToWeekly() { Recipe recipe = recipeSelector.getValue(); int nPortions = Integer.valueOf(portionField.getText()); kitchen.addRecipeToWeekly(recipe.createNPortions(nPortions)); updateChosen(); } /** * Triggered when user presses button to state that all recipe for next week are finished. */ @FXML private void submitWeeklyRecipes() { kitchen.registerWeekly(); recipesChosen.setText("Menu finished. " + kitchen.getWeeklyRecipes().size() + " recipes added. You can now close this window."); }
Her var det ikke mye som skulle skrives, men en måtte forstå hvordan ting hang sammen.
addRecipeToWeekly(): Hent ned valgt oppskrift fra recipeSelector(som var en combobox som inneholdt Recipe-objekter), og antallet porsjoner. Så måtte en huske på å kjøre skalering før en legger inn i kitchen-objektet.
submitWeeklyRecipes(): To formål: kalle kitchen.registerWeekly(), oppdatere recipesChosen med informasjon om at dette er gjort.
Vanlige feil: Tro at recipeSelector inneholdt strenger. Ikke lese antallet porsjoner, ikke gjøre dem om til tall, ikke kjøre skalering til antall porsjoner før lagring i kitchen.