Hoofdstuk 7
case: ocl-expressies In dit hoofdstuk worden de expressies ontwikkeld bij het domein-klassediagram van de case zoals dat in hoofdstuk 5 ontwikkeld is. Daarna worden de resterende stappen uit het stappenplan voor het klassediagram uit hoofdstuk 4 voor deze case beschreven.
7.1 Vind de regels voor de expressies
Bij het schrijven van de expressies gaan we uit van de stappen zoals die gegeven zijn in § 6.4. De ‘business rules’ die gelden in onze case zijn: 1. Er mag maar één Voorraadkast zijn. 2. Er mag maar één Kookboek zijn. 3. De kok van een WarmeMaaltijd is altijd ook bij de WarmeMaaltijd aanwezig. 4. Er kan alleen een Recept bij een Maaltijd zijn als er ook een kok is en andersom verplichten we de kok om onmiddellijk een recept te kiezen. 5. Het aantal KoudeMaaltijden waarvoor een voorkeur opgegeven kan worden, is gelijk aan het aantal Maaltijden in een Dag minus het aantal WarmeMaaltijden in een Dag. 6. Het Kookboek bevat minstens één Recept. 7. Een Dag heeft minstens één Maaltijd.
7.2 Vind de context en schrijf de expressies in OCL
We gaan uit van het klassemodel uit figuur 5‑2. De regels 6 en 7 zijn simpele expressies die we met een multipliciteitsindicatie bij de associaties in het klassemodel weergeven. De regels 1 en 2 lijken sterk op elkaar en zullen allebei op dezelfde manier in OCL worden weergegeven. De context waar we van uitgaan is Voorraadkast, respectievelijk Kookboek. De OCL-expressies zijn: 1. context Voorraadkast inv: Voorraadkast::allInstances()->size() <= 1 2. context Kookboek inv: Kookboek::allInstances()->size() <= 1
Als context voor regel 3 kunnen we kiezen tussen WarmeMaaltijd of Persoon. Omdat WarmeMaaltijd alle kennis heeft omtrent aanwezigen en de kok, leggen we de verant79
praktisch uml
woordelijkheid voor deze constraint bij WarmeMaaltijd. De constraint wordt dan (let op: de associatie aanwezigen wordt overerfd van Maaltijd): 3. context WarmeMaaltijd inv: aanwezigen->includes( kok )
Regel 4 stelt dat er alleen een Recept bij een WarmeMaaltijd mag zijn als er ook een kok is en andersom. Ook hier ligt alle kennis omtrent zowel het recept als de kok bij WarmeMaaltijd en kiezen we deze klasse als context. 4. context WarmeMaaltijd inv: kok->notEmpty()
implies recept->notEmpty()
and
recept->notEmpty() implies kok->notEmpty()
Volgens regel 5 is het aantal KoudeMaaltijden waarvoor een voorkeur opgegeven kan worden, gelijk aan het aantal Maaltijden in een Dag minus het aantal WarmeMaaltijden in een Dag. Het is moeilijk om voor deze regel een context te kiezen. De reden daarvoor is dat we de regel eigenlijk niet precies genoeg hebben geformuleerd. We willen namelijk ook dat elke dag hetzelfde aantal warme en koude maaltijden heeft. Deze informatie nemen we op in twee nieuwe klasseattributen bij de klasse Dag. Als namen voor de klasseattributen kiezen we aantalWarmeMaaltijden en aantalKoudeMaaltijden, beide zijn van het type Integer. We werken de constraint betreffende het aantal warme en koude maaltijden eerst uit. De context is natuurlijk Dag. context Dag inv: Dag::aantalWarmeMaaltijden = maaltijden->select(isTypeOf(WarmeMaaltijd))->size() context Dag inv: Dag::aantalKoudeMaaltijden = maaltijden->select(isTypeOf(KoudeMaaltijd))->size()
Nu zijn we zover dat we de oorspronkelijke regel in OCL kunnen opschrijven. We kiezen hiervoor de context Persoon, want Persoon kent de voorkeuren. 5. context Persoon inv: voorkeuren->size() = Dag::aantalKoudeMaaltijden
Het formuleren van regel 5 in OCL heeft ons een veel exacter inzicht gegeven in het model van ons systeem.
80
ho ofdstuk 7 – case: o cl-expressies
7.3
Maak het klassediagram af
Het schrijven van OCL-expressies is een van de laatste stappen in het stappenplan voor het klassediagram zoals dat in hoofdstuk 4 gegeven is. Nu we de expressies voor de case hebben vastgesteld kunnen we verder met dit stappenplan.
7.3.1 Groepeer klassen in packages De volgende stap is het verdelen van het systeem in packages. Voor onze case laten we dit (voorlopig) achterwege omdat het een klein systeem betreft (zie ook § 17.3.6).
7.3.2 Itereer over de gedane stappen De laatste stap is dan het afmaken en valideren van het klassediagram door een nieuwe iteratieslag in te gaan. Hierbij reflecteren we alle keuzen die we gemaakt hebben bij het bouwen van het klassediagram. Nieuwe informatie wordt meegenomen, oude informatie wordt vaak beter begrepen en leidt zo ook tot veranderingen. In deze case leidt de reflectie tot het idee dat de verhouding tussen Maaltijd en zijn subklassen nog steeds niet helemaal goed ligt. Is WarmeMaaltijd niet toch een rol van een Maaltijd? De gevolgen van een dergelijke verandering voor ons objectmodel zijn vrij groot. Twee klassen worden verwijderd, de klasse Maaltijd wordt complexer omdat in sommige gevallen het gedrag van de klasse af zal hangen van de rol. Maar het biedt ook meer flexibiliteit. Stel dat voor een koude maaltijd ook een gerecht met een recept klaargemaakt moet worden. In het objectmodel tot nu toe geeft dat grote problemen, in een objectmodel zonder de subklassen WarmeMaaltijd en KoudeMaaltijd is dat vrij simpel. Voor beide mogelijkheden zijn argumenten te geven. Wij kiezen voor de flexibiliteit en maken dus een aangepast objectmodel zonder WarmeMaaltijd en KoudeMaaltijd. De operaties van WarmeMaaltijd worden nu opgenomen bij Maaltijd. De associaties kok en de niet benoemde associatie tussen WarmeMaaltijd en Recept worden ook naar Maaltijd verlegd waarbij de multipliciteit wordt aangepast. Niet alle Maaltijden hebben een kok en een benodigd Recept. Verder hebben we een attribuut nodig dat het type van de maaltijd aangeeft: type: {warm, koud}. Met dit attribuut kunnen we een extra constraint formuleren op de voorkeuren van een Persoon. We hadden gesteld dat er alleen een voorkeur gegeven mag worden voor Maaltijden met het type koud. In OCL zou dit geschreven kunnen worden als: context Persoon inv: voorkeuren->forAll(type = MaaltijdType::koud)
Maar is het wel zo slim om aan deze constraint vast te houden? Waarom zouden we geen voorkeur toestaan voor een warme maaltijd? Wanneer noch Hans noch Jacqueline aangeeft te willen koken wordt er geen recept gekozen. Als beiden dan toch bij de maaltijd 81
praktisch uml
aanwezig zijn, zou het handig zijn als er bijvoorbeeld voldoende diepvriespizza’s of iets dergelijks in huis zijn. Het lijkt een goed idee om deze constraint te laten vervallen. Maar hoe ziet dan een voorkeur voor een warme Maaltijd eruit? Is daar ook een Recept bij betrokken of niet? Om deze vraag te kunnen beantwoorden moeten we nogmaals goed kijken naar de associatie voorkeur van Persoon naar KoudeMaaltijd. Eigenlijk is een voorkeur voor een lunch niet een echte instantie van de klasse Lunch, informatie over de dag en het tijdstip waarop de lunch plaatsvindt is namelijk helemaal niet nodig. Sterker nog: de voorkeur maaltijd is een abstract begrip en kan in dit systeem weergegeven worden met een lijstje Ingrediënten. Dit geldt zowel voor koude als voor warme Maaltijden. We introduceren dus een klasse Voorkeur die een aantal Ingrediënten bevat, en een attribuut dat aangeeft voor welke ‘soort’ Maaltijd dit een voorkeur is. De constraint die we geformuleerd hadden voor regel 5 moet nu ook worden aangepast, net als de klasseattributen bij Dag. Dag krijgt nu één klasse-attribuut genaamd aantalMaaltijden. Regel 5 stelt nu dat iedere persoon een voorkeur moet hebben voor iedere maaltijd. context Persoon inv: voorkeuren->size() = Dag::aantalMaaltijden
Verder moet natuurlijk gelden dat er per ‘soort’ Maaltijd maar één voorkeur opgegeven wordt. We gebruiken een attribuut maaltijdnaam om bij de Voorkeur de maaltijd mee aan te duiden. De volgende constraint stelt dat wanneer we een verzameling van dergelijke attributen hebben, deze verzameling gelijk moet zijn aan de verzameling getransformeerd naar een Set. Dit komt neer op stellen dat elk element in de verzameling uniek moet zijn. context Persoon inv: voorkeuren.maaltijdnaam = voorkeuren.maaltijdnaam->asSet()
Regels 3 en 4 worden in de nieuwe situatie constraints bij Maaltijd. In het nieuwe klassediagram kan een koude maaltijd ook een kok en een Recept hebben. Willen we alleen recepten met kok toestaan voor warme maaltijden, zoals in het oorspronkelijke klassediagram, dan zullen we dit moeten aangeven bij Maaltijd. De invariant luidt dan: context Maaltijd inv: type = MaaltijdType::koud implies kok->isEmpty() and recept->isEmpty()
82
ho ofdstuk 7 – case: o cl-expressies
Hoewel we verwachten dat er bij koude maaltijden nagenoeg nooit een recept gebruikt zal worden, vinden we het niet nodig om deze constraint aan het model toe te voegen. We laten ook recepten toe bij koude maaltijden. Dit is toegevoegde flexibiliteit.
7.3.3 Resultaat Week
Dag
Maaltijd
7 aantalMaaltijden naam
nummer toonWeekschema()
1..*
naam type : MaaltijdType 0..1
*
<< enumeration >> MaaltijdType warm koud
aanwezigen * kok Persoon
0..1
0..1
naam type
0..1
Recept naam bereidingswijze bereidingstijd moeilijkheidsgraad 1..*
Supermarkt naam faxnummer
zoekRecept()
maaltijdnaam
*
0..1 *
Voorraadkast
bestelling 0..1
doeBestelling() ontvangBestelling() verminderVoorraad()
Kookboek
voorkeuren * Voorkeur
*
*
benodigdheden
Ingrediënt type hoeveelheid
voorraad 0..1
*
Figuur 7-1 Derde versie klassediagram voor EasyShop
Het resultaat van het werk in dit hoofdstuk is een herzien klassediagram en een aantal expressies in de vorm van invarianten op dit klassediagram. In de figuur 7‑1 vindt u dit klassediagram weergegeven, met hierbij de toegevoegde expressies. 83
praktisch uml
1. context Voorraadkast inv: Voorraadkast::allInstances()->size() <= 1 2. context Kookboek inv: Kookboek::allInstances()->size() <= 1 3. context Maaltijd inv: aanwezigen->includes( kok ) 4. context Maaltijd inv: kok->notEmpty()
implies recept->notEmpty()
recept->notEmpty() implies kok->notEmpty() 5. context Persoon inv: voorkeuren->size() = Dag::aantalMaaltijden
84
and