Göm menyn

Specifika projektprogrammeringstips

En kurs som denna har stora mängder kursmaterial. Det finns många hundra föreläsningsbilder, och en kursbok som Thinking in Java har över 1000 sidor att läsa.

Men var finns egentligen fallgroparna, där man kanske är lite snett ute trots att man först tror att man har gjort rätt? Vad behöver man tänka lite extra på när man går genom sin kod? Det är inte så lätt att få ut just den informationen ur den stora volymen kursmaterial.

Därför ger vi nu ett antal konkreta tips om vad man kan tänka på under den egna kodgranskningen. Tipsen har skapats med erfarenhet av hundratals projektinlämningar under cirka 10 års tid, och är alltså specifikt anpassade till just den här kursen och till studenter med just er bakgrund och situation under utbildningen. På det sättet kan vi komma ner från över 1000 sidor till bara 20-30 sidor, med fokus på just det som vi ser att kursdeltagarna kan behöva tänka lite mer på. I många fall finns dessutom en del hjälp från kodinspektionerna.


Det perfekta projektet skulle till 100 procent uppfylla alla kriterier nedan (och många fler), men det kan man så klart inte vänta sig att man ska uppnå på kursens 6 hp. Handledarna och examinatorn gör därför alltid en helhetsbedömning för att se vilket betyg du uppnår. Vissa "regelbrott" kan kompenseras av god kvalitet i övrig kod, medan andra kan vara så centrala och viktiga att man måste komplettera oavsett hur resten av koden ser ut. Detta beror inte bara på vilka kriterier man bryter mot utan hur man gör det. En del absoluta kriterier beskrivs på bedömningssidan.


