Tema for alle oppgavene er en restaurant, med fokus på menyen. Course-klassen representerer en enkeltrett, f.eks. laksemousse, biffsnadder eller karamellpudding. Meal representerer et ferdig sammensatt måltid som bestilles som et hele, f.eks. en tre-retters med laksemousse, biffsnadder og karamellpudding. Menu er menyen med utvalget av retter og måltider og deres priser. Table representerer bordet, i denne sammenheng bestillingene.

Under finner du utgitt kode, som fungerer som kontekst for oppgavene. Merk spesielt kommentarene til metoder, som inneholder krav til metodene, og linjer med ..., som indikerer manglende kode.

public class Course {
 
    public final String name, description;
 
    public Course(String name, String description) {
        super();
        ... initialization ...
    }
}
 
/**
 * Represents a set of (pre-defined) Courses that are ordered as a whole
 */
public class Meal {
 
    private final String name, description;
 
    public Meal(String name, String description, Course[] courses) {
        super();
        ... initialization ...
        this.courses = Arrays.asList(courses);
    }
    
    public String getName() {
        return name;
    }
    
    public String getDescription() {
        return description;
    }
}
 
/**
 * Manages the set of Courses and Meals offered and their prices.
 */
public class Menu {
 
    ... fields and methods ...
 
    /**
     * Gets the price for a Course.
     * @param course
     * @return the price
     * @throws IllegalArgumentException if this Menu doesn't include the provided Course
     */
    public double getPrice(Course course) throws IllegalArgumentException {
        ....
    }
 
    /**
     * Sets/changes the price of the provided Course.
     * @param course
     * @param price
     */
    public void updatePrice(Course course, double price) {
        ...
    }
    
    /**
     * Gets the price for a Meal. If the registered price is 0.0,
     * the price is computed as the sum of the prices of the Meal's courses.
     * @param meal
     * @return
     * @throws IllegalArgumentException if this Menu doesn't include the provided Meal,
     *  or if a price of a Course is needed, but is missing
     */
    public double getPrice(Meal meal) throws IllegalArgumentException {
        ...
    }    
 
    /**
     * Sets/changes the price of the provided Meal.
     * @param meal
     * @param price
     */
    public void updatePrice(Meal meal, double price) {
        ...
    }
}

/**
 * Manages the set of ordered items for a table (set of guests).
 */
public class Table {
 
    ... fields and methods ...
 
    /**
     * Initializes a new Table with a Menu that provides the prices for the Courses and Meals
     * @param menu
     */
    public Table(Menu menu) {
        ...
    }
    /**
     * Computes the total price for all the added items. Prices are provided by the Menu.
     * @return the total price
     * @throws IllegalStateException when the price of an item cannot be provided by the Menu
     */
    public double getPrice() throws IllegalStateException {
        ...
    }
}
(5p) Oppgave a) Innkapsling av navn og beskrivelse

Course- og Meal-klassene representerer henholdsvis enkeltretter og måltider sammensatt av flere retter, slik vi finner i menyer på spisesteder.

Både Course og Meal skal initialiseres med navn og beskrivelse, som siden ikke skal kunne endres. I den utgitte koden er det brukt to varianter for å håndtere dette. Course har public-felt og ingen get- eller set-metoder, mens Meal har private-felt og public get-metoder. Angi fordeler og ulemper med hver kodingsteknikk. Hvilken anbefaler du? Begrunn svaret!

Dette handler om innkapsling, som har to aspekter: 1) sikring av gyldig tilstand og 2) skjuling av implementasjonsdetaljer, så koden lettere kan endres uten at andre klasser påvirkes. Av disse to er aspekt 1) viktigst.

Den første teknikken krever mindre kode og er derfor enklere å lese og skrive. Siden en bruker final-modifikatoren så sikres aspekt 1), siden verdiene/tilstanden ikke kan endres tross at feltet er public. Imidlertid så er det andre aspektet ved innkapsling ikke ivaretatt, siden feltet er eksponert. Derfor er den andre teknikken å foretrekke, som ivaretar begge aspektene.

