Gå til indholdet

En OOP terning

Denne side viser en Terning i mange forskellige udformninger. Brug siden som opsummering.

Struct eller klasse

Hvis du skal skabe en type der repræsenterer en terning skal du tage stilling til om du vil benytte en struktur eller en klasse. Den umiddelbare forskel er om variabler indeholder værdier (stuktur) eller referencer (klasse). Hvis der er tale om en struktur er det nemt at kopiere og sammenligne instanser, og det er nemt for runtime at rydde op. Hvis der er tale om en klasse er det nemt at sende/gemme referencer til terninger rundt i applikationen, men til gengæld koster det lidt at allokere og rydde op. Det store argument for at benytte en klasse er dog muligheden for nedarvning, og med tanke på at en terning kan benyttes i mange forskellige versioner (meyer, yatzy, ludo mv) bør en terning repræsenteres af en klasse.

public class Terning 
{

}

Klassen er public for at sikre, at den kan benyttes i andre projekter.

Felter

Felter (fields) er typens (her klassens) interne data, og en terning har som minimum en værdi mellem 1 og 6. Du kan vælge at benytte en byte (8 bit) for at det fylder så lidt som muligt, men i virkeligheden er det nok ligegyldigt så du kan lige så godt benytte en int (32 bit):

public class Terning
{
    public int Værdi;
}

Her har klassen fået tilføjet et offentligt felt, så nu kan der tildeles en værdi:

Terning t = new Terning();
t.Værdi = 4;

Problemet ved at benytte et offentligt felt er dog, at Værdi kan tildeles alt hvad en int kan består af:

Terning t = new Terning();
t.Værdi = -4;

Det kunne skabe en del bøvl så det duer ikke. Du bør ikke have nogle offentlige felter i dine typer!

Du kunne måske overveje at benytte en enum tli at repræsentere en værdi:

public enum TerningVærdi
{
    Et = 1, 
    To = 2, 
    Tre = 3, 
    Fire = 4, 
    Fem = 5,
    Seks = 6
}
public class Terning
{
    public TerningVærdi Værdi;
}

men det giver nok for meget bøvl med konvertering til int hele tiden. Havde det været eksempelvis et kort fra et kortspil du skulle repræsentere med en type giver det god mening. Vi holder fast i en int som type til at holde værdien af terningen.

Der kan være andre felter som måske kunne medtages

  • TidspunktForOprettelse (DateTime)
  • ForrigeVærdi (int)
  • SnydeFaktor (double)

og så videre.

Statiske felter

Du kan eventuelt tilføje statiske felter hvis du ønsker instanser som kan tilgås af alt kode (offentlige statiske felter) eller som kan tilgås af instanser af klassen (private offentlige felter). Se afsnit om metoder for et eksempel på et privat statisk felt.

Konstruktør

En konstruktør (constructor) giver mulighed for at afvikle kode når der oprettes en instans. Standard konstruktøren er uden argumenter, og de brugerdefinerede er med argumenter. I terningen kan begge måske give mening:

public class Terning
{
    public int værdi;

        // standard (default)
        public Terning()
        {
            this.værdi = 1;
        }

        // brugerdefineret (custom)
        public Terning(int startVærdi)
        {
            if (startVærdi < 1 || startVærdi > 6) {
                throw new Exception("Forkert værdi");
            }
            this.værdi = startVærdi;
        }
}

Nu kan man oprette en instans på to måder:

Terning t1 = new Terning();
Console.WriteLine(t1.værdi);        // 1
Terning t2 = new Terning(2);
Console.WriteLine(t2.værdi);        // 2

Metoder

Metoder er typens måde at afvikle instruktioner - typisk relateret til typens felter. I en terning kunne der være flere metoder, men en bør der som minimum være. Det kunne være fikst hvis terningen selv kan få en tilfældig værdi - så der bør være en Ryst() metode.

public class Terning
{
    public int Værdi;

    public void Ryst()
    {
        System.Random rnd = new Random();
        this.Værdi = rnd.Next(1, 7);
    }
}

Nu kan terningen “rystes” for at få felter Værdi tildelt en ny værdi:

Terning t = new Terning();
 t.Ryst();
