Design

Indata till designen är kravspecen som den uttrycks i de modeller som skapades under analys. Syftet med design är att tänka igenom hela det avsnitt av koden som ska konstrueras och att skapa en tydlig bild av hur koden ska struktureras. De klasser, metoder osv som finns i designmodellen är tänkta att finnas i koden också.

Design är en väldigt viktig aktivitet för att få en genomtänkt kod. Det är också en relativt svår aktivitet som mycket handlar om erfarenhet. Här är centrala delar av "designkunskaperna" sammanfattade i några viktiga principer och designmönster.

Just eftersom design är så viktig måste vi komma ihåg att det inte ska vara mer än "en stunds eftertanke för att komma mer rätt från början". Designen kommer aldrig att bli så bra att kodningen blir en rent mekanisk översättning av diagrammen till källkodsfiler. Mycket av det som finns i designmodellen kommer att ändras vid kodning. Det är knappast meningsfullt att ägna mer än en halv dag åt design i en enveckas iteration.

Designens grundprinciper

Det här är väldigt långt ifrån en fullständig kurs i objektorienterad design, vi tar bara en kort titt på några av de allra viktigaste principerna. Först av allt, syftet:

Det ska vara lätt att ändra programmets beteende och att utöka dess funktionalitet, detta ska kunna göras utan att behöva ändra i befintlig kod.

Når vi dit kan programmet leva länge utan att koden degenererar till en ounderhållbar gröt som ingen vågar ändra i.

Definition - Implementation

Definitionen är det som andra objekt kan se, tex namn på klasser och interface och signaturer för publika metoder. Implementationen är till exempel privata metoder och koden som publika metoder innehåller. Ingenting får vara beroende av implementationen, det enda som ska användas av andra objekt är definitionen. På så sätt är det möjligt att ändra i implementationen utan att påverka någon annan kod än den som ändras. Betrakta detta lilla exempel från collection-API:et i paketet java.util:
List är ett interface som innehåller definitioner av metoder för en lista. ArrayList och LinkedList är klasser som innehåller helt olika implementationer av en lista, dvs helt olika kod. Låt oss nu anta att något objekt använder en lista på det här sättet:
List list = new ArrayList();
list.add(obj);
list.get(2);
//en massa andra operationer på listan.
Helt plötsligt kommer vi på att det var dåligt att använda ArrayList, vi borde ha en LinkedList i stället. Det enda som behöver göras är att ändra första raden till List list = new LinkedList(). Detta innebär att en helt annan kod (implementationen) kommer att exekveras men tack vara att vi bara refererade till interfacet List (definitionen) krävde det nästan ingen alls förändring av koden! Interface borde användas på detta sätt i mycket större utsträckning än vad som är fallet!

Detta resonemang gäller även på andra nivåer, vi kan till exempel ha ett paket med bara en publik klass (en sådan klass brukar kallas fasad). Dess publika gränssnitt utgör då paketets definition och alla andra klasser i paketet utgör dess implementation.

Det är ofta ofarligt att ändra en implementation men livsfarligt att ändra en definition eftersom det kan finnas väldigt många beroenden av den.

Inkapsling

Enligt resonemanget ovan är viktigt att ha en stabil definition. Exakt vad är det då som utgör definitionen av till exempel en klass? Jo, det är allting som någonting utanför klassen kan se, dvs klassens namn samt signaturen av dess publika variabler, metoder samt inre klasser och interface (dvs dess publika gränssnitt). Desto mindre denna definition är desto lättare är det hålla den stabil. Alltså bör klassen ha få publika metoder och helst inga publika variabler alls.