(5p) Oppgave b) super()

I konstruktørene til Course og Meal står det super() i første linje. Hva betyr/gjør denne linja? Hva ville skjedd om den ble fjernet?

super() kaller konstruktøren i superklassen og trengs for å sikre at også superklassens konstruktør blir kjørt. Her er super-klassen implisitt Object-klassen. Hvis linja ikke er med, så vil et tilsvarende kall, altså til en konstruktør uten argumenter, bli lagt til av kompilatoren. Derfor kan vi trygt fjerne linja.
(5p) Oppgave c) courses-feltet i Meal

I konstruktøren for Meal er det lagt til ei linje for initialisering av feltet courses, som lagrer matrettene som måltidet er satt sammen av. Skriv en passende deklarasjon for courses, slik at feltet er egnet til formålet og initialiseringslinja blir riktig.

Her er poenget å velge en type som passer til hvordan feltet brukes og verdien den blir tilordnet. Verdien som tilordnes er av typen List<Course>, så typen må enten være List eller en av dens superklasser, som er Collection og Iterable. Hvis vi bare trenger metodene i Collection, som List arver fra, så er det bedre å bruke Collection i deklarasjonen.

private Collection<Course> courses;

(5p) Oppgave d) Arrays.asList-metoden

Initialiseringskoden bruker metoden Arrays.asList. Skriv en (mulig) metodedeklarasjon (altså den første linja i koden for metoden, med modifikatorer og alt som trengs) for denne metoden, som passer til slik metoden brukes.

Her er poenget å ha riktig argument- og resultat-type, henholdsvis en Course-array og Course-liste, samt merke seg at metoden kalles ved klassenavnet foran og derfor trenger static-modifikatoren.

public static List<Course> asList(Course[] courses)

Det går an å bruke varargs-notasjonen (se Varargs), slik den har i virkeligheten, men det er ikke påkrevd.

(2p) Oppgave e) Initialisering av courses-feltet

I initialiseringskoden brukes this.courses = ... Hva ville skjedd om vi utelot this-nøkkelordet, altså bare skrev courses = ... ? Ta i betraktning dine svar på tidligere delspørsmål!

Velg ett alternativ
  • varsel om mulig feil (gul strek i editoren) 
  • kræsj (unntak) ved kjøring
  • logisk feil ved kjøring
  • det virker som det skal

 

feilmelding ved kompilering (rød strek i editoren)
(3p) Oppgave f) Initialisering av courses-feltet forts.

Begrunn/forklar ditt valg i forrige deloppgave!

Uten this. foran vil venstresiden referere til metode-argumentet av typen Course[]. Linja vil da prøve å tilordne en (referanse til en) List<Course> til en Course[]-variabel, som ikke er lov.
 
(7p) Oppgave g) Innkapsling av Course-objekter i Meal-klassen

Skriv nødvendig kode for å innkapsle Course-objektene i Meal-klassen. Det skal være mulig å legge til og fjerne Course-objekter, samt iterere (løpe gjennom) alle Course-objektene i et Meal-objekt med kode slik som dette:

for (Course course : meal) {
   ... gjør noe med course her ...
}

For å legge til og fjerne Course-objekter, så trengs addCourse- og removeCourse-metoder som tar inn et Course-argument. For å kunne iterere med et Meal-objekt bak kolonet i en for-løkke, så må Meal-klassen implementere Iterable<Course> og derfor ha en iterator()-metode som returnerer Iterator<Course>.

public class Meal implements Iterable<Course> {

	...

	private Collection<Course> courses;

	//
	
	public void addCourse(Course course) {
		this.courses.add(course);
	}
	
	public void removeCourse(Course course) {
		this.courses.remove(course);
	}

	// implement Iterable<Course>

	@Override
	public Iterator<Course> iterator() {
		return courses.iterator();
	}
}
(10p) Oppgave h) Menu-klassen

Menu-klassen skal kunne lagre et sett Course- og Meal-objekter og deres priser. Skriv ferdig klassen, med nødvendige felt og metoder for dette.