Console.WriteLine(t.Værdi); // tilfældig værdi

Der kunne være mange andre metoder:

  • GemTilDisk()
  • HentFraDisk()
  • SkrivTilKonsol()
  • SnydNæsteGang()

og så videre.

Brug af Random

Rent performancemæssigt kan man godt argumentere for, at følgende Ryst() metode måske ikke er super smart:

public class Terning
{
    public int Værdi;

    public void Ryst()
    {
        // Hver gang Ryst() kaldes bliver der skabt et nyt objekt
        System.Random rnd = new Random();
        this.Værdi = rnd.Next(1, 7);
    }
}

For at undgå at der oprettes et objekt af Random hver gang Ryst() kaldes kunne instansen af Random placeres på klasseniveau som et privat felt:

public class Terning
{
    public int Værdi;
    private readonly System.Random rnd;

    public void Ryst()
    {   
        this.Værdi = this.rnd.Next(1, 7);
    }
    public Terning()
    {
        this.rnd = new Random();
    }
}

Nu oprettes en instans af Random når der skabes en instans af Terning, og ike hver gang Ryst() kaldes (readonly betyder, at rnd ikke må tildeles en værdi andre steder end i en konstruktør).

Måske kan feltet endda gøres statisk således, at der kun er et objekt som kan bruges af alle terninger:

public class Terning
{
    public int Værdi;
    private static readonly System.Random rnd;

    public void Ryst()
    {
        this.Værdi = rnd.Next(1, 7);
    }
    static Terning()
    {
        rnd = new Random();
    }
}

Bemærk den nu statiske konstruktør.

Statiske metoder

Ryst er en instans metode - den arbejder på instansens data, men du kan også vælge at tilføje statiske metoder. Det er typisk hjælpemetoder som ikke har noget med instanserne at gøre. På en terning kunne en statisk metode måske være FindTilfældigTerningVærdi():

public class Terning
{
    public int Værdi;
    public void Ryst()
    {
        System.Random rnd = new Random();
        this.Værdi = rnd.Next(1, 7);
    }

    public static int FindTilfældigTerningVærdi() {
        System.Random rnd = new Random();
        return rnd.Next(1, 7);
    }
}

Metoden kan bruges som følger:

Console.WriteLine(Terning.FindTilfældigTerningVærdi());     // tilfældig værdi

Bemærk, at metoden findes på typen og ikke på en instans.

Egenskaber

Et offentlig felt er en skidt idé fordi du ikke kan styre tildeling og aflæsning af værdier:

public class Terning
{
    public int Værdi;
}

Dette er ikke smart:

Terning t = new Terning();
t.Værdi = -4;

Men du kan vælge at indkasple data ved at gøre felterne private:

public class Terning
{
    private int værdi;
}

Bemærk, at værdi nu er stavet med lille hvilket er den generelle navngivningsstandard.

Nu kan feltet ikke tilgås udefra men ved hjælp af en offenlig egenskab skabes der adgang. I egenskaben kan du skrive den kode du ønsker:

public class Terning
{
    private int værdi;

    public int Værdi
    {
        get
        {
            // sikkehedskode
            // logkode
            return this.værdi;
        }
        set
        {
            // sikkehedskode
            // logkode
            // valideringskode
            if (this.værdi < 1 || this.værdi > 6)
                throw new Exception("Forkert værdi");
            this.værdi = value;
        }
    }
}

Microsoft anbefaler, at en egenskab har samme navn som feltet men med det første bogstav med stort.

Nu kan feltet værdi tilgås gennem egenskaben Værdi men med valideringskode:

Terning t1 = new Terning();
t1.Værdi = 4;

Terning t2 = new Terning();
t2.Værdi = -4;  // Exception

Du kan også overveje at benytte en automatisk egenskab som kun kan aflæses udefra, og så benytte en konstruktør til at angive en værdi:

public class Terning
{
    public int Værdi { get; private set; }

    public Terning()
    {
        this.Værdi = 1;
    }

    public Terning(int startVærdi)
    {
        if (startVærdi < 1 || startVærdi > 6)
        {
            throw new Exception("Forkert værdi");
        }
        this.Værdi = startVærdi;
    }
}

