ConsoleIO.cs
/*
Nu kommer all in- och utmatning att hanteras via IIO-gränssnittet och du kan enkelt byta ut ConsoleIO mot en annan
implementering av IIO om du behöver anpassa in- och utmatningen till en annan plattform eller användargränssnitt.
*/
GoalGenerator.cs
//Använt Linq
GoalGeneratorFactory
//
GuessChecker
//rad 5: Anledningen till att vi ändrar från int till string är att om man skriver 0018 som gissning så kommer C# lagra
det som 18 i en int. Då blir det svårare att jämföra hundratal och tusental med goal-siffran.
GuessCheckerFactory
//
MainGame
//
PlayerData
//Ändrat Name till Player för att kunna skapa många olika slags spelare, såsom Monster, Warrior eller Wizard osv.
PlayerDataStorage
////In the provided code, the Instance property of the PlayerDataStorage class is written with a capital 'I' to follow the
naming convention commonly used for singleton instances in C#.
//By convention, when creating a singleton instance, it is common to use the name "Instance" with a capital 'I' as a property name.
This convention helps to distinguish the singleton instance from other properties or variables within the class.
//The use of a singleton pattern allows for the creation of only one instance of the class and provides a global point of access to
that instance. In this case, the Instance property ensures that only one instance of the PlayerDataStorage class is created and
returned when accessed.
//Note that the use of a singleton pattern and the specific naming convention for the instance property are not enforced by
the C# language itself but rather a convention followed by developers for clarity and consistency.
//In the provided code, the Instance property is used to implement the Singleton design pattern. The purpose of the Singleton pattern
is to ensure that only one instance of a class is created and provide a global point of access to that instance.
//By using the Instance property, the PlayerDataStorage class guarantees that there is only a single instance of the class throughout
the application.When accessed for the first time, the Instance property checks if the instance variable is null and creates a new
instance of PlayerDataStorage if it is.Subsequent calls to Instance will return the already created instance.
//The difference between instance with a lowercase 'i' and Instance with an uppercase 'I' lies in their usage and scope. The instance
variable is a private static field within the PlayerDataStorage class. It holds the reference to the single instance of PlayerDataStorage
that is created. It is only accessed and modified within the PlayerDataStorage class.
//On the other hand, the Instance property is a public static property that provides the global access point to the single instance of
PlayerDataStorage. It can be accessed from other classes in the application to obtain the singleton instance.
//The capitalization of 'I' in Instance is a common convention used to indicate that it is a public property representing the singleton
instance. It helps to differentiate it from the private instance variable and other properties or variables in the class.
_______________
Singleton.cs
//One of the commonly used design patterns is the Singleton pattern, which ensures that only one instance of a class can be created.
//In the given program, the PlayerDataStorage class could benefit from using the Singleton pattern.Currently, each
//instance of the PlayerDataStorage class operates on the same file, but each instance opens and closes the file individually,
//which could be inefficient.
//In the modified code, the PlayerDataStorage class now has a private constructor, a private static instance field, and a public static
Instance property that provides access to the singleton instance. The instance is lazily initialized, and a lock is used to ensure
thread safety.
//Now, instead of creating an instance of PlayerDataStorage using new PlayerDataStorage(resultFilePath), you can access the singleton
instance by calling PlayerDataStorage.Instance. For example:
//IPlayerDataStorage storage = PlayerDataStorage.Instance;
//By using the Singleton pattern, you ensure that all parts of the program access the same instance of PlayerDataStorage and avoid
unnecessary file opening and closing operations.
____________________________________________________________________________________________
Sammanfattning:
För det första så har vi delat in denna långa kod i olika interfaces och klasser enligt OOP.
En klass ska sköta EN sak och göra det bra. En metod ska sköta EN sak och göra det bra. Genom
detta får programmet en bättre struktur och läsbarhet :)
Vi har kikat lite på namngivningen och bytt namn till lite mer läsbara namn.
Exempel: checkBC är numera GuessChecker (en klass).
makeGoal heter GoalGenerator (en klass).
Name är bytt till Player i PlayerData för att det är mer skalbart om man vill utveckla
spelet i framtiden. T ex för att kunna skapa många olika slags spelare, såsom Monster,
Warrior eller Wizard osv. Den blir mer värdeladdad, alltså man kan ge en Player
fler egenskaper såsom ett namn, yrke, ras med mera.
I PlayerDataStorage ändrade vi också ShowTopList till GetPlayerResult som i våra öron lät mer
förståelig.
Vi har valt att använda Factory Method-pattern i klasserna för GoalGenerator och GuessChecker, för att
kapsla in interfaces så att ingen utifrån kan påverka koden. Utöver det är det också enklare att utveckla
spelet och anpassa det utefter nya förutsättningar.
Detta mönster är särskilt användbart när man har ett system med många objekt som delar vissa gemensamma
egenskaper, men som också kan variera i hur de skapas eller konfigureras. Genom att använda Factory Method
kan man definiera ett gemensamt gränssnitt (interface) för objektskapande och låta olika fabriker
implementera detta gränssnitt på olika sätt för att skapa olika typer av objekt. Metoden är användbar
när man har ett större system med gemensamma egenskaper.
I klassen PlayerDataStorage arbetade varje instans av klassen PlayerDataStorage med samma fil, men varje
instans öppnade och stängde filen individuellt, vilket kunde vara ineffektivt. Singleton-pattern säkerställer
att endast en instans av en klass kan skapas när den faktiskt behövs, istället för att den startas direkt när
programmet startas eller när klassen laddas.
Genom att använda Singleton-pattern säkerställer vi att alla delar av programmet har åtkomst till samma instans
av PlayerDataStorage och undviker onödiga öppnings- och stängningsoperationer av filer. Med hjälp av
"lock"-uttrycket förhindrar vi att flera anrop får tillgång till och möjlighet att ställa till det i spelet.
_________________________________________________________________________________________________________
Från Chatrine om PlayerData:
Förändringar och förklaringar:
Namn på variabler: Jag ändrade namnet på variabeln totalGuess till totalGuesses för att följa en mer
konsekvent namngivningskonvention.
Använd is-operatorn: I metoden Equals, använde jag is-operatorn för att kontrollera om den inkommande
objektet är en instans av PlayerData. Detta är en säkrare metod än att försöka kasta objektet.
Samma radkontroll: Jag använde == för att jämföra strängar i Equals-metoden istället för Equals-metoden.
Båda metoderna är ekvivalenta, men == är mer vanligt förekommande när det gäller strängjämförelser.
Läsbarhetsförbättringar: Jag har också justerat indrag och formatering för att göra koden mer läsbar och
följa vanliga kodningskonventioner.
Getter för Player-egenskap: Eftersom du inte ändrar Player-egenskapen efter dess instansiering, kan du
ha en privat set-del eller helt ta bort den för att göra egenskapen skrivskyddad från andra klasser.
________________________________________________________________________________________________________
From Chatrine om DI:
In the provided code, it looks like you're already using dependency injection to some extent by passing
various dependencies (such as factories, storage, and I/O) through the constructor of the MainGame class.
This is a good practice that promotes loose coupling between components and makes the code more testable
and maintainable. MainGame class constructor is already taking in dependencies as parameters.
public MainGame(IGoalGeneratorFactory generatorFactory, IGuessCheckerFactory checkerFactory,
IPlayerDataStorage storage, IIO io)
{
// ...
}
These dependencies are then stored as private readonly fields within the MainGame class and used throughout
the class's methods.
Tester
PlayerDataTests:
PlayerData_Initialization:
I det tillhandahållna testfallet är fokus för testmetoden PlayerData_Initialization att testa initialiseringsbeteendet för klassen PlayerData.
I det här fallet antas det att en spelare med namnet "Fatima" spelar spelet endast en gång (därav initialiseras NGames till 1)
och gör 5 gissningar (vilket är värdet som skickas till konstruktorn som initialGuesses). Detta scenario används för att säkerställa
att när en instans av PlayerData skapas så initialiseras dess egenskaper (Player, NGames, och totalGuesses) korrekt och matchar förväntade
värden under detta specifika initialiseringsfall.
Inuti testmetoden:
Arrange: De inledande villkoren för testet etableras. En string-variabel playerName tilldelas värdet "Fatima", och en int-variabel initialGuesses tilldelas värdet 5.
Act: Handlingen som testas utförs. En instans av PlayerData-klassen skapas med de angivna värdena för playerName och initialGuesses.
Assert: Den faktiska beteendet hos koden kontrolleras mot det förväntade beteendet. Tre påståenden används för att kontrollera:
Om Player-egenskapen för instansen playerData är lika med playerName.
Om NGames-egenskapen för instansen playerData är lika med 1.
Om resultatet av metoden GetTotalGuesses() för instansen playerData är lika med initialGuesses.
PlayerData_Update:
I det angivna testfallet (PlayerData_Update), inleds PlayerData-instansen med en spelare vid namn "Behzad" och ursprungligen 3 gissningar.
Sedan utförs en uppdatering av spelardata genom att lägga till ytterligare 2 gissningar. Detta resulterar i totalt 5 gissningar under en omgång
av spelet för spelaren "Behzad". Så det är en omgång av spelet med totalt fem gissningar.
I detta testfall, PlayerData_Update, fokuseras testmetoden på att testa uppdateringsbeteendet för klassen PlayerData.
Arrange: Inledningsvis etableras de inledande villkoren för testet. En instans av PlayerData skapas med spelarens namn "Behzad" och ursprungliga gissningar 3.
Dessutom anges ett ytterligare antal gissningar, som är 2.
Act: Handlingen som testas utförs. Metoden Update anropas på playerData-instansen med det ytterligare antalet gissningar.
Assert: Därefter görs verifieringar av det faktiska beteendet hos koden mot det förväntade beteendet. Två påståenden används för att kontrollera:
Om det totala antalet gissningar, inklusive de ursprungliga och de ytterligare gissningarna, är lika med summan av ursprungliga gissningar och ytterligare gissningar.
Om värdet för NGames är lika med 2, vilket indikerar att spelaren nu har spelat två gånger.
PlayerData_Average:
I den första omgången har Vidar ursprungligen 4 gissningar och sedan uppdaterar vi spelardatan med ytterligare 3 gissningar. Detta ger totalt 7 gissningar under den första omgången.
Efter dessa två omgångar beräknas genomsnittet av antalet gissningar genom att dela det totala antalet gissningar (7) med antalet spelomgångar (2).
Detta ger 3,5 som är det förväntade genomsnittet.
I detta testfall, PlayerData_Average, är fokuset på att testa beräkningen av genomsnittligt antal gissningar per spelomgång för klassen PlayerData.
Arrange: I början av testet sätts inledande villkor upp. En instans av PlayerData skapas med spelarens namn "Vidar" och ursprungliga 4 gissningar.
Därefter utförs en uppdatering av spelardata genom att lägga till ytterligare 3 gissningar. Detta resulterar i totalt 7 gissningar över två spelomgångar för spelaren "Vidar".
Act: Handlingen som testas utförs. Metoden Average anropas på playerData-instansen för att beräkna det genomsnittliga antalet gissningar.
Assert: Därefter görs en verifiering av det faktiska genomsnittliga antalet gissningar mot det förväntade genomsnittet. Eftersom det totala antalet gissningar är 7
och antalet spelomgångar är 2, förväntas genomsnittet vara 7 delat med 2, vilket är 3,5.
::(Factory Method är ett designmönster inom programvaruutveckling som används för att skapa objekt på ett flexibelt och hanterbart sätt.
En av huvudorsakerna till att man använder Factory Method är för att separera objektskapandet från dess faktiska användning.
Istället för att direkt skapa objekt med hjälp av en konstruktor, använder man sig av en särskild "fabrik" (Factory) för att producera
dessa objekt. Detta ger flera fördelar. För det första kan man enkelt ändra implementationen av objektskapandet i framtiden utan att
påverka koden som använder objekten. Det gör det också möjligt att hantera olika varianter eller typer av objekt på ett strukturerat
sätt.
Sammanfattningsvis, Factory Method möjliggör en ren och strukturerad kodbas genom att isolera objektskapandet och tillåter enkel
anpassning och utökning av objektskapandet i framtiden. Det hjälper också till att hantera komplexiteten när det gäller att skapa och
använda olika objekttyper.)