Her er problemet å finne en god måte å representere settet med Course- og Meal-objekter OG deres priser. En Map passer til dette, siden den kobler et sett med objekter til hvert sitt andre objekt, her prisen. Vi velger å bruke to Map-objekter, etter for hver objektklasse (Course og Meal). Vi kunne strengt tatt klart oss med én, som var spesialisert til Object, tilsvarende slik vi gjør senere, når MenuItem introduseres som en felles superklasse. En alternativ teknikk er å bruke to sett med to lister, hvor prisen er på samme indeks som tilsvarende Course eller Meal-objekt, men det er mer knot.

private Map<Course, Double> courses;
private Map<Meal, Double> meals;

public double getPrice(Course course) throws IllegalArgumentException {
   if (! courses.containsKey(course)) {
      throw new IllegalArgumentException("This menu does not include the course " + course.name);
   }
   return courses.get(course);
}
 
public void updatePrice(Course course, double price) {
   courses.put(course, price);
}
 
public double getPrice(Meal meal) throws IllegalArgumentException {
   if (! meals.containsKey(meal)) {
      throw new IllegalArgumentException("This menu does not include the meal " + meal.getName());
   }
   double total = meals.get(meal);
   if (total == 0.0) {
      for (Course course : meal) {
         total += getPrice(course);
      }
   }
   return total;
}
 
public void updatePrice(Meal meal, double price) {
   meals.put(meal, price);
}
(10p) Oppgave i) Table-klassen

Table-klassen representerer alle retter (Course) og måltider (Meal) som gjestene ved et bord har bestilt. For å kunne beregne (total)prisen, er Table-klassen knyttet til en Menu.

Skriv ferdig klassen.

Vi bruker to lister, én for hver objekt-klasse (Course og Meal). Menu-objektet som trengs for å beregne priser, tas inn i konstruktøren. Ved beregning av pris, så må en fange opp evt. unntak av typen IllegalArgumentException og utløse dem på nytt som IllegalStateException, slik at unntakstypen blir som deklarert.

private Collection<Course> courses = new ArrayList<>();
private Collection<Meal> meals = new ArrayList<>();
 
public void addCourse(Course course) {
   this.courses.add(course);
}
 
public void addMeal(Meal meal) {
   this.meals.add(meal);
}

private final Menu menu;
 
public Table(Menu menu) {
   this.menu = menu;
}
 
public double getPrice() throws IllegalStateException {
   double total = 0.0;
   for (Course course : courses) {
      try {
         total += menu.getPrice(course);
      } catch (IllegalArgumentException e) {
         throw new IllegalStateException(e);
      }
   }
   for (Meal meal : meals) {
      try {
         total += menu.getPrice(meal);
      } catch (IllegalArgumentException e) {
         throw new IllegalStateException(e);
      }
   }
   return total;
}

Koden finnes på github, se del 1 av konten 2017

Denne delen handler om hvordan arv kan utnytte at Course og Meal har en del felles.

... class MenuItem {
    ...
}
 
// inherits from MenuItem
... class Course ... {
 
    ...
}
 
// inherits from MenuItem
... class Meal ... {
 
    ...
}
 
// updated to use MenuItem
... class Menu ... {
 
    ...
}
 
// updated to use MenuItem
... class Bill ... {
 
    ...
}
(5p) Oppgave a) MenuItem-klassen

Course- og Meal-klassene har en del felles egenskaper, som kan samles i en felles superklasse kalt MenuItem. Skriv koden til MenuItem.

Her er poenget å samle det som er felles. Klassen bør være abstract, siden det ikke gir mening å instansere den.

public abstract class MenuItem {

	private final String name, description;

	protected MenuItem(String name, String description) {
		this.name = name;
		this.description = description;
	}

	public String getName() {
		return name;
	}

	public String getDescription() {
		return description;
	}
}
(10p) Oppgave b) Bruk av MenuItem