Hur är det då med klasser, metoder osv som är protected eller paketprivata? Protected behöver väldigt sällan användas så det hoppar vi över. Paketprivat åtkomst är däremot mycket vanlig. Den är mycket närmre besläktad med privat åtkomst än med publik. Låt oss anta att vi ska utveckla en viss begränsad funktionalitet, till exempel hantering av ett bankkonto (inte av ett helt banksystem utan bara av ett konto). Vi inser att det finns olika typer av konton, olika räntor osv varför det inte räcker med en enda klass. Vi utvecklar då ett kontopaket istället för en kontoklass. Detta kontopaket blir en mycket väl sammanhållen enhet med ett tydligt publikt gränssnitt. För tydlighets skull råkar paketet innehålla mer än en klasser. Att dessa klasser anropar varandras metoder (paketprivat åtkomst) är dock ett relativt ofarligt beroende eftersom de är en så väl sammanhållen enhet. Detta resonemang håller inte alltid till fullo eftersom ett pakets innehåll inte alltid är så väl sammanhållet. Det viktigaste är dock att begränsa den publika åtkomsten även om privat i och för sig är bättre än paketprivat. Protected undviker vi om vi inte vet mycket väl varför vi ska ha det.

En annan aspekt av inkapsling är att variabler helst alltid ska vara privata, ovanstående resonemang gäller främst allt utom variabler. Anledningen till att variabler ska vara privata är att om de accessas uteslutande via metoder i klassen har vi full koll på hur de accessas. Vi kan till exempel i metoderna utföra omvandlingar mellan enheter, utföra säkerhetskoller, göra typkonverteringar osv. Att accessa variabler via metoder gör också koden robustare. Om vi till exempel vill byta typ på variabeln behöver vi inte byta typ på det publika gränssnittet i metoden som accessar den. Vi har också möjlighet att till exempel synkronisera access av variabeln om det helt plötsligt skulle bli nödvändigt.

Hög sammanhållning (high cohesion)

En klass ska ha väldefinierad kunskap och en väldefinierad uppgift. En klass Anställd ska till exempel inte innehålla en massa information om företaget den anställde jobbar på. Den kan i stället ha en referens till en klass Företag som innehåller den informationen.

Vidare ska klasser bara innehålla metoder som är relaterade till det klassen representerar. Klassen Anställd ska till exempel innehålla metoder som hanterar dess namn och adress och vilket företag den jobbar på, medans klassen Företag ska innehålla metoder som hanterar till exempel lönehantering för den anställde.

En slutsats av detta blir att det är viktigt att dela upp klasser. Det är bara alltför vanligt att program innehåller för få och för stora klasser som har för mycket och för oklar kod som snart blir grötig och svårunderhållen.

Låg koppling (low coupling)

Klasser ska ha litet beroende av andra klasser, dvs de ska inte i onödan anropa metoder på andra klasser eftersom koden i dem då påverkas av ändringar av koden i de anropade klasserna. Olika typer av beroenden är dock olika farliga. Det är till exempel ofarligt att använda klasserna i paketen java.* som levereras med J2SE eftersom dessa klasser är extremt stabila. Mer generellt är ett beroende ofarligare ju stabilare den anropade klassen är. Vidare är det ofarligare med ett beroende av en klass i samma paket än av en klass i ett annat paket eftersom vi har större koll på klassen i samma paket. Desto större koll vi har på klassen vi är beroende av detso ofarligare är beroendet.

Några fler tips (patterns)

Kanske inte direkt designens grundprinciper men vanliga lösningar på vanliga problem

Controller

Problem

Vart ska ett system event (anropen från aktören i SSD) ske? Vem ska hantera dem?

Lösning

Klasser som har detta ansvar kallas controller.

En variant är att använda klasser (eller metoder i klasser) som representerar scenarion i ett use case. Det är bra (för tydlighets skull) om namnet på use case:t finns med i namnet på metoden (om det är en metod per use case/scenario) eller klassen (om det är en klass per use case/scenario). Denna variant kallas use case controller.

En annan lösning är att inte associera controllern med scenarion utan med en del av programmet (tex ett paket). Klassen hanterar då all användning av subsystemet oavsett scenario. Detta kallas en facade controller.

Observera att en controller aldrig ska vara en del av användarggränssnittet.

Creator

Problem

Vem ska skapa nya instanser av klasser?

Lösning

Det är lämpligt att A skapar instanser av B om

