Gå til indholdet

Den objektorienterede terning

Her er et komplet eksempel der viser, hvordan man kan implementere et objektorienteret terningesystem, der demonstrerer centrale begreber inden for objektorienteret programmering (OOP).

Information til undervisere

Dette eksempel kan bruges overordnet til at demonstrere vigtige OOP-koncepter som abstraktion, indkapsling, arv og polymorfi i praksis. Det er velegnet til summering af grundlæggende OOP-principper i C#.

De vigtigste OOP-begreber, vi dækker her, inkluderer:

  • Abstraktion: Der oprettes en abstrakt klasse Terning, der fungerer som en skabelon for specifikke terningetyper. Den abstrakte klasse indeholder fælles funktionalitet, som alle terninger deler, såsom at rulle terningen.

  • Indkapsling: Hver terning har kontrol over sin egen tilstand (værdi) ved hjælp af egenskaber som Værdi, der er beskyttet mod direkte manipulation udefra.

  • Arv: Der oprettes flere specifikke terningetyper som LudoTerning, YatzyTerning, og PakkelegTerning, som arver fra Terning. Dette betyder, at de nedarver de fælles egenskaber og metoder fra Terning, men kan også udvide eller tilpasse dem.

  • Polymorfi: Polymorfi betyder, at man kan bruge en fælles Terning-reference til at pege på forskellige typer terninger og kalde metoder, der opfører sig forskelligt baseret på den faktiske type af terning, som reference peger på. Se også, hvordan en liste (bæger) kan indeholde forskellige typer terninger.

Klassediagram

Her er klassediagrammet for eksemplet:

classDiagram
    class Terning {
        <<abstract>>
        int Værdi
        +void Ryst()
        +string ToString()
    }

    Terning <|-- LudoTerning
    Terning <|-- YatzyTerning
    Terning <|-- PakkelegTerning
    Terning <|-- ManipuleretTerning
    YatzyTerning <|-- YatzyTerningRandomOrg

    class LudoTerning {
        +bool ErGlobus()
        +bool ErStjerne()
        +string ToString()  
    }

    class YatzyTerning {        
    }

    class PakkelegTerning {
        +bool ErSekser()        
    }

    class ManipuleretTerning {
        -int manipuleretVærdi
        -double vægt
        +void Ryst()        
    }

    class YatzyTerningRandomOrg {
        -HttpClient client
        +void Ryst()        
    }

Terninger

/// <summary>
/// Abstrakt klasse Terning, som alle terningetyper skal nedarve fra.
/// Abstrakt betyder, at denne klasse ikke kan instantieres direkte.
/// Den indeholder fælles funktionalitet, som kan bruges af alle specifikke terningklasser.
/// </summary>
public abstract class Terning
{
    /// <summary>
    /// Repræsenterer terningens aktuelle værdi efter et kast.
    /// Kan kun sættes af denne klasse eller afledte klasser.
    /// </summary>
    public int Værdi { get; protected set; }

    /// <summary>
    /// Konstruktor for Terning, der automatisk ryster terningen (giver en tilfældig værdi) ved oprettelse.
    /// </summary>
    public Terning()
    {
        this.Ryst(); // Kalder Ryst() for at give terningen en tilfældig værdi ved oprettelsen.
    }

    /// <summary>
    /// Virtual metode til at ryste terningen (generere en tilfældig værdi).
    /// Afledte klasser kan overskrive denne metode for at tilpasse funktionaliteten.
    /// </summary>
    public virtual void Ryst()
    {
        // Giver terningen en tilfældig værdi mellem 1 og 6.
        this.Værdi = Random.Shared.Next(1, 7);
    }

    /// <summary>
    /// Returnerer en tekstbaseret repræsentation af terningens værdi.
    /// </summary>
    /// <returns></returns>
    public override string ToString()
    {
        return $"[{this.Værdi}]"; // Returnerer værdien som en almindelig terning.
    }

}

/// <summary>
/// LudoTerning nedarver fra Terning og tilføjer specifikke Ludo-funktioner såsom Globus og Stjerne.
/// </summary>
public class LudoTerning : Terning
{
    /// <summary>
    /// Returnerer true, hvis terningens værdi er 3 (Globus i Ludo).
    /// </summary>
    /// <returns>True hvis terningens værdi er 3, ellers false.</returns>
    public bool ErGlobus()
    {
        return this.Værdi == 3;
    }

    /// <summary>
    /// Returnerer true, hvis terningens værdi er 5 (Stjerne i Ludo).
    /// </summary>
    /// <returns>True hvis terningens værdi er 5, ellers false.</returns>
    public bool ErStjerne()
    {
        return this.Værdi == 5;
    }

    /// <summary>
    /// Konstruktor for LudoTerning, der kalder basisklassens konstruktor.
    /// </summary>
    public LudoTerning() : base() { }