Introduksjonen av MenuItem-klassen gjør det nødvendig å endre de eksisterende klassene. Skriv ny kode for Course-, Meal-, Menu- og Table-klassene, slik at de virker med og utnytter mulighetene som MenuItem-superklassen gir. Vurder spesielt om felt og metoder kan forenkles/slås sammen. Flett gjerne inn kommentarer til endringene og valg du gjør i koden.

Poenget her at siden Course og Meal nå har en felles superklasse, så blir det enklere å ha datastrukturer med begge disse objekttypene i. Dette forenkler både Menu- og Table-klassene. En blir her bedt om å skrive mye kode på nytt, og siden eksamen var digital, så bør det være greit i praksis. Det er greit å bare skrive de delene som blir endret, til nød forklare endringene med tekst.

public class Course extends MenuItem {
    public Course(String name, String description) {
		super(name, description);
	}
	...
}
 
public class Meal extends MenuItem {
    public Meal(String name, String description) {
		super(name, description);
	}
	...
}

public class Menu {
	private Map<MenuItem, Double> items;

	public double getPrice(Course course) throws IllegalArgumentException {
		if (! items.containsKey(course)) {
			throw new IllegalArgumentException("This menu does not include the course " + course.getName());
		}
		return items.get(course);
	}

	public void updatePrice(MenuItem item, double price) {
		items.put(item, price);
	}

	public double getPrice(Meal meal) throws IllegalArgumentException {
		if (! items.containsKey(meal)) {
			throw new IllegalArgumentException("This menu does not include the meal " + meal.getName());
		}
		double total = items.get(meal);
		if (total == 0.0) {
			for (Course course : meal) {
				total += getPrice(course);
			}
		}
		return total;
	}
}
 
public class Table {

	private Collection<MenuItem> items = new ArrayList<>();


	// har endret fra to add-metoder til én
	public void addItem(MenuItem item) {
		this.items.add(item);
	}

	//

	private final Menu menu;

	public Table(Menu menu) {
		this.menu = menu;
	}

	public double getPrice() throws IllegalStateException {
		double total = 0.0;
		for (MenuItem item : items) {
			try {
				// note: cannot use menu.getPrice(item), since there is no menu.getPrice(MenuItem) method
				if (item instanceof Course) {
					total += menu.getPrice((Course) item);
				} else if (item instanceof Meal) {
					total += menu.getPrice((Meal) item);
				}
			} catch (IllegalArgumentException e) {
				throw new IllegalStateException(e);
			}
		}
		return total;
	}
}

Koden finnes på github, se del 2 av konten 2017

 

Denne delen handler om samhandling mellom Table-klassen og en ny Kitchen-klasse.

public class Table {
 
    ...
 
    /**
     * Sets the Kitchen that should be notified when items are added.
     * Note that this method may be called several times with different Kitchen objects.
     * @param kitchen
     */
    public void setKitchen(Kitchen kitchen) {
        ... what should be done here? ...
    }
 
    ...
}
 
/**
 * Interface for classes that want to know when Courses have been produced by a Kitchen.
 */
public interface KitchenListener {
    public void courseReady(Table table, Course course);
}
 
/**
 * Manages a queue of courses to produce, based on what is requested by Tables.
 */
public class Kitchen {
 
    // for each Table that has requested Courses,
    // there is a Collection of the Courses that are yet to be made
    private Map<Table, Collection<Course>> courseQueue = new HashMap<Table, Collection<Course>>();
    
    /**
     * Enqueues a Course in the production queue, that is part of the provided Table.
     * @param table
     * @param course
     */
    private void produceCourse(Table table, Course course) {
        Collection<Course> courses = courseQueue.get(table);
        ... what should be done the first time a Table requests a Course? ...
        courses.add(course);
        courseQueue.put(table, courses);
    }
 
    /**
     * Internal methods that must be called when a Course of a Table has been produced.
     * Notifies registered listeners about the event.
     * @param table
     * @param course
     */
    private void courseProduced(Table table, Course course) {
        Collection<Course> courses = courseQueue.get(table);
        courses.remove(course);
        ... what should be done here, to support observers? ...
    }
 