Arkitektur

Arkitekturen är systemets "skelett", hur det ser ut i stora drag. Arkitekturen talar inte om hur problem löses, snarare att de kan lösas och var de löses.  Exakt hur arkitekturen ska se ut kan vi inte veta nu, det är tvärtom det främsta syftet med elaboration att fastställa en arkitektur. Är vi väldigt osäkra på arkitektur är det bästa antagligen att börja utveckla systemet förutsättningslöst och se vad det resulterar i för arkitektur. I det här fallet säger erfarenheten att det är lämpligt att bygga arkitekturen kring ett arkitekturellt mönster som kallas MVC (Model View Controller). Vi måste dock vara beredda att ändra och utveckla arkitekturen vartefter systemet utvecklas och vi får nya insikter om det.

MVC

MVC delar in systemet i tre delar: modell, vy och kontroller. Modellen innehåller programmets data, där sker också all hantering av datat. Vyn består enbart avanvändargränssnittet, utan någonsom helst logik. Kontrollern är den som hanterar användarens inmatningar (skriva text, klicka med musen osv).

Det finns många olika möjligheter att implementera MVC, bilden ovan är bara en grov skiss av en lösning. Vi kommer att titta lite mer på det här i period tre.

Design av ett use case

Efter denna blixtkurs i design är det dags att designa koden för use case:t överför pengar mellan konton.

Vi gör designen genom att rita sekvens- eller kollaborationsdiagram för de två metoderna i vårt system sequence diagram. Den första metoden är startaOverforing. Tills vidare struntar vi i allt som har med subsystemet view att göra, dvs vi bryr oss inte om hur användargränssnittet fungerar.

StartaOverforing

Första uppgiften är att välja en controller eftersom det är den som ska ta emot anvädarens inmatningar. Kandidater till controller är klasser i domänmodellen som kan anses hantera överföringen, till exempel Kassör och Kassa. Vi väljer Kassa med tanken att alla use case som startas i kassan ska hanteras av den (facade controller, se ovan). Kan hända kommer det i framtiden visa sig att det blir ohanterligt många use case, då får vi dela på klassen Kassa men tills vidare har vi den som controller:

Vad ska hända i modellen när metoden startaOverforing anropas? Ingenting ska uppdateras eftersom vi ännu inte vet något om vilka uppdateringar som ska ske, det kommer i nästa operation (angeKtoOchBelopp). Det enda vi kan göra här är att skapa (och koppla ihop) alla objekt som behövs för att hantera angeKtoOchBelopp. Vilka är då det? Vi tar en titt i domänmodellen och konstaterar att de enda objekt som är specifika för en viss överföring är overforing och overforingsRad. Övriga objekt måste redan finnas. overforingRad ska hanteras av overforing, alltså måste vi skapa overforing först. Vem ska då skapa overforing? Vi har i och för sig inte så mycket att välja på, det enda objekt som finns så här långt är kassa, men vore det helt vansinnigt att använda det skulle vi få införa ett nytt objekt. En titt på mönstret Creator ovan säger dock att det är rimligt att kassa skapar overforing eftersom överföringar registreras i kassan. Nu ser diagrammet ut så här:

Slutligen ska overforingsRad skapas. Enligt domänmodellen ska objekt av OverforingsRad finnas "i" overforing. Detta säger oss att overforing behöver någon datastruktur som ska innehålla alla overforingsRader. Enligt Creator är det uppenbart att overforing själv ska skapa denna datastruktur eftersom den ska innehålla den, har extremt hög koppling till den och det dessutom knappast finns något annat objekt över huvud taget som har någon koppling till den:

AngeKtoOchBelopp

Även här börjar vi med att välja controller och av samma anledning som ovan väljer vi Kassa:

Enligt scenariot i use case:t ska detta anrop ska resultera i att ett kvitto genereras, att beloppet uppdateras på de berörda kontona och att transaktionen loggas.