I beskrivningarna nedan anges ofta #taggar. Detta är till för att vi ska kunna hänvisa till dem med unika namn, t.ex. i projektbedömningarna.

  1. Allmän kodstruktur och organisation

    Läsbarhet, struktur och organisation är viktigt eftersom programmering ofta går ut på att utöka och ändra existerande kod. Tid som läggs ner på att göra kod lättläst och välstrukturerad, och att dokumentera, är alltså inte bortslösad utan sparas in med råge i senare utvecklingsfaser. Slutmålet är därför inte bara att skapa ett program som åstadkommer rätt saker, utan att skapa ett program som gör på rätt sätt.

    Olika aspekter av detta har diskuterats under flera olika föreläsningar. Här ger vi några konkreta exempel på vad ni kan tänka på för att förbättra läsbarhet, struktur och organisation. Detta är långt ifrån uttömmande!

    1. #klassansvarHar klasser tydliga ansvarsområden?

      Under en föreläsning gavs exempel på klasser som gör många olika saker och som är svåra att beskriva med en enda mening. Sådana klasser kanske egentligen ska vara flera.

    2. #metodansvarHar metoder tydliga ansvarsområden, lagom omfattning?

      Om en metod gör flera separata saker (flyttar alla spelobjekt, kontrollerar kollisioner, applicerar powerups, uppdaterar spelets allmänna tillstånd) är det ofta bra att dela upp den i flera. Om en metod är väldigt lång gör den troligen flera separata saker. IDEA: Refactor / Extract / Method.

    3. #paketAnvänder du paket för att strukturera?

      All kod måste ligga i ett paket. I nästan alla projekt är det bra att dela upp koden i flera paket, där klasser som hör ihop (t.ex. alla klasser för powerups, logikformler, ...) ligger i samma paket.

      Paketindelning sker normalt efter funktionella gränser. Man delar alltså inte upp koden efter språkbegrepp, t.ex. alla enum-typer i ett paket och alla exceptions i ett annat paket. Istället handlar det om att strukturera koden efter hur den "hör ihop" funktionalitetsmässigt när det gäller begrepp som hör till själva projektet.

  2. Kortfattad och koncis kod

    Tzu-li and Tzu-ssu were boasting about the size of their latest programs. 'Two-hundred thousand lines,' said Tzu-li, 'not counting comments!' Tzu-ssu responded, 'Pssh, mine is almost a million lines already.' Master Yuan-Ma said, 'My best program has five hundred lines.' Hearing this, Tzu-li and Tzu-ssu were enlightened.

    – Master Yuan-Ma, The Book of Programming

    Undvik WET, Write Everything Twice – följ DRY, Don't Repeat Yourself!

    Inlämnade projekt innehåller ofta kodsnuttar som upprepas, identiskt eller med små variationer. Upprepningarna kan ske inom en och samma metod, mellan metoder i samma klass, eller mellan olika klasser.

    Detta är problematiskt eftersom man får mer jobb att skriva, läsa och underhålla koden, och riskerar att ändringar inte genomförs konsekvent på alla platser där koden upprepas. Det är viktigt att man redan tidigt lär sig tänka på detta och lär sig skriva kod på ett effektivt sätt – att lära sig bra tekniker kan spara mycket tid i längden!

    Man kan ofta undvika onödig repetition genom hjälpmetoder (anropas flera gånger med olika parametrar), abstrakta hjälpklasser (samlar gemensam implementation inom en arvshierarki), eller liknande. Vet du inte hur du ska göra: Fråga innan du lämnar in!

    Notera att detta inte är något vi ignorerar. Det är vanligt med komplettering för upprepad kod, så gå genom exemplen nedan (och kom ihåg att det finns fler varianter av repetition än dessa).

    1. #radupprepningUndviker du att upprepa mer eller mindre identiska rader?

      Även om raderna inte är helt identiska, för att de använder olika värden på några platser, kan man bryta ut dem till en metod som anropas flera gånger med olika parametrar.

    2. #deluttryckGer du namn till deluttryck som används flera gånger?

      Om abs(mouseEvent.getX() - (BUTTON_X + BUTTON_WIDTH / 2)) används mer än en gång: Tilldela värdet till en lokal variabel (som kan deklareras final), och använd denna. Detta kan förbättra läsbarheten förvånansvärt mycket.

      På motsvarande sätt kan följande:

      p.setxDir(Calc.calculateDirection(ss.getX(), ss.getY(), toX, toY)[0]);
      p.setyDir(Calc.calculateDirection(ss.getX(), ss.getY(), toX, toY)[1]);
      	    

      ...gärna bytas ut mot:

      double[] direction = Calc.calculateDirection(ss.getX(), ss.getY(), toX, toY)
      p.setxDir(direction[0]);
      p.setyDir(direction[1]);

      Därifrån kan man så klart gärna gå vidare och införa en klass för riktningar, istället för en array där [0] och [1] inte är meningsfulla index:

      Direction direction = Calc.calculateDirection(ss.getX(), ss.getY(), toX, toY)
      p.setxDir(direction.x);
      p.setyDir(direction.y);

      Att bryta ut variabler kan man göra även för deluttryck som bara används en gång, vilket diskuteras under självförklarande kod.

    3. #samlingar Använder du listor istället för multipla variabler?

      Vi ser ofta kod där man skapar flera olika variabler istället för att använda en enda lista. Detta kan verka väldigt frestande när man bara har 2 eller 3 objekt av en viss typ, t.ex. spelare:

      
      	      public class Game {
      	          private Player player1;
      	          private Player player2;
      	          // ...
      	      }
      	  

      Det kan också gälla att man har exakt 2 färger (svart och vit) i schack, att man har exakt 4 banor i ett spel, eller liknande. Med ett litet konstant antal blir det frestande att skapa separata variabler för var och en, precis som ovan. Då kanske man också får metoder som getPlayer1() och getPlayer2().

      Problemet är att man ofta vill göra något med alla spelare, färger, eller banor. Det är mycket lättare att göra det om man har en lista ("för alla spelare i listan") än när man har två separata variabler. Annars blir det lätt en del upprepad kod, där man skriver om samma sak för player1 och player2.

      
      	      public class Game {
      	          private List<Player> players;
      
      	          private void tick() {
      	              for (Player player : players) { ... }
      	          }
      	          public Player getPlayer(int index) {
      	              return players.get(index);
      	          }
      	      }
      	  

      Eller så vill man slumpa fram en bana från de banor man har. Detta är enkelt om man bara kan slumpa fram ett index i listan på banor, och plocka ut levels.get(index). Om varje bana är en egen variabel får man istället en lång switch- eller if-else-sats: if (level == 3) return level3; else....

      Där ser vi också varför detta inte bara är en fråga om hur mycket kod vi skriver utan om modellering. Har vi en uppsättning av någonting vill vi kunna referera till detta som en uppsättning, till exempel listan av spelare. Med separata variabler för varje spelare missar vår modell kopplingen mellan dem: Att de hör ihop.

    4. #metodlikhet Kombinerar du metoder som gör nästan samma sak?

      Om två metoder gör nästan samma sak (har nästan samma ansvarsområde): Se om det är lämpligt att kombinera dem till en metod, där en parameter avgör vilken variant som utförs. Övning 4.2 visade en specifik variant av detta generella problem.

      En variant av detta kan uppstå i t.ex. schackspel: För många olika pjästyper kan det till exempel finnas behov av att kontrollera hur långt man kan gå i en viss riktning. Även här kan en riktning anges av en enum-klass (Direction med 8 värden för de 8 raka och diagonala riktningarna), och man kan ha en enda kontrollmetod som stegar enligt det deltaX och deltaY som anges av Direction-objektet!

    5. #abstraktklass Har du infört abstrakta klasser där detta passar?

      Om flera klasser lagrar samma typ av information, och dessa klasser "hör ihop" begreppsmässigt: Du behöver kanske en abstrakt klass som kan lagra den gemensamma informationen.

      Exempel: Player, Ball och Powerup lagrar x/y-positioner och har getters och setters för dessa. Vi inser att anledningen till likheten är att de alla är entiteter som ska visas på skärmen, vilket är ett intressant begrepp att införa. Därför adderar vi den abstrakta klassen ScreenEntity som representerar detta begrepp och innehåller den gemensamma funktionaliteten.

    6. #hierarkinivå Där du har typhierarkier, har du samlat kod på rätt nivå?

      Om samma eller liknande metodimplementation repeteras flera gånger, men i olika klasser i en hierarki: Ofta ska man ha en gemensam metod i en gemensam superklass. Detta kan i vissa fall vara en abstrakt klass. Exempel: Flera relaterade klasser har (nästan) samma kod för kollisionshantering.

    7. #datasomkod Använder du filer för att läsa in större mängder programdata?

      Det är till exempel möjligt att definiera spelbanor genom kod som konstruerar dem steg för steg, men om detta leder till 300 rader av kod som skapar "new Obstacle()" och placerar dessa objekt på rätt plats, kommer denna kod inte att räknas i projektet. Dessutom kan man fundera på om man istället ska skriva kod som läser in banor från en datafil, vilket ger mer flexibilitet för användaren att skapa egna banor. Inläsning från fil gjordes bland annat i en uppgift i Tetris, betyg 4 (användning av Gson).

    8. #idearep Har du använt IDEA för att hitta vissa former av repetition?

      Med hjälp av Analyze | Locate Duplicates kan man hitta vissa former av potentiellt repetitiv kod. Den kan ge många falska varningar, och missar mycket av vad som diskuteras ovan – men den är ändå potentiellt hjälpsam.

    9. #klasskombination Kombinerar du klasser som gör nästan samma sak?

      Om två klasser gör nästan samma sak (har nästan samma ansvarsområde): Se om det är lämpligt att kombinera dem till en klass, där en (konstruktor)parameter avgör skillnaden. Ett exempel på detta kan finnas i händelsehantering i GUI. Se till exempel följande klasser:

      private class UpAction extends AbstractAction
      {
          @Override public void actionPerformed(final ActionEvent e) {
      	game.move(Direction.UP);
          }
      }
      
      private class RightAction extends AbstractAction
      {
          @Override public void actionPerformed(final ActionEvent e) {
      	game.move(Direction.RIGHT);
          }
      }
      
      private class DownAction extends AbstractAction
      {
          @Override public void actionPerformed(final ActionEvent e) {
      	game.move(Direction.DOWN);
          }
      }
      
      private class LeftAction extends AbstractAction
      {
          @Override public void actionPerformed(final ActionEvent e) {
      	game.move(Direction.LEFT);
          }
      }

      Här kan samtliga klasser lätt ersättas med en:

      private class DirectionAction extends AbstractAction
      {
          private Direction dir;
          public DirectionAction(Direction dir) {
      	this.dir = dir;
          }
          @Override public void actionPerformed(final ActionEvent e) {
      	game.move(dir);
          }
      }

      Detta har också den stora fördelen att man inte behöver skapa nya sådana klasser om man inför nya riktningar (Directions). Istället kan samma klass återanvändas för samtliga riktningar.

      Om det till att börja med ser ut så här, då?

      private class UpAction extends AbstractAction
      {
          @Override public void actionPerformed(final ActionEvent e) {
      	game.moveUp();
          }
      }
      
      private class RightAction extends AbstractAction
      {
          @Override public void actionPerformed(final ActionEvent e) {
      	game.moveRight();
          }
      }
      
      private class DownAction extends AbstractAction
      {
          @Override public void actionPerformed(final ActionEvent e) {
      	game.moveDown();
          }
      }
      
      private class LeftAction extends AbstractAction
      {
          @Override public void actionPerformed(final ActionEvent e) {
      	game.moveLeft();
          }
      }

      Då går det inte lika enkelt att byta ut koden, eftersom det är olika metoder som anropas. Men då börjar problemet helt enkelt ett steg tidigare: Man behöver oftast inte olika metoder för att flytta sig i olika riktningar, så man borde börja med att införa move(Direction), och sedan fortsätta enligt exemplet ovan.

  3. Självförklarande kod

    There are two ways of constructing a software design: One way is to make it so simple that there are obviously no deficiencies, and the other way is to make it so complicated that there are no obvious deficiencies.

    – C.A.R. Hoare, 1980 ACM Turing Award Lecture

    Mycket kan vinnas i fråga om läsbarhet om man siktar på att skriva självförklarande kod. Ju tydligare koden själv visar vad den gör och varför, desto färre kommentarer behövs. Då minskar också risken att man glömmer uppdatera en kommentar när koden ändras!

    Whenever you think, "This code needs a comment", follow that thought with, "How could I modify the code so its purpose is obvious?"
    Talk with your code, not your comments.

    Här är det väldigt viktigt att man inför lämpliga abstraktioner, som låter koden säga vad som faktiskt menas på en högre nivå. Eloquent Javascript ger ett träffande exempel som vi kan återge i modifierad form:

    int total = 0, count = 1;
    while (count <= 10) {
        total += count;
        count += 1;
    }

    Denna kod är ganska kort och vi kan ganska lätt lista ut vad den gör: Summerar talen från 1 till 10. Men varför ska vi behöva lista ut något? Tänk om vi inför funktionen range:

    int total = 0;
    for number in range(1, 10) {
        total += number;
    }

    Då behöver vi inte längre lista ut vad loopen egentligen gör med count-variabeln utan kan börja prata om intervall av tal. Och om vi inför funktionen sum:

    int total = sum(range(1, 10)); 

    Då kan vi direkt se vad koden gör, utan att lista ut att den räknar ut en summa. Här blir vi alltså hjälpta av att införa nya begrepp som vi kan använda för att beskriva vad programmet ska utföra. Sedan kan det hända att programmet totalt sett blir längre när man tar med definitionen av sum och range, men det blir ändå lättare att förstå.

    Nedan diskuterar vi bland annat att införa begrepp och att ge dem informativa namn.

    1. #variabelnamn Används informativa och rättvisande namn på variabler, inklusive parametrar och fält?

      Då minskar behovet av dokumentation. Exempel:
      int i = 10; // Number of pixels to move for each step

      Förbättrad namngivning (som också gör det lättare att förstå vad som händer på alla ställen som faktiskt använder variabeln):
      int pixelsPerStep = 10;

      En lista över uppkopplingar i en nätverksklient kan heta connections eller kanske currentConnections, medan c inte är informativt och windows är helt missvisande, även om varje uppkoppling har ett eget fönster på skärmen.

      Tänk dig att några andra kursdeltagare för första gången skulle titta på en enskild metod i din kod. Skulle de förstå vad namnen betyder? Ger namnen tillräcklig information utan att man redan vet allt i förväg, som du själv gör? Är namnen otvetydiga, svåra att missförstå även om man "försöker"?

      Samlingar av objekt namnges normalt i plural, utan att man anger typ av samling: List<Connection> connections; istället för List<Connection> connectionList;. Eftersom namnet är plural framgår det redan att det handlar om en samling.

    2. #metodnamn Används informativa och rättvisande namn på metoder?

      Precis som för variabler minskar detta behovet av dokumentation. Exempel: En metod som flyttar alla fiender i ett spel kan gärna heta moveEnemies(), medan update() visserligen stämmer men inte är så informativt.

      Metodnamn är normalt verb, eftersom metoder säger till objekt att göra något. Till exempel döper man normalt inte en metod till fileReader() eller fileReading() utan till readFile().

    3. #klassnamn Används informativa och rättvisande namn på klasser och gränssnitt?

      Då minskar behovet av dokumentation. Till exempel är Info och Data inte så informativa namn, då i princip alla objekt innehåller information / data.

      Tänk på att klassnamn normalt är substantiv i singular: Tower, inte Towers. Därmed gör man t.ex. "new Tower()" (ett nytt torn), inte "new Towers()" (flera nya torn?).

    4. #begreppsnamn Har du infört namngivna begrepp när detta underlättar läsbarhet?

      Både metoder och lokala variabler kan användas för att ge meningsfulla namn och göra koden självförklarande. Vi ger flera exempel, eftersom detta är viktigt:

      1. Från Best practices for commenting your code: Använd meningsfulla identifierare och konstanter, även om de bara används en gång!

        // Before
        // Calculate monkey's arm length
        // using its height and the magic monkey arm ratio
        double length = h * 1.845; //magic numbers are EVIL!
        // After - No comment required
        double armLength = height * MONKEY_ARM_HEIGHT_RATIO;

        Liknande exempel: Även korta uttryck som Math.abs(x1-x2), som används en eller flera gånger i ett större uttryck, kan tilldelas till en egen namngiven variabel innan de används: int deltaX = Math.abs(x1-x2). Detta förklarar direkt vad deluttrycket innebär.

      2. Anta att man implementerar ett schackspel. Tekniskt sett går det utmärkt att testa om vissa koordinater ligger inom spelbrädets ramar på följande sätt:
        1 <= x && x <= board.getWidth() &&
        1 <= y && y <= board.getHeight()

        Men om vi inför metoden contains(x,y) i spelbrädet, blir det lättare att läsa, förstå och kontrollera korrektheten:
        board.contains(x,y)

        Detta gäller även om uttrycket bara används en enda gång – för genom att införa namnet "contains" förklarar koden på en högre nivå vad den egentligen försöker åstadkomma!

      3. I samma schackspel: Tekniskt sett går det utmärkt att kolla om det finns en pjäs på en viss position genom att testa om board.getPiece(x,y) == null. Men genom att införa board.isEmptyAt(x,y) inför vi ett nytt intressant begrepp, och vi slipper bry oss om implementationsdetaljen att "null" innebär en tom ruta.

      4. Kod som testar om (player.getRightEdge() - offset >= monster.getLeftEdge()) && player.getRightEdge() - offset <= monster.getRightEdge() blir lättare att läsa om vi inför metoden between(lower, value, upper), och anropar between(monster.getLeftEdge(), player.getRightEdge() - offset, monster.getRightEdge(). Inte bara för att det blir lite mindre kod att läsa, utan för att vi direkt ser att vi testar om ett värde är mellan två andra.

    5. #magiskt Är programmet fritt från magiska konstanter?

      Exempel: Ser man width/30 vet man kanske inte vad 30 betyder. Följande är lättare att förstå:

      public final static int BLOCK_SIZE = 30;
      ...width/blockSize...

      I vissa fall är konstanter av en mer lokal natur. Då går det bra att istället använda en "lokal konstant" för att definiera ett namn:

      final int blockSize = 30;
      ...width/blockSize...

      Bra namngivning är ett bra sätt att göra koden mer självförklarande och minska behovet av kommentarer.

      Exempel 2:

      // Shuffle the deck (standard decks have 52 cards)
      for (int i = 1; i <= 52; i++) {
          // 53 is the deck size + 1
          deck.swap(i, i + randomInt(53-i) - 1);
      } 

      Inför vi en namngiven konstant blir 52/53 självdokumenterande. Vi kan använda en lokal konstant (final int), eller deklarera konstanten i klassen (till exempel private final static int DECK_SIZE = 52;). Då blir det också enkelt att ändra värdet vid behov. Här kunde man annars ändra alla "52" men glömma "53" – eller råka ändra värdet 52 på en plats där det egentligen var en skärmkoordinat istället för antal kort!

      1
      // Shuffle the deck
      final int deckSize = 52;
      for (int i = 1; i <= deckSize; i++) {
          deck.swap(i, i + randomInt(deckSize+1-i) - 1;
      }
    6. #enum Används enum-typer överallt där en variabel kan ha ett fåtal förutbestämda värden?

      Uppräkningsbara typer (enum) är ett utmärkt verktyg för att se till att en variabel verkligen bara kan få rätt värden, som dessutom inte kan blandas ihop med värden av andra typer.

      Med andra ord, undvik t.ex. String direction="up"; // eller "down", "left", "right" och int state = 1; // eller 2...7. Med en enum kan man bara ange korrekta värden (exempel: mode = NORMAL, INVISIBLE, GHOST).

      Att inte använda enum leder ofta till följdproblem och komplettering.
    7. #förenkling Har du försökt förenkla kod som kan vara svår att förstå?

      Tänk på att även andra ska kunna läsa koden. Förenklingar bör vara det första steget, innan man väljer att dokumentera.

  4. Dokumentation och kommentarer

    Som du kan se från föregående avsnitt satsar vi mycket på att göra koden mer lättläst, för att behovet av dokumentation och kommentarer ska minska, men det betyder inte att behovet helt försvinner.

    1. #strukturdok Har du skrivit övergripande dokumentation för programmets struktur?

      För projektet ska detta finnas i projektrapporten, eftersom man ofta behöver läsa dokumentationen för att överhuvudtaget veta vilka klasser man är intresserad av, om den ens är relaterad till en specifik klass.

      Ett par förkortade exempel på vad som kan passa in här: "Alla rörliga objekt på skärmen är subklasser till Mover. Vill man ha flygfunktionalitet kan man ärva från Flier istället, eftersom detta har inbyggda funktioner för att landa. Varje rörligt objekt har en egen bakgrundstråd och rör sig individuellt med synkronisering via... /alternativt/ Vi använder en centraliserad spelloop som med jämna mellanrum anropar samtliga rörliga objekts uppdateringsmetod så de kan röra sig ett steg. Objekt som vill uppdateras mer sällan kan ange detta genom..."

    2. #lättförståelig Är koden tillräckligt lättförståelig och kommenterad i övrigt?

      Tänk på att handledaren och examinatorn ska läsa genom hela projektet!

    3. #kommentarstyp Ger kommentarerna rätt typ av förklaringar?

      Det är lätt hänt att man skriver en enkel förklaring av vad koden gör, steg för steg – ibland är detta motiverat, men ibland leder det helt enkelt till en duplicering av information som man lätt kunde ha fått genom att läsa själva koden istället. Med andra ord, kommentera inte det som är fullständigt uppenbart:

        windows.add(myNewWindow); // Add the window to the list

      Kommentarer som förklarar varför man gör något eller varför man gör det just på detta sätt är ofta mer värdefulla, liksom kommentarer som förklarar koden i ett större sammanhang. Denna typ av kommentarer är ofta svårare att ersätta med lättläst kod.

  5. Korrekt objektorientering

    1. #infoklass Lagras information i rätt klass?

      Ofta är det tydligt var information hör hemma. Om ett spelarobjekt kan rita ut sig själv på skärmen, så är det lämpligt att spelarbilden lagras just i detta objekt, och att det är spelarobjektet som håller reda på hur stor bilden är. Vi har sett exempel där detta ansvarsområde delas upp mellan flera olika klasser (till exempel genom att bildhöjden är hårdkodad i en annan klass), vilket oftast inte är korrekt.

    2. #livstid Lagras information med rätt livstid?

      Använd till exempel inte fält i onödan. Om information bara behöver lagras under ett visst metodanrop och ingen återanvänder den efter att metoden avslutas, är det bättre att lagra den informationen i en lokal variabel. Annars riskerar man till exempel att flera metodanrop (parallella eller rekursiva) skriver över varandras värden då de använder samma objekt och alltså samma fält.

    3. #barakonstrueraAnvänder du konstruktorer bara för att konstruera?

      Konstruktorer bör användas för att konstruera ett objekt och ge dess fält initialvärden. Det kan vara frestande att lägga mer kod i konstruktorn för att man alltid vill köra den så snart ett objekt har konstruerats:

      public class Game {
          public Game() {
              // ... construct ...
              // ... set field values ...
              // ... start game thread ...
          }
      } 

      Men konstruktorn ska användas för att konstruera objektet, se till att fält sätts till rimliga värden. Sedan ska vi anropa andra metoder för att göra saker med objektet. I detta fall skulle man sannolikt vilja ha en separat metod för att starta en bakgrundstråd för spelet, så att vi har två separata operationer:

      • Skapa ett spelobjekt som håller reda på speldata.

      • Starta en tråd så att spelet kör igång.

      Samma gäller här:

      public class StrongPowerup {
          public StrongPowerup(Player player) {
              // ... construct ...
      	player.setStrength(100);
          }
      } 

      Det vore bättre att istället ha en separat metod apply(Player player), så man frikopplar skapandet från användandet.

      Ett tredje exempel:

      public class Dialog {
          public Dialog() {
              // ... construct GUI, etc ...
              // ... ask user a question ...
          }
      } 

      Här vill vi istället att dialogklassens konstruktor skapar dialogobjektet (lagrar relevanta data i relevanta fält) och att en separat metod sedan anropas för att visa dialogen så att användaren kan svara på frågan.

    4. #staticvariabler Undviker du static-variabler eller använder du dem korrekt (undantagsfall)?

      Static-variabler (till skillnad från globala konstanter) har primärt ett acceptabelt användningsområde: Lagring av data som måste vara unika/globala, och som inte kan skickas med till alla som behöver dessa data. Varje enskild static-variabel måste därför motiveras tydligt:

      • Varför är det viktigt att just denna information bara kan lagras en gång, i ett globalt tillstånd?

      • Varför skulle det vara alltför problematiskt att lagra just denna information i ett objekt som skapas en gång och skickas med till de som behöver den?

      Ett spelexempel:

      • Lagrar du data som statiska variabler, kanske för att andra lättare ska få tag på dem?

        public class GameBoard {
            public static Player player1;
            public static Player player2;
            public static List<Monster> Monsters;
            public static GameState currentState;
        } 

        Tänk om! Vi håller på med objektorientering, och data ska lagras i objekt så långt det går.

        I framtiden kan du dessutom vilja göra om ditt projekt för att spela flera parallella spel, och det går inte om du har deklarerat att du definitivt bara har ett currentState för alla pågående spel. Även om du vet att du inte vill ha flera åt gången, kommer vi att slå ner på statiska (globala) variabler då det leder till många andra problem: Icke-lokalitet, brist på kontroll över vem som använder informationen, implicita kopplingar mellan olika delar av ett system, problem vid multitrådad programmering, med mera. Det är bättre att från början lära sig att undvika statiska variabler, och senare (med mer erfarenhet) förstå de begränsade fall när de faktiskt är motiverade.

        Undantaget är (1) namngivna konstanter, som public static final int SIZE = 20, och (2) data som faktiskt gäller själva klassen, som vårt tidigare exempel private int circlesCreated = 0;

      • Skapar du ett globalt tillgängligt GameBoard istället?

        public class GameBoard {
            public Player player1;
            public Player player2;
            public List<Monster> Monsters;
            public GameState currentState;
            public static GameBoard theOneAndOnly = new GameBoard();
        } 

        Tänk om! Du har kommit en bit på vägen, men i och med att du har ett enda unikt GameBoard har du ändå nästan alla problem som vi diskuterade nyss. Skicka istället med GameBoard till de som behöver den – i vissa fall innebär det att du skickar GameBoard till ett annat objekts konstruktor så att objektet permanent kan känna till Gameboard, men ofta är det bättre att istället skicka med GameBoard till de metoder som behöver det just då.

      • Använder du public static HighScoreList board = new HighScoreList(); för att det är bekvämt att alla får tillgång till listan utan att du behöver ge den till dem? Om du är helt säker på att du aldrig vill ha flera listor, och det är krångligt att skicka med listan till de som behöver den, kan detta vara acceptabelt.

        Men är det verkligen krångligt? Är det inte egentligen så att listan kunde skapas på ett ställe (spelet) och möjligen skickas med i ett par metodanrop till något enstaka ställe där den också behövs? I så fall har vi ju inget behov av en global variabel (static), utan kan använda en helt vanlig variabel istället! Spelklassen kan ju lätt se till att inte skapa två listor, så det finns ingen risk att "råka" få flera när man bara vill ha en.

    5. #singleton Undviker du Singleton-mönstret, eller använder du det korrekt (undantagsfall)?

      Singleton-mönstret används för att kunna komma åt ett unikt objekt av en viss klass från godtycklig punkt i programmet. Det delar därför många av nackdelarna med static-variabler, som diskuterades i förra punkten. Detta vill vi oftast undvika. Singleton ses ibland som ett "anti-mönster" som används alltför ofta!

      Varje användning av Singleton måste motiveras tydligt: Varför är det viktigt att informationen bara kan lagras en gång, i ett globalt tillstånd?

  6. Ärvning, polymorfism med mera

    Typhierarkier, ärvning och polymorfism är kraftfulla verktyg, men de ska inte användas till allt, och det finns ett antal fallgropar som har diskuterats under kursens gång.

    1. #användhierarkier Använder du typhierarkier där detta är rimligt?

      Om flera klasser hör ihop begreppsmässigt (Rectangle, Circle Octagon) kanske de bör ha en gemensam supertyp som reflekterar vad som är gemensamt för dem (Shape). Detta kan till exempel vara ett gränssnitt (interface).

    2. #undvikhierarkier Låter du bli typhierarkier där dessa inte behövs?

      Det kan hända att det som nu är flera olika klasser egentligen borde vara samma klass. Se föreläsningsbilderna, avsnittet om ärvning, "Fördelar och nackdelar!". Där finns exempel på en persondatabas där det visade sig orimligt att ha en typhierarki med en klass för varje "typ" av person, och bättre att ha en enda klass för alla personer.

      Se även Ärvning: Undvik onödiga underklasser i samma föreläsning. Vi ska inte skapa nya klasser bara för att det går, utan för att vi faktiskt behöver modellera något som är så annorlunda att det inte passar in i en existerande klass. Om bara data skiljer sig åt (t.ex. styrkan på en spelare) är det nästan alltid bättre att göra detta till en parameter, och att inte skapa några subklasser. Konkreta exempel finns i föreläsningsbilderna. Detta är en relativt vanlig anledning till komplettering.

    3. #komposition Gör du rätt val mellan typhierarkier och komposition?

      Är den eller har den? Om B är subtyp till A, så ska B vara en speciell sort/variant av A, det vill säga "alla B är A" (alla cyklar är fordon).

      Vi ska alltså inte låta BankAccount ärva från ArrayList bara för att det är ett bekvämt sätt att kunna lagra en lista av transaktioner. Ett bankkonto är inte "en speciell sorts lista"! Istället är det bättre att bankkontot har en lista – komposition istället för ärvning, som diskuteras i föreläsningsbilderna.

      En praktisk anledning till att undvika detta, utöver argumentet att ett bankkonto inte är en sorts lista, är att när man ärver från en klass gör man också alla den klassens publika och "protected" metoder tillgängliga för andra. Det blir alltså möjligt att manipulera bankkontot med alla listmetoder. Detta kan vara farligt och/eller förvirrande (när man blir överväldigad av alla ArrayList-metoder som inte är relevanta för bankkontot). Även av den anledningen kan det vara bättre att bankkontot har en lista.

      Att man ärver superklassens metoder innebär också att man får se upp om superklassen ändras och lägger till nya metoder, kanske med samma namn och parametertyper som dina egna metoder.

      Detta betyder så klart inte att ärvning alltid är dåligt – ärvning har viktiga användningsområden, men är inte lösningen på alla problem.

    4. #typkontroll Undviker du att göra "egen" typkontroll? Använder du polymorfism där detta är rimligt?

      (Se även föreläsningen om ärvning, avsnitt "Egen typkontroll i Java".) Vi ser ibland projekt där man använder olika sätt att ta reda på vilken typ ett visst objekt har inom en typhierarki, och agera olika beroende på typen:

      • if (x instanceof Monster) { monsterCollision(); }
        else if (x instanceof Player) { playerCollision(); }

      • if (x.getClass() == Monster.class) { monsterCollision(); }
        else if (x.getClass() == Player.class) { playerCollision(); }

      • if (x.getType() == Type.MONSTER) { monsterCollision(); }
        else if (x.getType() == Type.PLAYER) { playerCollision(); }

      • if (x.isMonster()) { monsterCollision(); }
        else if (x.isPlayer()) { playerCollision(); }

      • ...

      IDEA varnar för vissa av dessa men inte för alla "workarounds", till exempel de rader som visserligen undviker "instanceof" men ändå gör i princip samma sak: Inspekterar objektets typ och agerar därefter.

      Det är just detta som subtypspolymorfism är till för! I 999 fall av 1000 är det bättre att låta Monster och Player ha en gemensam supertyp, till exempel GameObject. Låt GameObject ha en abstrakt collision()-metod. Låt Monster och Player implementera den på varsitt eget sätt: Monster gör monsterCollision(), medan Player gör playerCollision(). Och istället för att göra en test som i exemplet ovan, anropa helt enkelt x.collision(). Då ser den dynamiska bindningen till att rätt implementation används.

      Behövde du någon information som bara fanns tillgänglig i klassen där testet fanns? Skicka då med den som parametrar till din nya collision()-metod!

      Användning av instanceof, eller liknande konstruktioner, är en relativt vanlig källa till kompletteringar!

      Obs: Detta gäller även om det bara är en enda test som görs: "Om det är ett monster, gör så här. Annars gör vi inget." – Detta är också en typkontroll som kunde hanteras genom att monstrets metod gör "så här", och andra klasser har tomma metoder som inte gör något alls.

      Obs: Det finns vissa situationer där detta kan vara svårare att införa än annars, till exempel där det man ska göra beror på typen hos två olika objekt, inte bara ett (x ovan). Om du tror du har ett sådant fall: Fråga examinatorn i god tid innan inlämning!

  7. Felhantering och robusthet

    1. #felmeddelanden Ges rimliga felmeddelanden vid felaktig inmatning från användaren?

      Ett program ska aldrig krascha på grund av att användaren har matat in "felaktig" information, vare sig det sker via inmatningsformulär eller via styrning av ett spel med tangentbord, mus eller liknande. Vi kan förutse att användaren kan göra i princip vad som helst, och behöver ta hand om det för att kunna ge rimliga felmeddelanden.

      Detta kan hanteras via exceptions eller via returvärden.

    2. #filersaknas Agerar programmet korrekt om medskickade filer (bilder, ...) saknas?

      Många projekt använder sig till exempel av bilder som ska skickas med som en del av programmet. Om dessa bilder saknas kan man tänka sig att återhämta sig genom att istället t.ex. rita en enfärgad rektangel... men detta är inte ett krav. Istället kan det vara korrekt att signalera att filen saknas (ge ett tydligt felmeddelande med t.ex. System.out.println() eller en dialogruta) och sedan avsluta programmet med System.exit(felkod). Det är också OK om programmet helt enkelt kraschar på det ställe där filen skulle läsas in , till exempel genom att man gör new ImageIcon(...getSystemResource(...)...), där new ImageIcon(...getSystemResource(...)...) returnerar null. Då kan i alla fall programmeraren se var felet uppstod.

      Men det är inte korrekt att ignorera att filen saknas när man försöker läsa in en bild, så att (t.ex.) en bildpekare fortsätter vara null, så att programmet sedan kraschar på någon helt annan plats – till exempel i paintComponent()! Man ska inte heller avsluta programmet utan att signalera problem med felmeddelande eller exception – det blir förvirrande att programmet bara avslutas.

    3. #stacktrace När du fångar ett undantag som inte kan "fixas", skriver du ut (eller loggar) en stacktrace?

      Som vi har diskuterat under föreläsningarna är det väldigt bra för avlusning (debugging) att veta både var fel uppstår och vem som då hade anropat vem. Då behöver man se till att anropskedjan till exempel skrivs ut (som en stack trace) med ex.printStackTrace().

      Detta är så klart inte det enda som behöver göras: Man behöver också se till att felet hanteras eller att (t.ex.) användaren meddelas att något gick fel. Loggar räknas inte som meddelanden till användaren.

    4. #hanteracatch Hanterar programmet fångade exceptions (undantag) på rätt sätt?

      Om du väl har fångat ett fel, låt då inte programmet bara gå vidare. Föreläsningsexempel:

      public class WordProcessor {
          private Document doc = null;
      
          public WordProcessor(String filename) {
              try {
                  loadFile(filename);
              } catch (FileNotFoundException e) {
                  System.out.println("File not found");
              }
             System.out.println("Size is " + doc.getLength());
          }
      
          private void loadFile(String filename) throws FileNotFoundException {
              FileInputStream is = new FileInputStream(filename);
              // ...
              doc = parseDocumentFrom(is);
          }
      }

      I detta exempel: Om filen saknas skriver konstruktorn ut "File not found", men fortsätter sedan vidare och får NullPointerException i anropet till doc.getLength().

      Detta innebär även att main() inte ska skicka exceptions vidare till "systemet", utan fånga och ta hand om dem.

  8. Designmönster

    Att använda namngivna designmönster i projektet är absolut inget krav; vi tittar lite på dem i labbarna och det räcker för kursens syften. Vi vill ändå varna för vissa designmönster som ibland leder till modelleringsproblem i projekten.

    1. Om du använder State, använder du det korrekt?

      Meningen med State-mönstret är att man ska kunna byta ut beteende hos ett objekt beroende på det tillstånd det är i. Med beteende menas här programkod – ett objekt har tillstånd (data) och beteende (metoder som kan anropas).

      Det räcker då till exempel inte att ett State-objekt har en metod som returnerar en konstant. Man måste ha en eller flera metoder som kan anropas i State-objektet och som faktiskt gör något kvalitativt annorlunda beroende på vilken typ av State-objekt man har just nu.

      Om man bara behöver hålla reda på "vilket av de 5 tillstånden jag är i just nu", så att annan kod (utanför State-klasserna) kan agera olika beroende på tillstånd, räcker det utmärkt att använda med enum med 5 olika värden. Att använda det fullständiga State-mönstret i detta läge blir då "overkill".

    2. Om du använder MVC, är det då den bästa lösningen?

      Varning: MVC-mönstret (Model/View/Controller) är ofta för komplicerat att applicera på projekten. Detta mönster användes i Tetris, där vi helt separerade datamodellen (Board) från grafisk visning. Detta var relativt enkelt då Tetris har en enkel modell. Skriver man till exempel ett spel med många olika spelare och andra entiteter som ska visas på skärmen, kan det vara bättre (i ett mindre projekt som detta) att hoppa över MVC och låta varje sådan klass ha hand om både modell och grafisk visning. Med andra ord, att låta Player både hålla reda på var spelaren är, hur många liv den har, och hur den ska se ut och kanske animeras. Annars är det lätt att man får en alltför centraliserad lösning med en enda utritningsklass som ska känna till hur allt ska se ut, vilket istället bryter mot decentraliseringsprinciper.

  9. Körbara program

    Kan projektet enkelt köras på någon annans dator – till exempel hos handledaren som ska testa inlämningen? Detta kräver mer av programmet.

    1. #användresurser Läses programmets egna filer in via resurser?

      När ett program läser in egna medskickade filer, t.ex. bilder, spelbanor eller andra datafiler, måste programmet först hitta filerna. Men programmet kan vara installerat var som helst, och kan till och med vara ihoppackat i en enda fil, ett JAR-arkiv. Därför ska man inte använda vanlig filhantering för sådana filer, utan Javas inbyggda stöd för "resurser". Då hittas filerna automatiskt på samma ställe som själva programmet. Vi har diskuterat detta under GUI-föreläsningen och använder då ClassLoader.getSystemResource().

      Notera att filer som användaren skapar varken kan eller bör hanteras på detta sätt. Där är det användaren som bestämmer var de ska ligga, och användaren som måste ange korrekt sökväg (på kommandoraden, i en konfigurationsfil, via fildialog, eller liknande).

      Samma gäller filer som programmet skapar under körningen. Dessa kan inte sparas som en del av programmet (i JAR-arkivet) utan sparas på annan lämplig plats, t.ex. i nuvarande katalog.

      HomeOrSrcDirectory upptäcker vissa fall där man refererar till en hemkatalog eller en src-katalog. Detta är korrekt i vissa fall, men inte för att läsa filer som skickas med projektet. Då bör man använda resurshantering istället.

      FileFromResource upptäcker vissa fall där man hämtar in en resurs-URL men sedan skapar ett File-objekt från den. Detta antyder att man försöker läsa in en fil via vanlig filhantering, vilket inte fungerar om resursen ligger packad i en JAR-fil.

      ImageIconFromFile upptäcker vissa fall där man skapar en ImageIcon från ett filnamn. Här bör man använda resurshantering istället.

      ImageFromFile upptäcker vissa fall där man använder ImageIO tillsammans med ett filnamn. Här bör man använda resurshantering istället.

      PathFromResource upptäcker vissa fall där man plockar ut en sökväg från en resurs-URL. Detta kan bero på att man sedan vill använda sökvägen för att öppna en fil, vilket inte täcker fallet där resursen t.ex. pekar ut en fil inuti ett JAR-arkiv.

      FileSeparatorInGetResource upptäcker vissa fall där man försöker använda File.separator inuti ett anrop till getResource(). Detta är fel: File.separator är olika på olika plattformar, men getResource() använder en URL, där separatorn alltid är /.


Sidansvarig: Jonas Kvarnström
Senast uppdaterad: 2022-01-14