CSC

 

Laboration 4 - Designmönster

Designmönster beskriver beprövade lösningar som kan användas i flera olika, men liknande, sammanhang inom objektorienterad programmering. Med designmönster i "verktygslådan" kan designproblem lösas snabbare och säkrare än utan. Vissa mönster är enkla och lätta att lära sig medan andra är lite komplicerade.

När man ska lära sig att använda ett nytt mönster praktiskt så är det en god idé att programmera upp mönstret på ett litet "leksaksexempel" för att verkligen förstå hur det är tänkt att fungera.

I den här labben ska ett par vanliga och populära mönster studeras genom att små exempel programmeras.

Mönstret Composite

Idén med mönstret Composite är att grupper av objekt ordnade hierarkiskt i en trädstruktur ska behandlas på samma sätt som enstaka objekt. Ett sammansatt objekt (composite object) innehåller andra objekt som själva kan vara sammansatta eller enstaka. Ett typiskt exempel på Composite är en filkatalog där en katalog kan innehålla både filer och andra kataloger som i sin tur kan innehålla både filer och andra kataloger o.s.v. Filerna är löv i trädet. Ett annat exempel är en bild som byggs upp av grafiska primitiver (linjestycken, ovaler, rektanglar m.m) och sammansättningar av grafiska primitiver där en sammansättnng kan innehålla både andra sammansättningar och primitiva objekt. I Java-biblioteket finns ett riktigt paradexempel på Composite, nämligen klasserna Component, Container och alla dess subklasser. En Container ärver från Component och kan innehålla många Components. Dessa kan själva vara Containers eller så kan de vara "löv"-komponenter som inte kan innehålla några andra.

En beskrivning av Composite som inleds med ett UML-diagram finns här: www.dofactory.com/Patterns/PatternComposite.asp

Deluppgift 1 -- Resväska som Composite

När man ska packa en resväska kan man använda sig av mönstret Composite. Koftor och byxor lägger man direkt i resväskan medan hårspännen och gummisnoddar stoppas i små askar eller påsar som kanske i sin tur läggs i en necessär eller större påse som läggs bredvid större plagg i väskan. Strumpor läggs i en påse som läggs i väskan.

Implementera de klasser som behövs för en resväska enligt mönstret Composite. Den klass som kallas Component i mönstrets vanliga beskrivning är en abstrakt klass från vilken klasserna för de enskilda prylarna såväl som klassen eller klasserna för sammansättningarna ärver. Allra minst tio saker av minst fem olika typer ska packas (t.ex. tjock tröja, jeans, strumpa, armband, penna ... använd fantasin!). Minst tre olika nivåer av sammansättningar ska användas. T.ex. att hårspännen ligger i en påse som ligger i en necessär tillsammans med tvål och schampoo och att necessären ligger i resväskan tillsammans med större klädesplagg.

Välj själva att göra olika klasser för de olika sorternas prylar eller använda samma klass med en String som beskriver prylen. Även när det gäller sammansättningarna kan man välja att ha en enda klass för alla eller en för Necessär, en för Påse osv.

Varje pryl ska ha ett pris och en vikt som instansvariabler. Dessa ges värden när objekten skapas genom en standardmässig konstruktor. Två operationer ska finnas (Operation() i allmänna beskrivningen av mönstret): De två operationerna är pris() och vikt() som returnerar motsvarande data. Varje sammansättning ska innehålla en lista (t.ex. en ArrayList) med de komponenter den innehåller och beräkna sitt pris och sin vikt genom att gå igenom listan och summera.

Skriv ett testprogram som bygger upp en resväska. Resväskans totala vikt ska beräknas i ett metodanrop, t.ex. resväska.vikt() och skrivas ut, priset visas på motsvarande sätt. Tänk på att själva väskan och packpåsar har egen vikt och pris utöver vikt och pris för innehållet!

Observera att inga rekursiva metoder ska skrivas. Det är mönstrets struktur som ser till att hela trädstrukturen gås igenom då en metod anropas i trädets rot.

Det är tillåtet att programmera ett annat exempel på mönstret Composite om ni kommer på något passande. Det måste innehålla det antal objekt och det antal nivåer som krävs i exemplet ovan.

Factory-teknik

Det finns flera designmönster som innehåller ordet Factory. Gemensamt för dem är att man inte skapar objekt på det vanliga sättet med new utan genom ett metodanrop. Metoderna ses som "fabriker" där objekt skapas (inuti metoderna står det förstås new) och vanliga namn på metoderna är getInstance() eller create(). Typen för det skapade objektet bestäms i metoden utom räckhåll för klienten (användaren). Vanliga skäl för den här tekniken är att användaren ska slippa bry sig om objektets typ eller att det säkrare blir rätt typ om programmet väljer och inte användaren.

Deluppgift 2 -- Human factory

Impementera en abstract klass Human med två konkreta subklasser Man och Woman. I Human ska en factory-metod finnas :
    public static Human create (String pnr) {
        // metodkropp
    }
Vi förutsätter att parametern är ett riktigt personnummer (ni behöver inte kolla det). Om näst sista siffran är udda så ska en instans av Man returneras, om den är jämn returneras ett objekt av Woman.