Vi börjar med att ändra beloppet på de två kontona. Ett konto skapas inte när ett uttag (eller insättning) ska göras, det finns redan på något sätt. Hur? Och var? "På riktigt" skulle det säkert finnas i en databas men databaser ingår inte i den här kursen och dessutom påverkas inte designen av var det finns. Vi kan helt enkelt anta att kontobjektet redan finns i minnet. Vem ska hålla referensen till kontot? Vi tar en titt i domänmodellen. Det kan inte vara Överföring eller ÖverföringsRad eftersom det är objekt som bara finns medans use case:t pågår. Det kan inte heller vara KontoSpecifikation eftersom den bara ska innehålla information som är gemensam för alla konton av en viss typ och inte till exempel saldo. Kund kanske kunde vara en idé men då har vi samma problem med vem som ska ha referensen till Kund. För att förenkla designen i det här exemplet väljer vi inte kund. Kassör, Kassa och BankKontor har egentligen inget med Konto att göra, enligt princien low coupling väljer vi ingen av dem. Den enda som återstår nu ärBank. Vi väljer den, det är rimligt att det finns en koppling mellan en bank och dess konton. Kassan ska alltså be Banken om de konton den ska uppdatera. Detta innebär att det måste finnas en koppling från Kassa till Bank. Den skapas lämpligtvis redan när programmet startas, det måste vi komma ihåg att fixa. Nu ser diagrammet ut så här:

Sedan ska eventuella uttagsavgifter dras. Information om uttagsavgifter finns i KontoSpecifikationer som finns i KontoKatalogen. Hur får vi tag i KontoKatalogen? Det är lämpligt att fundera i termer av high coupling, vem bör ha hand om den informationen? Ett konto vet rimligvis vilken typ det är av. Det är också bäst att Konto sköter hanteringen av uttagsavgifter själv, skulle vi låta Kassa sköta den blev det krångligt om någon annan än Kassa skulle göra ett uttag. En bra lösning verkar vara att ett Konto alltid är associerat med rätt KontoSpecifikation:

KontoKatalog används bara när ett konto skapas för att slå upp en KontoSpecifikation av rätt typ och associera den med det nyskapade Kontot.

Nu har vi sett till att saldona uppdateras, nu är det dags attskapa kvittot. Informationen på kvittot beskrivs av en Öveföring. Den skapades redan i StartaÖverföring, nu ska vi fylla i dess innehåll. Eftersom Kassa redan har rätt Överföring och Överföring ska innehålla ÖverföringsRader är det lämpligt att Kassa säger åt Överföring att skapa ÖverföringsRaderna:

Vi struntar i att designa loggningen nu så därför är vi klara med modellen. Nu går vi över till vyn.

Vyn

Vi kommer att diskutera design av grafiska användargränssnitt i period tre, nu ska vi bara se hur UI-klasserna kan kopplas ihop med modellen. Det är lämpligt att ha en JPanel i botten av varje skärmbild vi skapar eftersom vi då är fria att lägga den i vilken JFrame, JPanel eller JApplet som helst. Det use case vi håller på att designa behöver en skärmbild OverforingPanel, den kanske ser ut ungefär så här:

En titt på vår arkitektur (MVC) säger att allting användaren gör ska hanteras av controllern. Vyn ska inte göra något annat än att visa aktuellt data för användaren och att ge användaren möjlighet att utföra operationer (tex genom att klicka på en knapp). Eftersom det är via vyn användaren ger indata kan vi byta ut aktören kassör i diagrammen ovan mot klassen OverforingPanel. En fråga är var metoderna actionPerformed ska finnas. Det är alltid bäst att översätta till användargränssnittsoberoende händelser så långt ut som möjligt, de ska alltså ligga i OverforingPanel (lämpligtvis i inre klasser). På så sätt blir controllern oberoende av användargränssnittet.

Om datat i modellen uppdateras och detta ska visas i vyn är den klassiska MVC-lösningen att vyn på något sätt lyssnar efter händelser från modellen, se avsnittet arkitektur ovan. I det här use case:t behövs ingen sådan hantering, det enda som händer är att kassa returnerar ett kvitto (Overforing) som vyn ska hantera på något sätt:

Det var det, nu har vi designat hela scenariot. I period tre kommer vi att prata mer om hur modellen, controllern och vyn kan kommunicera med varandra.

Use case StartUp

I varje iteration är det lämpligt utveckla tillräckligt mycket av use caset StartUp för att kunna köra den funktionalitet som läggs till i iterationen. Det är bäst att utveckla StartUp sist för att veta säkert vad som ska startas.

Aktören i detta use case är metoden main. Det enda den ska göra är att skapa alla nödvändiga objekt och koppla ihop dem. Det bästa är att main skapar få större objekt och att dessa sedan "vecklar ut sig", dvs skapar sina "underobjekt" och kopplar ihop dem. Vi skulle kunna nöja oss med att main skapar ett objekt i modellen, förslagsvis bank, och ett i vyn, tex någon JFrame som ska visas när programmet skapas:

i kod blir main inte mycket mer än:

public static void main(String[] args) {
    Bank bank = new Bank();
    new MainJFrame(bank.getKassa());
}

Klassdiagram

Innan vi börjar koda kan det underlätta att rita ett klassdiagram. Här kommer ett för modellen:

Undantag

Checked exceptions ska användas för alla fel som inte kan upptäckas vid programmering. Det är till exempel ett typiskt checked exception att det inte finns tillräckligt mycket pengar på ett konto för att kunna utföra ett uttag. Å andra sidan är det ett typiskt unchecked exception att försöka läsa på för högt index i en array. I det första fallet ska vi kasta en egenutvecklas klass som ärver av Exception, i det andra fallet kastas automatiskt ArrayIndexOutOfBoundsException som  ärver av RuntimeException.

Här kommer några enkla riktlinjer för hur undantag ska användas:
  1. Undantagen ska ha namn som beskriver felet.
  2. Skapa inte onödigt många undantagsklasser. Finns det många undantagsklasser finns det risk att metodsignaturer ändras hela tiden när vi kommer på nya undantag. Det kan räcka med en klass per "subsystem", tex kastar alla klasser i paketet java.sql undantaget SQLException oavsett vilket fel som uppstod i databasen. Om vi använder få klasser är det klokt att låta undantaget innehålla tex en sträng som beskriver felet.
  3. Undantag ska inte propagera upp genom flera lager/skikt. 
  4. Många fel kan åtgärdas direkt men en del fel måste ramla hela vägen upp till klienten och visas för användaren. Ett annat subset av felen ska förmodligen loggas.  

Paket

Hela diskussionerna som fördes om "low coupling - high cohesion" och om inkapsling i avsnittet om designens grundprinciper gäller paket precis lika väl som klasser. Det är viktigt att ett pakets publika gränssnitt är stabilt, om ändringar sker inuti ett paket är inte lika farligt. Desto fler beroenden det finns av ett paket, desto stabilare måste det vara.

Vad gäller vårat banksystem börjar alla paketnamn med att identifiera organisationen där de utvecklas:

se.kth.isk
.leffe

Därefter kommer produktens namn:

se.kth.isk.leffe.bank

Sedan kommer namnet på skiktet där paketet finns:

se.kth.isk.leffe.bank.model
se.kth.isk.leffe.bankview

Sedan kan vi eventuellt dela upp skikten i ytterligare paket efter funktionalitet. Om det finns några generella "bra att ha"-klasser kan vi lägga dem i ett eget paket:

se.kth.isk.leffe.bank.util
se.kth.isk.leffe.bank.view.util
se.kth.isk.leffe.bank.model.util

Koden som startar programmet kan lämpligen ligga i ett eget paket:

se.kth.isk.leffe.bank.startup

Tester

Tester kan ligga i ett paket test inuti det paket de testar. Tester för klasser i tex paketet
se.kth.isk.leffe.bank.model  hamnar alltså i så fall i paketet se.kth.isk.leffe.bank.model.test. Ett annat alternativ är att lägga testerna i en anonym inre klass i den klass de testar.