Nu kan terningen kun få en værdi ved oprettelse - hereter kan den kun aflæses.

Nedarvning

For Terning er en klasse kan den benyttes i et arvehieraki. Her er en generel terning og to mere specifikke terninger:

public class Terning
{
    protected static readonly System.Random rnd;

    public int Værdi
    {
        get; protected set;
    }

    public Terning()
    {
        this.Ryst();
    }

    static Terning()
    {
        rnd = new Random();
    }

    public void Ryst()
    {
        this.Værdi = rnd.Next(1, 7);
    }

    public Terning(int startVærdi)
    {
        if (startVærdi < 1 || startVærdi > 6)
        {
            throw new Exception("Forkert værdi");
        }
        this.Værdi = startVærdi;
    }
}


public class PakkelegTerning : Terning
{
    public bool MåTagePakke()
    {
        return this.Værdi == 6;
    }
}

public class LudoTerning : Terning
{
    public bool ErStjerne()
    {
        return this.Værdi == 3;
    }
    public bool ErGlobus()
    {
        return this.Værdi == 5;
    }
}

Bemærk at instansen af System.Random og egenskaben Værdi nu kan tilgås gennem et protected felt således, at objektet eventuelt kan benyttes i underklasser.

Polymorfi

Den ene gren af polymorfi (børn kan have deres egen implementation) kan også give god mening i en terning. For det første kan den virtuelle metode ToString (fra System.Object) overskrives så den giver lidt mere mening:

public class Terning
{
    protected static readonly System.Random rnd;

    public int Værdi
    {
        get; private set;
    }

    public Terning()
    {
        this.Ryst();
    }

    static Terning()
    {
        rnd = new Random();
    }

    public void Ryst()
    {
        this.Værdi = rnd.Next(1, 7);
    }

    public Terning(int startVærdi)
    {
        if (startVærdi < 1 || startVærdi > 6)
        {
            throw new Exception("Forkert værdi");
        }
        this.Værdi = startVærdi;
    }

    public override string ToString()
    {
        return $"[ {this.Værdi} ]";
    }
}


public class PakkelegTerning : Terning
{
    public bool MåTagePakke()
    {
        return this.Værdi == 6;
    }
}

public class LudoTerning : Terning
{
    public bool ErStjerne()
    {
        return this.Værdi == 3;
    }
    public bool ErGlobus()
    {
        return this.Værdi == 5;
    }

    public override string ToString()
    {
        if (this.ErStjerne())
            return "[ S ]";
        if (this.ErGlobus())
            return "[ G ]";
        return $"[ {this.Værdi} ]";
    }
}

Nu giver ToString god mening:

Terning t1 = new Terning();
Console.WriteLine(t1);

LudoTerning t2 = new LudoTerning();
Console.WriteLine(t2);

PakkelegTerning t3 = new PakkelegTerning();
Console.WriteLine(t3);

Du kan også vælge at skabe egne virtuelle metoder. Hvad med en virtuel Ryst således, at underklasser kan vælge at overskrive den:

public class Terning
{
    protected static readonly System.Random rnd;

    public int Værdi
    {
        get; protected set;
    }

    public Terning()
    {
        this.Ryst();
    }

    static Terning()
    {
        rnd = new Random();
    }

    public virtual void Ryst()
    {
        this.Værdi = rnd.Next(1, 7);
    }

    public Terning(int startVærdi)
    {
        if (startVærdi < 1 || startVærdi > 6)
        {
            throw new Exception("Forkert værdi");
        }
        this.Værdi = startVærdi;
    }

    public override string ToString()
    {
        return $"[ {this.Værdi} ]";
    }
}


public class PakkelegTerning : Terning
{
    public bool MåTagePakke()
    {
        return this.Værdi == 6;
    }

    public override void Ryst()
    {
        // Hvis det er et lige millisekund...
        if (DateTime.Now.Millisecond % 2 == 0)
            this.Værdi = 6;
        else
            base.Ryst();
    }
}

public class LudoTerning : Terning
{
    public bool ErStjerne()
    {
        return this.Værdi == 3;
    }
    public bool ErGlobus()
    {
        return this.Værdi == 5;
    }