Skriv ett testprogram som skapar objekt av Man och Woman med hjälp av metoden ovan. Se till att det inte går att skapa objekt av Man och Woman på annat sätt än genom metoden ovan. Det ska alltså inte gå att kompilera new Man(...) eller new Woman(...) i testprogrammet. Det får inte heller gå att skapa en anonym subklass till Human.

Tips: Gör ett paket human som innehåller Human och dess subklasser. Paketet läggs på en underkatalog med samma namn. Med hjälp av lämpliga modifierare för synlighet (public, private m.fl.) kan man åstadkomma den begärda strukturen. Lägg testprogrammet på katalognivån ovanför paketet. Se också till att testprogrammet på något sätt bekräftar att objekt av rätt typ skapats, t.ex. genom en metod toString() som skriver ut information om objekten. Exempel:

    :
    Human anna = Human.create("Anna", "xxxxxx-012x");
    Human magnus = Human.create("Magnus","xxxxxx-011x");
    System.out.println(anna);
    System.out.println(magnus);
kan ge utskriften
    Jag är kvinna och heter Anna
    Jag är man och heter Magnus

Redovisning och krav på programmen

  • Demonstrera en körning av Composite-test-programmet där total vikt och totalt pris för resväskan (eller motsvarande struktur) skrivs ut.
  • Som vanligt ska ett UML-klassdiagram över klasserna visas upp.
  • Rita även upp ett objektdiagram över de objekt som Composite- testprogrammet skapar. Diagrammet behöver inte följa UML-standard men tydligt visa vilka objekt som finns.

  • Demonstrera en körning av Human-factory-programmet där en man och en kvinna skapas.
  • Demonstrera en annan version av programmet där man försöker med new Man(...) och new Woman(...) men kompilering misslyckas.

Extrauppgift - Mönstret Iterator

Syftet med mönstret är att iterera eller "gå igenom" alla element ur en sammansatt struktur utan man behöver befatta sig med den underliggande strukuren. Att använda en iterator på en lista är naturligt och implementationen av en listiterator är ofta enkel.

Även för mer komplicerade strukturer som har man god nytta av iteratorer. Composite-sammansättningar och andra trädstrukturer kan mycket väl förses med iteratorer. Iteratorn "levererar" elementen i en sekvens utan att man behöver veta hur strukturen är uppbyggd. "Man" är här den som använder iteratorn. Den som programmerar iteratorn måste förstås känna till strukturen. Naturliga val för genomgångar av en trädstruktur är t.ex. pre-, in- och post-order.

Lite mer om iteratormönstret finns här:
www.dofactory.com/Patterns/PatternIterator.asp

Extrauppgift - Två iteratorer över en Composite

Uppgiften här är att implementera två iteratorer på den Composite-struktuer som gjordes i labbens första del. De två iteratorerna ska gå igenom elementen i preorder respektive bredden-först-ordning. Preorder betyder att roten besöks först, därefter alla delträd från vänster till höger, använd preorder på varje träd. I bredden-först tar man efter roten alla rotens barn, därefter alla rotens barnbarn o.s.v.

Iteratorerna ska implementera följande interface (eller ett snarlikt):

interface Iterator {
    Object first();       // returnera första elementet
    Object next();        // gå fram ett steg och returnera nästa element
    boolean isDone();     // returnera true när alla element är besökta
    Object currentItem(); // returnera aktuellt element utan att gå vidare
}
Skriv två klasser som implementerar interfacet, en för preorder-genomgång och en för bredden-först-genomgång av Composite-strukturen. All åtkomst av Composite-elementen måste göras från Iteratorklasserna. Därför är det en god idé att ge Iteratorklasserna en konstruktor som tar en Composite (den ni använd i labbens första del) som parameter.

Att skapa själva iterator-objekten är en uppgift för Composite-klassen som alltså ska utvidgas med en metod liknande

    Iterator getIterator (String s) {
        if (s.equals("PO"))
            return new POIterator(this); //Preorder-iterator
        else
            return new BFIterator(this); //Breadth-first-iterator
    }
Metoden närmast ovan är den enda ändring/utvidgning som är tillåten i de ursprungliga Composite-klasserna! Ev. utvidgningar som är naturliga delar av klasserna och som kunde ha gjorts från början är tillåtna. T.ex. går det bra att lägga till toString-metoder om sådana saknas.

Iteratorn får förutsätta att Composite-objektet är likadant under hela iterationen. Om objekt läggs till eller tas bort under iterationen så duger det att iteratorn visar hur Composite-objektet såg ut då iteratorn skapades.

Labbredovisning

Testning av de nya iteratorerna går bra att göra i samma program som demonstrerar Composite-strukuren då labbens första del redovisas. Skapa de två iteratorerna, iterera var och en och kontrollera genom någon utskrift att alla element finns med och att de kommer i den förväntade ordningen.

Sidoansvarig: <ann "at"nada.kth.se>
Senast ändrad 16 april 2010
Tekniskt stöd: <webmaster@nada.kth.se>