Abou Chleih

{the magic lies between the brackets}

Menü Schließen

Schlagwort: vererbung

UML Beziehungen und ihre Umsetzung im Code

Wer Software plant und entwickelt wird unweigerlich mit der Unified Modeling Language (kurz UML) in Berührung kommen.
Ich werde in  diesem Blog-Eintrag vor allem auf das Klassendiagramm, genauer auf die Beziehungen der einzelnen Klassen eingehen.

Das Klassendiagramm:

UML_ClassdiagramDas Klassendiagramm zeigt  Klassen eines Namespaces an und deren jeweilige Abhängigkeiten. Es handelt sich um eine statische Darstellung, da lediglich die Attribute und Methoden, sowie die Verbindungen der Klassen untereinander dargestellt werden. Es wird nicht gezeigt, wie diese Verbindungen stattfinden.

Darstellungselemente:

Zur Realisierung eines Klassendiagramms stehen folgende Formen und Notationen zur Verfügung

Klasse

ClassEine Klasse wird mit als Rechteck dargestellt und enthält Attribute und Methoden (Operationen). Sollte es sich um eine abstrakte Klasse handeln, so wird der Klassennamen kursiv dargestellt

Zusätzlich werden die in der Klasse enthaltenen Attribute und Operationen (bspw. Methoden) mit deren Daten- oder ggf. Rückgabetypen und Sichtbarkeiten dargestellt.

Class_ExplanationClass_AttrOperation

Zur Darstellung der Sichtbarkeit von Attributen und Funktionen bietet UML folgende Zeichen:

  • + steht für public, also eine öffentliche Funktion/ein öffentliches Attribut
  • # steht für protected, also eine geschützte Funktion/ein geschütztes Attribut
  • – steht für eine private Funktion/ein privates Attribut