    public override string ToString()
    {
        if (this.ErStjerne())
            return "[ S ]";
        if (this.ErGlobus())
            return "[ G ]";
        return $"[ {this.Værdi} ]";
    }
}

Nu er der en stor sansynlighed for at, pakkeleg-terningen rystes til en 6’er.

Abstrakt klasse

I et arvehieraki er den øveste klasse tit blot “mor” for alle andre, og der dermed ikke bør kunne skabes en instans af klassen. I så fald kan klassen gøres abstrakt. Det giver også mulighed for abstrakte metoder uden implementation som skal implementeres af underklasser:

public abstract class Terning
{
    protected static readonly System.Random rnd;

    public int Værdi
    {
        get; protected set;
    }

    public Terning()
    {
        this.Ryst();
    }

    static Terning()
    {
        rnd = new Random();
    }

    public abstract void Ryst();

    public Terning(int startVærdi)
    {
        if (startVærdi < 1 || startVærdi > 6)
        {
            throw new Exception("Forkert værdi");
        }
        this.Værdi = startVærdi;
    }

    public override string ToString()
    {
        return $"[ {this.Værdi} ]";
    }
}


public class PakkelegTerning : Terning
{
    public bool MåTagePakke()
    {
        return this.Værdi == 6;
    }

    public override void Ryst()
    {
        if (DateTime.Now.Millisecond % 2 == 0)
            this.Værdi = 6;
        else
            this.Værdi = rnd.Next(1, 7);
    }
}

public class LudoTerning : Terning
{
    public bool ErStjerne()
    {
        return this.Værdi == 3;
    }
    public bool ErGlobus()
    {
        return this.Værdi == 5;
    }

    public override string ToString()
    {
        if (this.ErStjerne())
            return "[ S ]";
        if (this.ErGlobus())
            return "[ G ]";
        return $"[ {this.Værdi} ]";
    }

    public override void Ryst()
    {
        using (System.Net.WebClient w = new System.Net.WebClient())
        {
            // Henter tilfældigt tal fra random.org
            string s = w.DownloadString("https://www.random.org/integers/?num=1&min=1&max=6&col=1&base=10&format=plain&rnd=new");
            this.Værdi = Convert.ToInt32(s);
        }
    }
}

Nu er underklasser tvunget til at implementere Ryst(). PakkelegTerning bruger System.Random og et ur, og LudoTerning henter tal fra random.org.

Delegates

Brugen af delegates (referencer til metoder) kan blandt andet bruges til at afkoble Ryst-metoden i terningen således, at den der benytter terningen selv skal tilføje metoden.

Det kan gøres på “den gamle måde” ved at definere en delegate der returnerer en int:

namespace Demo
{
    using System;

    class Program
    {
        static void Main(string[] args)
        {
            // Enten
            RystDelegate d1 = new RystDelegate(MinRyst);
            Terning t1 = new Terning(d1);
            Console.WriteLine(t1);

            // Eller 
            RystDelegate d2 = MinRyst;
            Terning t2 = new Terning(d2);
            Console.WriteLine(t2);

            // Eller             
            Terning t3 = new Terning(MinRyst);
            Console.WriteLine(t3);
        }

        static int MinRyst()
        {
            return new Random().Next(1, 7);
        }

    }

    public delegate int RystDelegate();

    public class Terning
    {

        private RystDelegate RystMetode;

        public int Værdi
        {
            get; private set;
        }

        public Terning(RystDelegate ryst)
        {
            if (ryst == null)
                throw new ApplicationException("Mangler Ryst");
            this.RystMetode = ryst;
            this.Ryst();
        }

        public void Ryst()
        {
            this.Værdi = this.RystMetode.Invoke();
        }

        public override string ToString()
        {
            return $"[ {this.Værdi} ]";
        }
    }
}

Brug af indbyggede delegates

I stedet for at definere en delegate kunne du benytte en Func i stedet:

namespace Demo
{
    using System;