    /// <summary>
    /// Returnerer en tekstbaseret repræsentation af LudoTerningens værdi, med specielle symboler
    /// for Globus (G) og Stjerne (S).
    /// </summary>
    /// <returns>En streng der repræsenterer terningens værdi.</returns>
    public override string ToString()
    {
        if (this.ErGlobus())
            return "[G]"; // Returnerer "[G]" hvis det er en Globus.
        if (this.ErStjerne())
            return "[S]"; // Returnerer "[S]" hvis det er en Stjerne.
        return $"[{this.Værdi}]"; // Returnerer værdien som en almindelig terning.
    }
}

/// <summary>
/// YatzyTerning nedarver fra Terning og repræsenterer en standard Yatzy-terning.
/// </summary>
public class YatzyTerning : Terning
{
    /// <summary>
    /// Konstruktor for YatzyTerning, der kalder basisklassens konstruktor.
    /// </summary>
    public YatzyTerning() : base() { }

}

/// <summary>
/// PakkelegTerning nedarver fra Terning og har en speciel regel om, at
/// hvis værdien er 6, vises et specielt symbol.
/// </summary>
class PakkelegTerning : Terning
{
    /// <summary>
    /// Konstruktor for PakkelegTerning, der kalder basisklassens konstruktor.
    /// </summary>
    public PakkelegTerning() : base() { }

    /// <summary>
    /// Returnerer true, hvis terningens værdi er 6.
    /// </summary>
    /// <returns>True hvis terningens værdi er 6, ellers false.</returns>
    public bool ErSekser()
    {
        return this.Værdi == 6;
    }

}

/// <summary>
/// ManipuleretTerning er en specialiseret terning, der er manipuleret til at favorisere en bestemt værdi.
/// </summary>
class ManipuleretTerning : Terning
{
    /// <summary>
    /// Den værdi, som terningen er manipuleret til at favorisere.
    /// </summary>
    private readonly int manipuleretVærdi;

    /// <summary>
    /// Sandsynligheden for at rulle den manipulerede værdi (mellem 0 og 1).
    /// </summary>
    private readonly double vægt;

    /// <summary>
    /// Konstruktor for ManipuleretTerning, der initialiserer den manipulerede værdi og vægten.
    /// </summary>
    /// <param name="manipuleretVærdi">Den værdi, terningen er manipuleret til at favorisere.</param>
    /// <param name="vægt">Sandsynligheden for at rulle den manipulerede værdi (mellem 0 og 1).</param>
    public ManipuleretTerning(int manipuleretVærdi, double vægt) : base()
    {
        if (manipuleretVærdi < 1 || manipuleretVærdi > 6)
            throw new ArgumentException("Værdi skal være mellem 1 og 6");

        if (vægt < 0 || vægt > 1)
            throw new ArgumentException("Vægt skal være mellem 0 og 1");

        this.manipuleretVærdi = manipuleretVærdi;
        this.vægt = vægt;
    }

    /// <summary>
    /// Overskrivning af Ryst-metoden for at implementere manipuleret terning adfærd.
    /// Tjekker om den manipulerede værdi skal vælges baseret på sandsynligheden (vægt).
    /// </summary>
    public override void Ryst()
    {
        // Tjekker om et tilfældigt tal mellem 0 og 1 er mindre end vægten, og hvis ja, rulles den manipulerede værdi.
        this.Værdi = new Random().NextDouble() < vægt ? manipuleretVærdi : Random.Shared.Next(1, 7);
    }

}

/// <summary>
/// YatzyTerningRandomOrg klassen arver fra YatzyTerning og bruger Random.org
/// til at hente et tilfældigt tal mellem 1 og 6.
/// Denne implementering af Ryst er synkron, hvilket teknisk set er forkert for netværkskald.
/// </summary>
public class YatzyTerningRandomOrg : YatzyTerning
{
    /// <summary>
    /// HttpClient bruges til at sende HTTP-forespørgsler til Random.org.
    /// </summary>
    private static readonly HttpClient client = new HttpClient();

    /// <summary>
    /// Konstruktor for YatzyTerningRandomOrg, der kalder basisklassens konstruktor.
    /// </summary>
    public YatzyTerningRandomOrg() : base() { }

