over analyse en ontwerp: In OOD, problems are modelled using objects. Objects have: 1. Behaviour (they do things) 2. State (which changes when they do things) For example, a car could be an object. It has a state: whether its engine is running, and it has a behaviour: starting the car, which changes its state from "engine not running" to "engine running". In object-oriented design, complexity is managed using abstraction. Abstraction is the elimination of the irrelevant and the amplification of the essential Generate a list of requirements for the system. Pick a requirement. Identify some classes whose objects might be involved in satisfying the requirement. Do the requirements suggest any relationships between the classes? (draw class diagrams). Work out how the objects interact to satisfy the requirement (draw object diagrams). Add operations and any new relationships between classes to the class diagram. Try to think of other ways (different classes, different interactions between objects) to satisfy the requirement. The first thing you think of is not usually the best. Do your decisions have repercussions for earlier parts of the design? Pick another requirement... Other Advice Try to work in "levels" of abstraction Start at a high level and as each level "firms up" start looking at the layer below. It isn't necessary to have a level complete before moving on - remember problems that occur in one level may have an effect on the level above. Defer decisions Don't make decisions about, say, physical containment early on. Things like storage in a list or hash table are fairly low-level details. Create new operations to avoid dealing with details. Find out what makes a good design We've already seen that tight coupling can be a problem. Another "rule of thumb" is that classes that are used by many other classes should be stable (because changing them affects many other classes) and that often means that they consist purely of interface. You can measure how good your design is using metrics. Anticipate likely modifications and likely candidates for re-use Design with these in mind, but don't go too mad - you could create an impenetrable jungle of classes. Bij het ontwerp van een systeem is het bepalen van de klassen hetzelfde als de verantwoordelijkheden verdelen over een aantal sub-systemen. Een bepaald object zal verantwoordelijk zijn voor bepaalde dingen. Het is zoals het bouwen van een huis: iemand zal verantwoordelijkheden toekennen aan mensen. De architect zorgt voor de plannen, de aannemer voor de stenen, de loodgieter voor de buizen, de electricien voor de electriciteit. Om tot een werkend geheel te komen moeten objecten met elkaar praten. Daartoe het elk object (klasse) een publieke interface = de verzameling methoden die aangeroepen kunnen worden door andere objecten = public functions van het object. De architect zal op een of andere manier aan de aannemer vertellen hoe het huis eruit moet zien (hij zal vb het plan overhandigen). De aannemer zal praten met zijn werknemers, en omgekeerd. De architect zegt vb "hier heb je het plan", en de bouwvakker zegt "ik heb stenen te kort". De interface van een object bepaalt wat er allemaal tegen het object gezegd kan worden. De aannemer zal dus een interface hebben met oa hierHebJeHetPlan(Plan p) en ikHebStenenTekort(Bouwvakker v). class Aannemer { public void hierHebJeHetPlan(Plan plan) public void ikHebStenenTekort(Bouwvakker wie) } Dus moet je een klasse definitie opvatten: als "welke boodschappen kan die klasse verwerken". Dat vertaal je naar publieke methodes. Het kan zijn dat je object tergelijkertijd twee rollen vervult: een man kan thuis huisvader zijn, en op het werk manager. Diezelfde man heeft dus twee interfaces, afhankelijk van de andere objecten in z'n omgeving. De objecten die met managers werken, weten niks over huisvaders, en kinderen weten niks over managers. In dat geval is het belangrijk beide onderdelen zo te definiëren dat je ze onafhankelijk van elkaar kunt houden, omdat ze dan ook onafhankelijk van elkaar kunnen veranderen: 't is niet omdat de huisvader een kind bijkrijgt, dat er iets verandert aan z'n manager-verantwoordelijkheid. Naar implementatie toe specifieren we dus twee interfaces. interface Huisvader { geefBabyEten(Baby b) } interface Manager { maakWerkSchema() } class EenMan implements HuisVader, Manager { ... } Het belang van dat ingewikkeld gedoe komt pas in 't spel als we andere klassen definiëren. In de definitie van de Huismoeder klasse zullen we enkel de Huisvader interface gebruiken, nooit de klasse EenMan. Het interessante is dat we aan de implementatie van EenMan kunnen prutsen zoveel we willen, de HuisMoeder zal blijven "werken" omdat we van daaruit nooit rechtstreeks naar EenMan verwijzen, maar slechts door de vastliggende interface HuisVader. De reden waarom we het zo implementeren is omdat het duidelijk maakt dat een bepaalde rol of verantwoordelijkheid overeenkomt met een bepaalde groep methodes (interface). Dus interfaces zijn een manier om duidelijk in de code te stellen, dat je bezig bent met verantwoordelijkheden te definiëren. Het plezante gevolg is dat je code losser zal zijn: je zult minder afhankelijkheden hebben, of anders gezegd, je zult een klein stukje code gemakkelijker kunnen hergebruiken in een ander project, je zult het gemakkelijker kunnen wijzigen. Verantwoordelijkheden opsplitsen is even moeilijk als in het gewone leven: probeer een gebalanceerde groep objecten te hebben, die allemaal verantwoordelijk zijn voor een onderdeel. /** enkele tips (wekker 1) Elk object heef z'n eigen vast omlijnde verantwoordelijkheden. Die zijn goed verdeeld, er is niet een object die al het werk doet, elk doet een beetje, het werkend geheel zit in de interactie tussen de onderdelen. Als je naar de implementatie van de methodes kijkt zijn ze stuk voor stuk triviaal en heel kort. Dat is een teken dat het ontwerp goed is. Lange methodes zijn slechte stijl. De oplossing van elk probleem zit in opsplitsing in onderdelen. Je kunt daarin niet te ver gaan. Elke private variable wordt normaal gezien ingevuld in de constructor van het object. Private variabelen zijn altijd - gegevens die het object "bezit" (een alarm "heeft" een uur en een minuut getal) deze gegevens worden gemaakt in de constructor (AlarmClock bezit een Alarm, maakt het in z'n constructor) - gerelateerde objecten (een Alarm "kent" zijn Display, kreeg hem mee als parameter in z'n constructor) (Een Alarm moet z'n display kennen om tegen die display te kunnen zeggen setHours() en setMinutes(). Het Alarm zelf is bij uitstek het object om dat te doen, omdat dat object best weet wanneer zijn hours en minutes veranderen. en nooit tijdelijke variabelen: tijdelijke variabelen declareer je in de methode die ze nodig heeft. Elk object weet zo weinig mogelijk van de andere objecten waarmee het samenwerkt: - de beeper hoeft niemand te kennen, dus kent die ook niemand. - We gebruiken interfaces: die bepalen de minimale set methodes die het object "publiceert" aan de andere objecten. - We maken zoveel mogelijk methodes private - Alle variabelen zijn private, en als ze moeten kunnen gewijzigd worden, schijf je een set methode. Een methode van een object gebruikt variabelen uit dat object. Als dat niet zo is, staat je methode waarschijnlijk in het verkeerde object. (vb je moet geen setAlarmHours(int h) methode in het TimeUnit object maken, want TimeUnit heeft daar niks mee te maken. Een methode is - ofwel een vraag methode, die info terug geeft, maar geen wijzigingen aanbrengt (int TimeUnit.getHours(), boolean Alarm.isOn()) - ofwel een wijzigende methode zonder resultaat (void) (vb void Display.setHours()) - ofwel een actie-methode (in- en uitvoer, vb Beeper.start() Een methode die iets wijzigt en tgl iets teruggeeft is slechte stijl. Private variabelen en get/set methoden: vb in DefaultAlarm zit er een private boolean "on", en methodes boolean isOn(), void setOn() en void setOff() die de private "on" van waarde veranderen. De reden daarvoor is vooral dat de interface gebruiker (degene die met het type "Alarm" samenwerkt, vb AlarmClock) niet hoeft te weten dat we een boolean gebruikten om de toestand van het alarm te bewaren. Dat bepaalt ook wat je in de interface Alarm zult zetten: het concept dat je een alarm aan en af kunt zetten vertaal je naar een interface met twee functies die dat duidelijk aangeven. Een bijkomend voordeel is dat je kunt contoleren wanneer er iemand iets aan het object wil veranderen. Je kan vb telkens als die variable gewijzigd wordt ervoor zorgen dat de user interface weer ge-updated wordt (Alarm.advanceHour() roept display.setHour()). Of je kan de nieuwe waarde controleren op correctheid (advanceHour kijkt naar het uur: als het 24 wordt, springt het terug naar 0). Let op de strikte scheidingslijn die je kunt trekken tussen - de user interface objecten - ButtonPanel deze doet NIKS TENZIJ knopjes visualiseren, en buttonclicks doorgeven. - en de "business" classes - Display - TimeUnit - Alarm - Beeper - AlarmClock deze doen al het "denkwerk", en geen enkele gebruikt ButtonPanel. Als je een systeem ontwerpt moet je eerst business- klassen ontwerpen (vastleggen welke interfaces er meespelen en wie welke boodschap naar wie zal sturen om een bepaalde klus te klaren). De user interface bouw je achterna "daarboven" (daarmee bedoelt men dat de user interface als laagje boven de business classes komt, waarbij de bovenste laag wel functies van de onderste kan aanroepen maar niet omgekeerd: een onderste laag werkt op zich). Merk op: Display is toch ook een onderdeel van de user interface (=al wat op het scherm komt, en al wat van muiskliks en toetsenbord binnenkomt)? Daar wordt toch wel naar verwezen vanuit Alarm en vanuit TimeUnit? Klopt, het is een ontwerpfout die we nog moeten oplossen. Waarom is het een fout: stel dat je tgl 2 displays op het scherm wil (of natuurlijk x aantal displays) van dezelfde klok? Of stel dat de klok en het alarm dezelfde display gebruiken, afhankelijk van de toestand van een schuifschakelaar. In feite zijn dat user interface wijzigingen die de werking van het geheel (business logic) niet zouden mogen veranderen, maar probeer het maar eens te implementeren zonder aan de business klassen te komen. Maar er bestaat dus een heel elegante manier om dat te doen... Merk op: het is absoluut niet de gewoonte om voor bijna elke kasse die je gebruikt een afzonderlijke interface te specifiëren. Dat is eigenlijk pas zinvol voor klassen waarvan je weet dat je verschillende implementaties zult nodig hebben (vb voor Display). De kans is klein dat je Alarm op een andere manier wilt gaan implementeren. Maar het principe blijft: vastleggen welke methodes je wilt "publiceren" naar andere objecten toe. Het realiseren van die publicatie kan dan door de juiste methodes public en de andere private te zetten. Interfaces hebben het voordeel dat je ahw verschillende groepen methodes kunt publiceren naar verschillende objecten. */ /** enkele tips (wekker 2) Observer is een interface met 1 functie: void update(from,what). Een observer is een object dat "kijkt" naar een observable (vb de ui voorstelling van een object kijkt naar de business voorstelling). Een Observable is een object waaraan je (meerdere) Observers kunt hangen. Maw een observable heeft een lijstje observers. interface: addObserver(who), deleteObserver(who), notifyObservers(), en setChanged() vb: TimeUnit is een Observable (of in code: TimeUnit extends Observable) maakt dat je aan een timeUnit observers kunt hangen: timeUnit.addObserver(een observer). vb timeUnit.addObserver(time_display). De observable.notifyObservers() zal naar elke observer van de observable de boodschap update sturen (elke observer wordt op de hoogte gebracht van een update). Dus als je observable object (de time unit) op die manier wijzigt dat het invloed zal hebben op z'n observers (display), dan kun je die observers op de hoogte brengen van de wijziging door een notifyObservers(update) te doen. In het algemeen weet een observable (time unit) wel wanneer hij veranderd is (vb als iemand een bepaalde set-functie deed) maar hij weet niet of zijn toestand al stabiel is voor de observers (het kan zijn dat er nog sets volgen, het heeft geen zin om voor elke kleine weiziging de observers op de hoogte te brengen, als je weet dat er een reeks wijzigingen moeten gebeuren). Vandaar dat een observable in het algemeen nooit zelf notifyObservers() doet, de observable houdt enkel bij of zijn toestand veranderd is (met de functie setChanged()). Als een extern object (vb de button controller) boodschappen stuurt naar een observable (Alarm) die de toestand wijzigen, zal die button controller op het eind van z'n reeks boodschappen ook nog een notifyObservers() sturen naar de observable (Alarm). Op dat moment zal Alarm, INDIEN hij changed is, al z'n observers notifyen (naar elke observer wordt de boodschap update() gestuurd). Dus de omweg via setChanged is nodig omdat je geen tijd zou verliezen met het telkens notifyen van observers voor elke stap van een reeks veranderingen, je hoeft pas te notifyen op het eind van de reeks veranderingen. extends observable kun je niet op interface-niveau zeggen, omdat Observable een klasse is (MET implementatie, want ze houdt een lijst van observers bij, je hoeft dat zelf niet meer te implementeren). Een interface kan enkel interfaces extenden omdat een interface geen implementaties kan hebben (en dus ook niet kan erven). Dat is niet zo erg omdat het koppelen van de observers aan de observable een setup-probleem is: de addObserver doe je bij het creëren van de objecten, en daar weet je toch van welke klasse ze zijn omdat je bij creatie een klassenaam moet gebruiken (je kunt geen interfaces creeren, create gaat juist om het kiezen van een bepaalde implementatie (klasse)). Er gaat toch nog steeds communicatie van de businness naar de user interface? Inderdaad, de notifyObservers zegt tegen de observers (de user interface elementen kunnen zijn) dat het observable object veranderd is. Het verschil met vroeger is dat het Alarm nu enkel weet dat het "bekeken" wordt, terwijl het vroeger wist dat het juist 1 display had die voldeed aan de Display interface. Nu weet het Alarm veel minder, het alarm is minder sterk gekoppeld aan de andere objecten. Je moet objecten zo los mogelijk koppelen (vb door interfaces te gebruiken). Hier zie je wat ik bedoel met "groepen methodes selectief publiceren": voor Alarm is de Display geen display, maar een Observer. Alarm weet niet dat het een display is, het enige wat alarm weet is dat die observer een methode update() heeft. Vroeger kende de tijd bron (time unit, of alarm) zijn display, waardoor er - sterke koppling was tussen ui en business (de businness wist dat de Display methodes setHours en setMinutes had) - communicatie ging van businness naar user interface (de alarm.advanceHours() deed een display.setHours) Dat is twee keer slechte stijl. De goeie oplossing is de koppeling omkeren: de user interface componenten (Display) kennen hun businness voorstelling. De businness componenten kennen enkel "observers", abstracte dingen (kan vanalles zijn) die een update willen krijgen als de businness component veranderd werd. Er zijn veel voordelen: de displays zijn niet meer noodzakelijk: als de displays niet gemaakt worden blijft de clock toch werken. Anderzijds kun je meerdere displays koppelen, die allen tgl hetzelfde zullen weergeven, eventueel op verschillende manieren. Ten derde kun je andere objecten die geinteresseerd zijn in de huidige tijd, koppelen aan de time unit. Dat hoeven geen displays te zijn, eender welk object. Het zou vb heel "mooi" zijn om het object Alarm als observer van de time unit te installeren. Zo zijn beide volledig losgekoppeld van mekaar (tenzij door het observer principe), wat vb toelaat dat je verschillende alarm objecten kunt maken. Kritische geesten zeggen: de koppeling van Alarm naar Display is nu wel weg, maar er is een koppeling van display naar TimeSource bijgekomen. Dus echt minder koppeling is er toch niet? Onder "een koppeling tussen objecten" moet je begrijpen dat een bepaalde object toegang heeft tot bepaalde methoden in een ander object. De relatie tussen het alarm en z'n display, in versie 2, in mensentaal, is, - de display weet dat hij een time source heeft, waaraan hij getHours en getMinutes kan vragen. - het Alarm is een TimeSource (extends), concreet: - de display observes de TimeSource, dwz dat een time source (het alarm) weet dat hij observers (displays) heeft waartegen hij update() kan zeggen. De update van de display bevraagt de time source om zich te realiseren. In versie 1 was het zo - het alarm weet dat hij een display heeft waartegen hij kan zeggen setHours() en setMinutes() Het lijkt dus dat we erop achteruit zijn gegaan. De vooruitgang zit in als je begint te spelen met aantallen, als je denkt aan uitbreiding van het systeem. Wat moet een programmeur weten en coderen om een extra object te maken dat ook gebruik maakt van de Display? Als we in versie 1 een extra object maken dat ook een display heeft, dan maken we een analoge koppeling: het nieuw object kent z'n display en de set-methodes ervan. Telkens als er iets gebeurt met ons object gaan we manueel de setMethodes van onze display moeten oproepen. In versie 2 gaan we een nieuw object afleiden van TimeSource, waardoor de details in verband met de koppeling niet meer zichbaar zijn. Het enige wat we nu moeten weten is, dat we een setChanged() en een notifyObservers() moeten doen. Hoe ons nieuw object gekoppeld is aan de display, met welke interface, dat is iets wat in een andere "laag" is geprogrammeerd, de laag TimeSource-Display, en het observer/observable patroon. De details van die laag moeten we niet kennen. Het praktisch voordeel is hier redelijk klein omdat het een klein diplaytje is, maar je moet dit idee uitvergroten naa vb een spreadsheet programma, dat in verschillende windows tergelijkertijd dezelfde data visualiseert, maar een keer in grafiek, en een andere keer in getal vorm. Als men een getal aanpast, dan zouden we in versie 1 ZELF "MANUEEL" de grafiek moeten bijsturen, in versie 2 gaat het automatisch, omdat het de grafiek is, die de spreadsheet data gaat bevragen, om zich te kunnen tekenen. Je kunt het ook zien als een systeem waarbij in versie 1 het alarm zijn boodschappen push naar een display, zoals de telefoon een mens pusht op te nemen, terwijl in versie twee de display, als hij daar zin in heeft, het alarm poolt, bevraagt (zoals iemand z'n brievenbus ledigt wanneer hij wil). Een display moet niet geupdated worden als hij momenteel geminimized is, dat is ook een argument om aan te tonen dat het in elk geval niet het alarm is die de boodschap "herteken jezelf eens" moet sturen naar de display. De hele grafische user-interface-filosofie werkt trouwens zo: het initiatief tot efectief hertekenen van het scherm, komt van buiten uit, vb omdat een user een window verette waardoor een ander window zichtbaar werd. Maw, omdat het niet noodzakelijk zo is, dat een venster zich moet hertekenen, als het alarm veranderde van instelling, daarom is het niet aan het alarm om tegen z'n display te zeggen "herteken je" (setHour en setMinutes zijn eigenlijk herteken functies). Omdat het wel mogelijk is dat de display hertekend moet worden als het alarm van instelling veranderde, is dat het eninge wat het alarm aan z'n display mag zeggen: "ik ben veranderd". Dat doet de setChanged, notifyObservers. */ /* tips (wekker 3) De wijzigingen in deze versie: - het alarm is observer geworden van de time unit. Zo wordt het alarm net zoals een display, op zo los mogelijke manier gekoppeld aan de time unit: het enige wat de koppeling inhoud is, dat Alarm een update krijgt als de time unit ergens veranderd werd. - De thread die elke minuut de time unit en het alarm notifyde, moet nu enkel nog de time unit notifyen: het alarm wordt genotifyed door de time unit. Daardoor hebben we geen object AlarmClock meer nodig (dat object bestond enkel om van een Alarmclock de time unit en het alarm bij te houden voor de thread die beide elke minuut moest notifyen. Het alarm is nu tergelijker tijd observer (van de time unit) en observable (omdat de display's hem bekijken). Als je nu de main functie leest, zie je dat we maximaal ontkoppeled hebben, en zie je ook hoeveel "sterker" het systeem is geworden door zo ver mogelijk los te koppelen mbv het gebruik van interfaces, en observer/observable. - versie 1: dankzij het principe van interfaces, kunnen we verschillende soorten display's gebruiken zonder de andere klassen (die met Displays werken) te moeten aanpassen. - versie 1: het ui panel met knopjes is "los" van de business klasses, waardoor we het gemakkelijk kunnen vervangen door iets estetischer. Dat lukt omdat er enkel communicatie van dat panel naar een business class object gaat en niet omgekeerd. Dat is het heilige principe van gelaagd programmeren: de bovenste laag mag naar beneden toe communiceren maar nooit omgekeerd. Je ziet ook dat alles zou werken zonder dat panel, alleen is er dan nog geen manier om het alarm in te stellen, maar de point is dat het systeem zelf niet afhangt van een user interface object. - versie 2: een alarm of time unit kan meerdere displays hebben (dankzij observer/observable van TimeSource en Display), of helemaal geen display; alles blijft werken. - versie 3: een time unit kan meerdere (of geen) Alarms hebben, die elk apart ingesteld kunnen worden en allemaal tegelijkertijd actief zullen zijn, dankzij de observer/observable tussen de time unit en het Alarm. */ Er mag nooit 2 keer dezelfde code voorkomen in je programma. Indien dat zo is, dan moet je die code afzonderen in een aparte methode, en 2 keer die methode oproepen. Objecten hebben enerzijds methodes die een antwoord teruggeven, en anderzijds methodes die void zijn. Het is goede stijl om die soorten gescheiden te houden: de methodes die informatie teruggeven wijzigen niks aan het object, en de methodes die het object wijzigen geven void terug. over inheritance = overerving: extends, interface en implements wat: een klasse definiëren door een bestaande klasse te nemen, en ze groter, krachtiger of efficiënter te maken. mogelijkheden: Het kan zijn dat het verband tussen een groep klassen beperkt blijft tot de interface (naam van de beschikbare methodes). Het kan zijn dat er bepaalde implementaties gemeenschappelijk zijn, maar bepaalde dan weer niet Een interface in java is een klasse zonder implementatie = zonder method bodies. Als je een class van een interface wilt erven gebruik je "class MyBla implements MyInterface" Je kunt van meerdere interfaces erven: "class MyBla implements MyInterface, MyOtherInterface" Van een klasse overerven: class MyClass extends OtherClass combinatie mag: class MyClass extends OthreClass implements Interface, OtherInterface Je kunt ook een interface uitbreiding laten zijn van een interface: "interface BigOne extends SmallOne" In java erf je dus altijd alle publieke method definities. Dat betekent dan de sub-klasse altijd minstens alle methodes van de super-klasse bevat. Je erft daarenboven de method body als je extends gebruikt. Dat kan dus enkel bij klassen. Je kunt de geërfde implementatie wel overschrijven met een nieuwe implementatie. Dus, extends kun je gebruiken om enerzijds de interface uit te breiden (methodes en data toevoegen aan een klasse) en anderzijds om gedragingen te herdefiniëren, te herimplementeren. De bedoeling van overerving is polymorfisme: dat betekent concreet onder andere, dat de code { Animal a = ... a.getName() } niet vastlegd wat er juist zal gebeuren, omdat het effect kan variëren naargelang het "soort" animal. Met extends kun je dus een hiërarchie van "soorten" bouwen.