    /**
     * Should be called when a MenuItem is added to a Table,
     * so the corresponding Courses can be produced.
     * @param table
     * @param item
     */
    public void menuItemAdded(Table table, MenuItem item) {
        ... handle cases when item is a Course or a Meal ...
    }
 
    //
    
    ... fields and methods for supporting observers ...
}
(10p) Oppgave a) Kobling fra Table til Kitchen

Bestilling av mat henger sammen med laging av mat, og nå skal det innføres en Kitchen-klasse (se utgitt kode), som har en dobbel kobling til Table-klassen. Denne deloppgaven handler om den første koblingen, nemlig at et Table-objekt skal kunne ha en kobling til ett Kitchen-objekt, som skal få beskjed når en ny rett (Course) eller et nytt måltid (Meal) legges til i lista over bestillinger. Tenk på det som en bestilling fra kelneren som betjener bordet til kjøkkenet om hvilke retter som skal lages.

Fullfør Table-klassen med det som trengs for å håndtere en kobling til Kitchen og gi beskjed til Kitchen-objektet om retter og måltider som bestilles.

Vi trenger et felt for Kitchen og en setter (oppgitt i koden). Vi må også si fra til Kitchen-objektet om nye bestillinger i addItem-metoden.

private Kitchen kitchen;


// setter
public void setKitchen(Kitchen kitchen) {
	... her kommer det mere siden ...
	this.kitchen = kitchen;
	... her kommer det mere siden ...
}

// må si fra til kjøkkenet (hvis satt) når noe bestilles
public void addItem(MenuItem item) {
	this.items.add(item);
	if (kitchen != null) {
		kitchen.menuItemAdded(this, item);
	}
}
(10p) Oppgave b) Kobling fra Kitchen til Table og andre klasser.

Table-objektene representerer bordene, og de må få beskjed når en rett er ferdiglaget av kjøkkenet. Derfor må Kitchen-klassen være kodet for å støtte en slik varsling. For å gjøre koden mer generell, så brukes observert-observatør-teknikken (se utgitt kode). Fullfør Kitchen-klassen, så den kan fungere som en observert og Table-klassen som observatør vha. KitchenListener-grensesnittet.

 Dette er standard bruk av observatør-observert-teknikken, altså må en ha felt med liste av lyttere, metoder for å legge til og fjerne lyttere og helst en hjelpemetode for å varsle dem.

public class Kitchen {
 
	...
 
	private Collection<KitchenListener> kitchenListeners = new ArrayList<KitchenListener>();

	public void addKitchenListener(KitchenListener listener) {
		kitchenListeners.add(listener);
	}

	public void removeKitchenListener(KitchenListener listener) {
		kitchenListeners.remove(listener);
	}

	private void fireCourseReady(Table table, Course course) {
		for (KitchenListener listener : kitchenListeners) {
			listener.courseReady(table, course);
		}
	}
}

Table må implementere lyttergrensesnittet og dermed også varslingsmetoden(e), og en må huske å registrere den når Kitchen-objektet settes. Dette kompliserer setteren, siden den både må avregistrere seg fra det forrige Kitchen-objektet og registrere seg på det nye.

public class Table implements KitchenListener {
 
	...
 
	public void setKitchen(Kitchen kitchen) {
		if (this.kitchen != null) {
			this.kitchen.removeKitchenListener(this);
		}
		this.kitchen = kitchen;
		if (this.kitchen != null) {
			this.kitchen.addKitchenListener(this);
		}		
	}
 
	@Override
	public void courseReady(Table table, Course course) {
		// reaksjon på varslingen
	}
}
(10p) Oppgave c) Kitchen sin Course-kø

Kitchen-klassen (se utgitt kode) håndterer en kø av retter, organisert etter hvilket bord (Table) som har bestilt den. Logikken er bare delvis implementert, fullfør koden for metodene produceCourse, courseProduced og menuItemAdded så køen håndteres riktig, ved å fylle inn der det er angitt med ...