    class Program
    {
        static void Main(string[] args)
        {
            // Enten
            Func<int> d1 = MinRyst;
            Terning t1 = new Terning(d1);
            Console.WriteLine(t1);

            // Eller 
            Func<int> d2 = MinRyst;
            Terning t2 = new Terning(d2);
            Console.WriteLine(t2);

            // Eller             
            Terning t3 = new Terning(MinRyst);
            Console.WriteLine(t3);
        }

        static int MinRyst()
        {
            return new Random().Next(1, 7);
        }

    }

    public class Terning
    {

        private Func<int> RystMetode;

        public int Værdi
        {
            get; private set;
        }

        public Terning(Func<int> ryst)
        {
            if (ryst == null)
                throw new ApplicationException("Mangler Ryst");
            this.RystMetode = ryst;
            this.Ryst();
        }

        public void Ryst()
        {
            this.Værdi = this.RystMetode.Invoke();
        }

        public override string ToString()
        {
            return $"[ {this.Værdi} ]";
        }
    }
}

Lambda

I stedet for en konkret MinRyst-metode giver det måske mening at bruge en lambda i stedet:

namespace Demo
{
    using System;

    class Program
    {
        static void Main(string[] args)
        {
            // Enten
            Func<int> d1 = () => new Random().Next(1, 7);
            Terning t1 = new Terning(d1);
            Console.WriteLine(t1);

            // Eller             
            Terning t2 = new Terning(() => new Random().Next(1, 7));
            Console.WriteLine(t2);
        }
    }

    public class Terning
    {

        private Func<int> RystMetode;

        public int Værdi
        {
            get; private set;
        }

        public Terning(Func<int> ryst)
        {
            if (ryst == null)
                throw new ApplicationException("Mangler Ryst");
            this.RystMetode = ryst;
            this.Ryst();
        }

        public void Ryst()
        {
            this.Værdi = this.RystMetode.Invoke();
        }

        public override string ToString()
        {
            return $"[ {this.Værdi} ]";
        }
    }
}

Hændelser

Med hændelser kan du skabe en terning som selv afvikler kode når der rystes en sekser. I virkeligheden er der blot tale om en “beskyttet” delegate der erklæres ved hjælp af event-kodeordet, og der benyttes typisk den indbyggede EventHandler- eller EventHandler\<T> delegate for at overholde best-practice.

using System;

namespace Demo
{
    internal class Program
    {
        public static void Main()
        {

            Terning t = new Terning() ;
            t.Sekser += (s, e) => Console.Beep();
            t.Ryst();
            Console.WriteLine(t.Værdi);

        }
    }

    public class Terning
    {
        public event EventHandler Sekser;
        public int Værdi
        {
            get; private set;
        }

        public void Ryst()
        {
            this.Værdi = new Random().Next(1, 7);
            if (this.Værdi == 6)
                this.Sekser?.Invoke(this, new EventArgs());
        }

        public override string ToString()
        {
            return $"[ {this.Værdi} ]";
        }
    }
}

ASync

Du kan overveje at benytte en asynkron Ryst såfremt der er tale om instruktioner som kan tage tid at afvikle. Dermed kan hovedtråden frigives til andet arbejde medens Ryst afvikles (giver ikke meget mening i en konsol-app men i UI applikationer kan det have en værdi).

using System;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;

namespace Demo
{
    internal class Program
    {
        public static async Task Main()
        {

            Terning t = new Terning();
            t.Ryst();
            Console.WriteLine(t.Værdi);
            await t.RystAsync();
            Console.WriteLine(t.Værdi);

        }
    }

    public class Terning
    {

        public int Værdi
        {
            get; private set;
        }

        public void Ryst()
        {
            using (WebClient w = new WebClient())
            {
                string tal = w.DownloadString("https://www.random.org/integers/?num=1&min=1&max=6&col=1&base=10&format=plain&rnd=new");
                this.Værdi = Convert.ToInt32(tal);
            }
        }

        public async Task RystAsync()
        {
            HttpClient httpClient = new HttpClient();
            var tal = await httpClient.GetStringAsync("https://www.random.org/integers/?num=1&min=1&max=6&col=1&base=10&format=plain&rnd=new");
            this.Værdi = Convert.ToInt32(tal);
        }

        public override string ToString()
        {
            return $"[ {this.Værdi} ]";
        }
    }
}