I denna deluppgift ska ni beskriva och analysera spelramverket från laboration 1. Ni ska lämna in följande:
Laborationen görs i grupp precis som de föregående delarna. Inlämning görs via Fire.
Rita ett UML-diagram som beskriver de klasser/interfaces, samt relationer mellan dessa, som finns i er valda kodbas. Gör en avvägning kring relationer till externa klasser/interfaces (dvs klasser/interfaces som inte är en del av kodbasen); ta me de relationer som känns viktiga för att klargöra kodbasens design, utelämna de som inte gör det.
Analysera kodbasen utifrån de principer vi diskuterat i kursen. Peka ut något (minst en sak) som bryter mot någon av principerna och förklara hur det bryter mot principen i fråga. Identifiera och förklara (minst) tre olika användningar av design patterns vi tagit upp i kursen.
I denna deluppgift ska vi refactorera spelramverket. Refactorering är definierat enligt Wikipedia så här:
Refactoring is the process of changing a computer program's source code without
modifying its external functional behavior in order to improve some of the
nonfunctional attributes of the software. Advantages include improved code
readability and reduced complexity to improve the maintainability of the source
code, as well as a more expressive internal architecture or object model to
improve extensibility.
I strikt bemärkelse ska vi inte bara ägna oss åt refaktorering, då vi även kommer att ta bort vissa beteenden, metoder eller variabler som finns i det gamla ramverket. Det gamla ramverket ska kunna simuleras med hjälp av det nya, givet att man skapar lämpliga hjälpklasser. Vi ska också använda några kända designmönster.
Vi ska punkt för punkt diskutera specifika problem med det gamla ramverket och stegvis försöka åtgärda några av dem.
För att få något konkret och ganska annorlunda jämfört med Snake-spelet ska vi använda Othello/Reversi som första modell. Detta kommer mer konkret att påvisa några av begränsningarna i det befintliga ramverket. Spellogiken för Reversi ligger i klassen ReversiModel
.
Tillvägagångssätt:
Main
-klass och en ny implementation av en lämplig GameFactory
(som implementerar IGameFactory
).
Steg 1
Ladda ner IntelliJ projektet här och öppna detta med IntelliJ. Sedan kan man lägga till git versionshantering genom: ‘VCS’ -> ‘Enable version control integration…’. Lägga till
src
katalogen till git och gör en första commit. Glöm inte att göra en commit efter (minst) varje steg.Modifiera
GameFactory
så att det går att spela både Gold och Reversi. Vill ni lägga till Snake från laboration 1 får ni givetvis göra detta, men då måste ni också genomföra de förändringar som så småningom kommer att krävas även i Snake. Ett enklare alternativ är att lägga till Snake när ni är klara med hela labben. Detta är dock frivilligt.
Fördelen med abstrakta klasser i stället för interface är som bekant att man kan tillhandahålla en viss grundimplementation och sedan bara implementera de delar som varierar. En av nackdelarana (i alla fall i Java) är att man bara kan ärva implementation från en klass (som i sin tur kan ärva från en annan klass, o.s.v.). Detta medför att vi tvingas återanvända kod, även om den inte passar särskilt bra för uppgiften. I lab 2, om geometriska former, såg vi hur man kan komma ifrån denna begränsning, interfacet GeometricalForm
implementerades av en abstrakt klass vilka de konkreta klasserna Line
, Circle
, etc. ärvde från. Detta gör det möjligt att välja hur vår implementation ska se ut; endast om vi har nytta av implementationen som tillhandahålles av den abstrakta klassen använder vi klassen.
Studera klassen ReversiModel
. Denna klass tvingas att använda det befintliga spelbrädet på ett mycket konstlat sätt. Mer specifikt är inte ett fält av fält av GameTile
något bra sätt att hålla reda på tillståndet på brädet när vi skall utföra beräkningar eller förändringar. Det fungerar däremot bra för presentation av spelet.
Betydligt bättre är att istället bara använda instansvariabeln ReversiModel.board
. Vad ett anrop tillgetGameBoardState
resulterar i skall kunna härledas från instansvariablerna i ReversiModel
som representerar:
board
)
cursorPos
)
turn
)
Ingen annan lagring av brädet får ske än den som redan finns i dessa tre variabler. Detta inkluderar eventuella variabler i superklassen (GameModel
). Med nuvarande utseende på ReversiModel
uppdateras både board och det ärvda spelbrädet (från GameModel
). Detta är en dålig design; all logik som rör brädet måste dubbleras. När vi duplicerar data blir det svårare att hålla dem i fas.
I första hand bör man försöka härleda/beräkna värden från befintlig information snarare än att lagra dem. Se gärna riktlinje på sid. 91 i [Skr09]. Undantag från denna princip kan göras om klassen är icke-muterbar och beräkningarna utgör ett mätbart prestandaproblem.
Steg 2
Bygg om ramverket så att
GameModel
blir ett interface. Den (mycket lilla) funktionalitet den befintliga klassenGameModel
tillhandahåller bör man lägga över i en hjälpklass, lämpligt namn kan varaGameUtils
. KlassenGameUtils
får inte ha något tillstånd (instans-/klassvariabler). Metoderna ska direkt manipulera objekt av typenGameTile[][]
.Om
GameModel
ska vara ett interface, ska då någon av de metoder som har synlighetprotected
finnas med? Interfacet bör endast ha 3 (eller möjligen 4) metoder deklarerade.
Steg 3
Klassen
GameTile
lider av samma problem somGameModel
gör. Bygg om klassenGameTile
så att den istället blir ett interface. Här är det befogat att skapa en ny klass som har samma beteende som konkreta instanser avGameTile
har. Kalla denna nya klassBlankTile
(den ska givetvis också vara enGameTile
).
När detta steg är klar bör/skall ni diskutera vad ni gjort och hur ni gjort det med en handledare.
Då vi prövar att implementera något annat än Snake med ramverket blir några begränsningar ganska tydliga:
Reversi
-fallet hade det varit trevligt att kunna se vems tur det är och vad poängställningen är.
CompositeTile
.
Vi ska i tur och ordning se vilka förändringar och strukturförbättringar vi kan genomföra för att lösa problemen ovan.
Flera av kopplingarna mellan modell, vy och kontroller sätts upp då de olika objekten instantieras. Här finns ett utmärkt tillfälle att bygga om så att vi får lösare kopplingar. Det arkitekturella mönstret Model-View-Controller (MVC) kan tillämpas.
Dessa tre abstrakta entiteter (varje del av M, V och C kan bestå av fler än en klass) har en ansvarsfördelning enligt följande:
MVC implementeras vanligen genom någon av dessa varianter:
Passiv MVC Modellen ansvarar för att rätt information skickas till vyn. Detta kan också kallas för strikt “push”, eftersom modellen “knuffar” ut data till vyn. Här är alltså vyn mer passiv, den agerar på de data den får men frågar inte nödvändigtvis efter mer. Anropskedjan är då typiskt C -> M -> V.
Aktiv MVC Kontrollern ansvarar för att vyn får information om att något uppdaterats, vyn frågar därefter modellen efter den information som behövs. Vyn intar här en mer aktiv roll - den avgör själv vilka data som behövs för att presentationen ska förändras. Anropskedjan blir typiskt C -> M, C -> V -> M.
Aktiv MVC är i grova drag den struktur ramverket har från början:
GameController
känner till (och anropar) GameModel
och GameView
GameView
känner till och anropar GameModel
GameModel
känner inte till GameController
och GameView
, men kan svara på anrop från dem genom sina publika metoder
Flera av defekterna i listan på sida 3 ska vi börja komma till rätta med genom att mer strikt tillämpa passiv MVC.
En central idé i MVC-mönstret är att modellen ska ha lösa kopplingar till sina vyer. Detta verkar gå dåligt ihop med passiv MVC, men här är tricket att modellen bara känner till lyssnare eller observatörer. Dessa är en abstraktion, modellen har inte dessa förutbestämda utan det är upp till varje observatör att tala om för modellen att de vill lyssna efter förändringar.
Vi skall börja med att göra GameModel
observerbar. Vi kan dock inte nyttja java.util.Observable
för detta. Skälet är att Javas standardklasser för designmönstret Observer
inte är så genomtänkta som man skulle önska. java.util.Observable
är en klass och inte ett interface. En konsekvens av detta blir att man inte kan vara observerbar med mindre än att man ärver implementationen! Och GameModel
är ett interface!
Steg 4
Skapa ett nytt interface,
IObservable
. Detta bör endast ha två metoddeklarationer:
addObserver(PropertyChangeListener observer)
removeObserver(PropertyChangeListener observer)
Gör
GameModel
observerbar genom att utöka interfacetIObservable
.Alla klasser som är spelmodeller måste implementera metoderna i interfacet
IObservable
. Detta görs lättast genom att respektive modellklass delegera till en instans avPropertyChangeSupport
.Tänk på att lämpliga metoder i erat
PropertyChangeSupport
-objekt måste anropas när vi vill göra observatörerna uppmärksamma på att något inträffat.
Steg 5
När
GameModel
blivit observerbar bör vi pröva och se att uppdateringar till vyer inträffar när de ska.Ordna så att
GameView
implementerarPropertyChangeListener
. VarjeGameView
-objekt ska lyssna på sin egen respektive modell. När något händer (från modellen) räcker det med att anroparepaint
iGameView
.
Nu återstår att låta kontrollern släppa taget om vyn så att alla uppdateringar av vyerna endast sker genom att modellen anropar sina lyssnare.
Många av klasserna i Swing
-paketet har något dubbla roller. Dels kan de presentera information grafiskt, men de kan också lyssna efter användarhändelser. GameView
ärver JComponent
. Det är denna JComponent
som skall lyssnas på för att kontrollern skall kunna skicka händelserna vidare.
Det enda GameController
skall göra med vyn är att lägga till sin tangentbordslyssnare. I övrigt skall den lämna vyn i oförändrad.
Steg 6
Låt uppdateringarna till vyn gå genom modellen, utan att kontrollern direkt anropar vyn.
Nu har vi ett utmärkt tillfälle att också göra olika uppdateringsintervall möjliga. Om vi lägger till metoden
getUpdateSpeed()
till modellen kan den själv avgöra hur ofta eventuella tidsstyrda händelser skall ske från kontrollern. Givet att vi betraktar hastigheten som en modellegenskap (rimligt då den kan hänga ihop med spellogiken) är det en klar fördel att kontrollern inte bestämmer över detta.Här bör vi också i kontrollern möjliggöra direkta tangentbordstryckningar. Detta införs lättast genom att
enqueueKeyPress
anpassas så att den skickar tryckningarna direkt till modellen omgetUpdateSpeed()
antar särskilda värden (t.ex.getUpdateSpeed() < 0
).Glöm inte att pröva att det fortfarande går att byta eller starta om spel, även om man har en speltyp utan uppdateringsintervall.
Som vi sett tidigare har vi inte kunnat välja hur vi vill presentera eventuella poäng från spelen; den enda möjligheten har varit att låta modellen direkt sköta det. När vi har möjlighet att observera modellen blir detta mindre komplicerat.
Steg 7
Lägg till en lyssnare av typen
ReversiScoreView
iGameFactory
när enReversi
-modell skapas. För en specifik lyssnare är det tillåtet att gå på klasspecifika egenskaper. Alltså kanReversiScoreView
anropagetBlackScore()
ochgetWhiteScore()
. Det är efter lämpligt test
(evt.getSource().getClass() == ReversiModel.class)
tillåtet att göra antagandet att den som genererat uppdateringen är av typen
ReversiModel
.
ReversiScoreView
kan ha en mycket enkelpropertyChange
-metod, låt den bara skriva ut svarts och vits poäng samt vems tur det är att spela.
Nu har vi löst problem 1, 3 och 4 från vår lista! Bortsett från att Reversi-spelet nu inte köar massa tangentbords- tryckningar ser det ungefär likadant ut. Om vi däremot skulle välja att implementera ett nytt spel, t.ex. Tetris, kommer vi troligen att upptäcka att det blir betydligt enklare. För just Tetris behöver vi minst två funktioner som det ursprungliga ramverket inte hade: möjlighet att ändra fördröjningen mellan händelser samt möjlighet att visa nästa kloss och/eller poängställning på ett snyggt sätt.
Frivillig uppgift
Det finns givetvis både fler funktionsmässiga och strukturella defekter, både i ramverket och våra modeller.
I mån av tid, identifiera så många problem som möjligt (förutom dem vi redan nämnt) och diskutera med en handledare.
Åtgärda gärna så många brister som möjligt men försäkra er om att ni har en version av koden som bara innehåller lösning fram till uppgift 7.
Den färdiga koden redovisas muntligt.
[Skr09] Dale Skrien. Object-Oriented Design using Java. McGraw-Hill, international edition, 2009.
(Original lab av Pelle Evensen (2011) & Christer Carlsson (2015) & Niklas Brober (2016))