    /// <summary>
    /// Overskrivning af Ryst-metoden, som bruger Random.org til at hente et tilfældigt tal mellem 1 og 6.
    /// Denne metode er synkron, hvilket betyder, at den blokerer programmet, mens den venter på svar fra serveren.
    /// Dette er normalt ikke anbefalet for netværkskald, som bør være asynkrone.
    /// </summary>
    public override void Ryst()
    {
        try
        {
            // Random.org URL, der returnerer et tilfældigt tal mellem 1 og 6 i tekstformat.
            string url = "https://www.random.org/integers/?num=1&min=1&max=6&col=1&base=10&format=plain&rnd=new";

            // Sender en synkron GET-forespørgsel til Random.org for at hente et tilfældigt tal.
            HttpResponseMessage response = client.GetAsync(url).Result;

            // Sikrer, at svaret er succesfuldt (statuskode 200).
            response.EnsureSuccessStatusCode();

            // Læser det tilfældige tal som en streng fra responsen.
            string responseBody = response.Content.ReadAsStringAsync().Result;

            // Konverterer den hentede værdi fra streng til et heltal (int) og sætter Værdi.
            this.Værdi = int.Parse(responseBody.Trim());
        }
        catch (HttpRequestException e)
        {
            // Håndterer eventuelle fejl, f.eks. netværksproblemer, ved at falde tilbage til lokal Random.
            Console.WriteLine($"Request error: {e.Message}");
            this.Værdi = new Random().Next(1, 7); // Fallback til standard tilfældighedsgenerator.
        }
    }

}

Test

// Test af LudoTerning
Console.WriteLine("LudoTerning:");
LudoTerning ludoTerning = new LudoTerning();
ludoTerning.Ryst();
Console.WriteLine($"Værdi: {ludoTerning}"); // Bruger ToString() til at vise symbol eller værdi.
Console.WriteLine($"Er Globus: {ludoTerning.ErGlobus()}");
Console.WriteLine($"Er Stjerne: {ludoTerning.ErStjerne()}");
Console.WriteLine();

// Test af YatzyTerning
Console.WriteLine("YatzyTerning:");
YatzyTerning yatzyTerning = new YatzyTerning();
yatzyTerning.Ryst();
Console.WriteLine($"Værdi: {yatzyTerning}");
Console.WriteLine();

// Test af PakkelegTerning
Console.WriteLine("PakkelegTerning:");
PakkelegTerning pakkelegTerning = new PakkelegTerning();
pakkelegTerning.Ryst();
Console.WriteLine($"Værdi: {pakkelegTerning}");
Console.WriteLine($"Er Sekser: {pakkelegTerning.ErSekser()}");
Console.WriteLine();

// Test af ManipuleretTerning
Console.WriteLine("ManipuleretTerning (favoriserer 6 med 70% sandsynlighed):");
ManipuleretTerning manipuleretTerning = new ManipuleretTerning(6, 0.7);
manipuleretTerning.Ryst();
Console.WriteLine($"Værdi: {manipuleretTerning}");
Console.WriteLine();

// Test af YatzyTerningRandomOrg (henter værdi fra Random.org)
// Bemærk, at dette er synkront og vil tage lidt tid på grund af netværkskald.
Console.WriteLine("YatzyTerningRandomOrg (henter værdi fra Random.org):");
YatzyTerningRandomOrg yatzyTerningRandomOrg = new YatzyTerningRandomOrg();
yatzyTerningRandomOrg.Ryst(); // Synkron kald
Console.WriteLine($"Værdi: {yatzyTerningRandomOrg}");
Console.WriteLine();

// Demonstrerer polymorfi med terninger
Console.WriteLine("Polymorfi: Terning kan pege på sine afledte klasser");

// Opretter en reference af typen Terning, som peger på forskellige afledte klasser
Terning terning1 = new LudoTerning();
Terning terning2 = new YatzyTerning();
Terning terning3 = new PakkelegTerning();
Terning terning4 = new ManipuleretTerning(6, 0.7);

// Ryster og viser værdien for hver terning
terning1.Ryst();
terning2.Ryst();
terning3.Ryst();
terning4.Ryst();

Console.WriteLine($"LudoTerning: {terning1}"); // Bruger polymorfi, da terning1 er en LudoTerning
Console.WriteLine($"YatzyTerning: {terning2}");
Console.WriteLine($"PakkelegTerning: {terning3}");
Console.WriteLine($"ManipuleretTerning: {terning4}");
Console.WriteLine();

// Demonstrerer polymorfi i en liste af forskellige typer terninger (bæger)
Console.WriteLine("Polymorfi: Liste af forskellige typer terninger (bæger)");

// Opretter en liste med forskellige typer terninger (bæger)
List<Terning> terningBæger = new List<Terning>
{
    new LudoTerning(),
    new YatzyTerning(),
    new PakkelegTerning(),
    new ManipuleretTerning(6, 0.7),
    new YatzyTerningRandomOrg()
};

// Ryster og viser værdien for hver terning i bægeret
foreach (Terning terning in terningBæger)
{
    terning.Ryst(); // Polymorfisk kald, da Ryst er overskrevet i de afledte klasser
    Console.WriteLine($"{terning.GetType().Name}: {terning}"); // Viser typen og værdien af hver terning
}
Console.WriteLine("Polymorfi-demonstration er færdig.");