private void produceCourse(Table table, Course course) {
	Collection<Course> courses = courseQueue.get(table);
	if (courses == null) {
		courses = new ArrayList<Course>();
		courseQueue.put(table, courses);
	}
	courses.add(course);
}

private void courseProduced(Table table, Course course) {
	Collection<Course> courses = courseQueue.get(table);
	if (courses != null) {
		courses.remove(course);
		fireCourseReady(table, course);
	}
}

public void menuItemAdded(Table table, MenuItem item) {
	if (item instanceof Course) {
		produceCourse(table, (Course) item);
	} else if (item instanceof Meal) {
		for (Course course : (Meal) item) {
			produceCourse(table, course);
		}			
	}
}

Koden finnes på github, se del 3 av konten 2017

 

public class Meal {
    ...
    
    /**
     * Finds a Course satisfying the provided Predicate.
     * @param test
     * @return the first Course satisfying the provided Predicate, otherwise null.
     */
    public Course findCourse(Predicate<Course> test) {
        ... what should be done here? ...
    }
}

public interface Predicate<T> {
    /**
     * Evaluates this predicate on the given argument.
     * @param t the input argument
     * @return true if the input argument matches the predicate, otherwise false
     */
    boolean test(T t);
}

public class MealTest extends TestCase {

    private Course c1, c2;
    private Meal meal;

    @Override
    protected void setUp() throws Exception {
        ... what should be done here? ...
    }
    
    /**
     * Tests Meal's support for foreach loop (iteration)
     */
    public void testIteration() {
        ... test code ...
    }
    
    /**
     * Tests Meal's findCourse(Prediate) method
     */
    public void testFindCourse() {
        ... test code ...
    }
}
(5p) Oppgave a) Predicate-grensesnittet

Hva kalles et slikt grensesnitt som Predicate (se utgitt kode) og hvorfor?

Dette er et såkalt funksjonelt grensesnitt, siden det har én metode som (er ment som å) oppfører seg som en matematisk funksjon.

(5p) Oppgave b) findCourse-metoden

Meal skal utvides med en metode for å finne det første Course-objektet som tilfredsstiller gitte betingelser. For å gjør koden generell så gis betingelsen inn som argument i form av et Predicate-objekt (se utgitt kode). Fullfør findCourse-metoden (se utgitt kode).

Vi må løpe gjennom course-lista og returnere det første Course-objektet som Predicate-objektet svarer true for.

public Course findCourse(Predicate<Course> test) {
	for (Course course : courses) {
		if (test.test(course)) {
			return course; 
		}
	}
	return null;
}
(8p) Oppgave c) Testing av Meal

Den utgitte koden inneholder en JUnit-test for testing av Meal-klassen. To typer funksjonalitet skal testes, 1) metoden(e) for å støtte iterasjon vha. foreach-løkker og 2) findCourse-metoden. Fullfør koden. Hvis du ikke får til å skrive riktig kode for JUnit-rammeverket, så forklar med tekst og kode hvordan du ville gjort det.

 setUp-metoden brukes for initialisering av objektene som brukes i testene.

protected void setUp() throws Exception {
	c1 = new Course("c1", "c1");
	c2 = new Course("c2", "c2");
	meal = new Meal("test", "test", new Course[]{ c1, c2});
}

For å teste iterasjon med foreach-løkker, må vi bruke metoden som en slik løkke (implisitt) bruker, nemlig iterator()-metoden og Iterator-objektet som denne returnerer. Her brukes objektene som er rigget opp i setUp-metoden.

 public void testIterator() {
 	Iterator<Course> it = meal.iterator();
 	assertTrue(it.hasNext());
 	assertEquals(c1, it.next());
 	assertTrue(it.hasNext());
 	assertEquals(c2, it.next());
 	assertFalse(it.hasNext());
 }

For å teste findCourse-metoden, så må en lage seg et Predicate-objekt og sjekke at findCourse-metoden returnerer det riktige objektet.

public void testFindCourse() {
	assertEquals(c1, meal.findCourse(course -> course.name.equals("c1")));
}

Koden finnes på github, se del 4 av konten 2017