Wie Anfangs erwähnt, stellt das Klassendiagramm auch die Beziehungen der Klassen untereinander dar. Ich werde in diesem Beitrag auf folgende Beziehungen eingehen und zu der jeweiligen Beziehung ein Codebeispiel (C#) geben:

  • Vererbung
  • Binäre Assoziation (zwei Klassen sind beteiligt)
  • Aggregation
  • Komposition
Vererbung:

Eine Vererbung (engl.: Inheritance) ist auch als „Ist-Ein-Beziehung“ oder „Ist-Implementiert-Als-Beziehung“ bekannt.

Als Beispiel dienen hier die Klassen Employee und Manager
Da ein Manager auch ein Mitarbeiter (Employee) ist, erbt die Klasse Manager von Employee
Dadurch werden Variablen, Felder und Methoden, welche nicht private sind in die erbende Klasse übernommen.
Achtung: Konstruktoren werden nicht vererbt und müssen daher manuell angesprochen werden:

public Manager(string _name):base(_name){ ... }

Die Umsetzung in C# würde wie folgt aussehen (Bild):

[spoiler title=“Employee-Klasse“ style=“fancy“ anchor=“BeispielVererbung“]

namespace Vererbung
{
    class Employee
    {
        static int staticID = 1;

        protected int ID = 0;
        protected string Name = "";

        public Employee(string _name)
        {
            CreateEmployee(_name);
        }

        protected void CreateEmployee(string _name)
        {
            staticID++;
            ID = staticID;
            Name = _name;
        }

        public int getID()
        {
            return ID;
        }

        public string getName()
        {
            return Name;
        }
    }
}

[/spoiler]

[spoiler title=“Manager-Klasse“ style=“fancy“]

namespace Vererbung
{
    class Manager:Employee
    {
        static int staticManagerID = 1;

        int ManagerID;

        public Manager(string _name):base(_name)
        {
            CreateManager();
        }

        private void CreateManager()
        {
            staticManagerID++;
            ManagerID = staticManagerID;
        }

        public int getManagerID()
        {
            return ManagerID;
        }
    }
}

[/spoiler]

(Binäre) assoziation:

Eine Assoziation ist eine beliebige Beziehung zwischen den Objekten von Klassen. Hier werden wir nur die binären Assoziationen (2 Klassen sind beteiligt) behandeln.

Dies ist eine gerichtete Assoziation, das bedeutet, dass Klasse 1 die Klasse 2 kennt, dagegen Klasse 2 nichts von Klasse 1 weiß. In der Beschreibung wird Beschrieben auf welche Weise die Objekte in Verbindung stehen

Links sehen wir eine bidirektionale binäre Assoziation, d.h. die Firma kennt seinen Mitarbeiter und der Mitarbeiter seine Firma.

Die Form der Assoziation sähe in der Programmierung wie folgt aus (Man beachte die Kommentierung):

[spoiler title=“Bidirektionale binäre Assoziation“ style=“fancy“ anchor=“BeispielAssoziation“]

public class Firma 
{ 
  public Mitarbeiter mitarbeiter; 
}

 public class Mitarbeiter 
{ 
  public Firma firma; 
}

public class Verzeichnis
{ 
  Mitarbeiter Mustermann = new Mitarbeiter(); 
  Firma  Musterfirma  = new Firma(); 
  Mustermann.firma  = Musterfirma;
  Musterfirma.mitarbeiter = Mustermann; 
  // Man beachte, dass die Objekte von Firma und Mitarbeiter in einer
  // weitern Klasse liegen und somit nicht voneinander abhängig sind
  // und somit weiterleben, auch wenn eines der Objekte zerstört wird
}

[/spoiler]

Aggregation:

Bei der Aggregation handelt es sich um eine spezielle Form der Assoziation. Sie ist auch als „Ganzes-Teil-Beziehung“ oder „Hat-Ein-Beziehung“ bekannt.
Darunter versteht man ein Objekt (Aggregatobjekt), welches aus verschiedenen Einzelteilen (Objekten) besteht, wobei diese nicht existenzabhängig sind (d.h. sie leben weiter, auch wenn das Aggregatobjekt zerstört wird.

Eine Beispiel für eine Aggregation wäre das Auto (als Ganzes) und ein Rad, als Teil.

[tabs]
[tab title=“Allgmeines Beispiel“]

public class Aggregatklasse
{
    ExistenzUnabhängigeKlasse objKlasse;
    public void doSomething(ExistenzUnabhängigeKlasse obj)
    {
      //Das Objekt der Klasse ExistenzUnabhängigeKlasse wird als Parameter übergeben 
      objKlasse = obj; 
	  // und ist somit noch existent wenn die Aggregatklasse zerstört wird
    }
}

[/tab]
[tab title=“Auto-Rad-Beispiel“]Wird nachgereicht…

[/tab]
[/tabs]

Komposition:

CompositionBei der Komposition handelt es sich um eine strenge Form der Aggregation. Das Kompositionsobjekt besteht, wie bei der Aggregation auch, aus mehreren Einzelteilen (Komponentenobjekte), welche jedoch im Gegensatz zur Aggregation mit der Zerstörung des Ganzen (Kompositionsobjekt) zerstört werden (Existenzabhängigkeit).
Damit ist auch die Kardinalität/Multiplizität auf Seite des Kompositionsobjekts immer 1. Das bedeutet, dass eine Komponente nur Teil eines Objekts sein kann. Somit wird sichergestellt, dass die Lebensdauer aller Objekte gleich ist. Die Kardinalität auf der Seite der Komponenten kann jedoch variable sein

Beispiel:
Ein Kunde einer Bank hat mindestens eine Anschrift und keine oder mehrere hinterlegte Bankverbindungen (kann per Rechnung zahlen).

CompositionExampleStirbt der Kunden, so wohnt er logischerweise nicht mehr und ist ebenso wenig ein Kunde der Bank. Seine Anschrift(en), sowie seine Bankverbindung(en) werden also zerstört

Die Umsetzung im Code würde wie folgt aussehen:

[spoiler title=“Codebeispiel Komposition“ style=“fancy“ anchor=“BeispielKomposition“]

public class CompositionClass
{
    SomeUtilityClass objSC = new SomeUtilityClass(); //Das Objekt der Klasse
    // existiert lediglich in der CompositionClass und wird mit dem Objekt der
    // CompositionClass vernichtet

    public void doSomething()
    {
        objSC.someMethod();
    }
}

[/spoiler]

[C#/.NET] – Unterschied zwischen new virtual und override

Wer mit Vererbung arbeitet, hat sicher schon mit dem Begriff override zu tun gehabt.
Was es aber auch gibt ist das Schlüsselwort new bzw. virtual .
Wo ist der Unterschied?

Erstellen wir ein Beispiel. Wir haben eine Klasse mit der Bezeichnung Engineer:

     class Engineer
    {
        protected string name;
        protected double billingRate;

        public Engineer(string name, double billingRate)
        {
            this.name = name;
            this.billingRate = billingRate;
        }

        public virtual double calcMoney(double hours) //virtual ermöglicht das überschreiben der Methode in abgeleiteten Klassen
        {
            return billingRate * hours * 2.0d; //Standardfunktion, falls nicht überschrieben
        }

    }  

Hier haben wir die virtuelle* Methode calcMoney.

virtual: Mit dem virtual-Schlüsselwort kann eine Methode, eine Eigenschaft, ein Indexer oder eine Ereignisdeklaration geändert und in einer abgeleiteten Klasse überschrieben werden.

(Quelle: MSDN)

Nun erstellen wir eine Klasse CivilEngineer (Bauingenieur) und leiten von Engineer-Klasse ab:

     class CivilEngineer : Engineer
    {
        public CivilEngineer(string name, double billingRate)
            : base(name, billingRate)
        {
            this.name = name;
            this.billingRate = billingRate;
        }

        public override double calcMoney(double hours) //Hier wird die Funktion aus der Klasse Engineer
        { //überschrieben, d.h. hier wird jetzt diese Funktion verwendet, anstatt der Basisfunktion
            return billingRate * hours * 1.5d; //Faktor auf 1.5d geändert
        }
    } 

Der Konstruktor ruft hier zuerst den Konstruktor der Basisklasse (Engineer) auf base(name, billingRate) und übergibt die Parameter der CivilEngineer-Klasse.

Nun überschreiben wir die Methode der Basisklasse und ersetzen Sie durch calcMoney der CivilEngineer Klasse.

Jetzt erstellen wir ein Objekt des Engineers und ein Objekt des CivilEngineers.

             CivilEngineer c1 = new CivilEngineer("John",15d);
            Engineer e1 = new Engineer("Keith", 10d); 

Nun erstellen wir eine Liste von Engineers (List<Engineer>) und fügen beide Objekte dieser Liste hinzu:

             List elist = new List();
            elist.Add(c1);
            elist.Add(e1); 

Nun rufen wir bei beiden Objekten die Funktion calcMoney() auf.

             e1.calcMoney(10); //Es wird die Methode Engineer.calcMoney() aufgerufen (Ist ja klar!)
            c1.calcMoney(10); // Es wird die überschriebene Methode CivilEngineer.calcMoney() aufgerufen (Soweit, so gut. Passt alles) 

Jetzt rufen wir die calcMoney()-Methode innerhalb einer foreach-Schleife in der Liste auf:

             foreach (Engineer eng in blalist) //Alle Objekte, egal ob abgeleitet oder nicht, sind vom Typ Engineer
            {
                eng.calcMoney(10);
            } 

Beim ersten Durchlauf wird jetzt die Funktion des Objektes c1 aufgerufen, da dieses zuerst zur Liste hinzugefügt wurde.
Es wird, wie erwartet die überschriebene Methode der abgeleiteten Klasse (CivilEngineer) aufgerufen (also CivilEngineer.calcMoney()), da wir diese in der Klasse Engineer überschrieben (override) haben.

Im darauffolgenden Durchlauf wird jetzt die Funktion des Objektes e1 aufgerufen:
Es wird die Methode der Klasse Engineer aufgerufen, da das Objekt vom Typ Engineer ist – also Engineer.calcMoney(10).

Dies war jetzt mit den Schlüsselwörtern virtual und override.


Jetzt kommen wir zu new:

Fügen wir eine Klasse TankEngineer hinzu, welche die Methode calcMoney() hat, aber hier mit dem new Schlüsselwort deklariert:

    class TankEngineer : Engineer
    {
        public TankEngineer(string name, double billingRate) //Konstruktor der abgeleiteten Klasse
            : base(name, billingRate) //Ruft den Konstruktor der Basisklasse auf, da es von dieser ableitet (benötigt zur Existenz)
        //gleiche Parameter!
        {
            this.name = name; //Zuweisung der Eigenschaften (vererbt)
            this.billingRate = billingRate;
        }

        public new double calcMoney(double hours) //Hier wird die Funktion aus der Klasse Engineer
        { //verdeckt (!) und NICHT überschrieben
            return billingRate * hours * 3.0d; //Faktor auf 3.0d geändert
        }
    }

Nun erstellen ein Objekt vom Typ TankEngineer und fügen es der vorigen Liste mit den zwei anderen Objekten hinzu:

 TankEngineer t1 = new TankEngineer("Robert", 20d); 
 elist.Add(t1); // Füge den TankEngineer hinzu 

Rufen wir nun die Methode calcMoney direkt über das Objekt auf,

  t1.calcMoney(10); // Ruft TankEngineer.calcMoney auf, da vom Typ TankEngineer und Basismethode verdeckt 

Wird die Methode TankEngineer.calcMoney aufgerufen, da diese in der Klasse TankEngineer existiert.

Gehen wir aber nun per foreach wieder durch die Liste (elist):

             foreach (Engineer eng in elist)
            {
                eng.calcMoney(10);
                // 3. Durchgang (t1), ruft die Engineer.calcMoney-Methode auf, da diese existent ist und durch 
                // das new-Schlüsselwort nur verdeckt wurde. 
            } 

so wird hier jetzt die Methode der Basisklasse aufgerufen, da die Liste nur Objekte vom Typ Engineer hält und die Methode nicht überschrieben, sondern nur verdeckt wurde.
D.h. es waren zur Laufzeit beide Methoden des gleichen Namens in beiden Klassen verfügbar.

Zur Veranschaulichung habe ich euch ein kleines Projekt gebastelt, in welchem ihr durch Debugging selbst sehen könnt, wie override bzw. new arbeiten: DOWNLOAD

© 2020 Abou Chleih. Alle Rechte vorbehalten.

Thema von Anders Norén.