Forum: PC-Programmierung c++, Deadlock im Konzept


von Luigi (Gast)


Angehängte Dateien:

Lesenswert?

Hallo zusammen,

ich habe ein Problem mit meinem, in Qt(C++) programmierten Programm:
Ich Versuche mein Problem zunächst relativ abstrakt zu erklären, 
vielleicht kann mir dann schon jemand von euch den entscheidenden Tip 
geben ohne einen riesen Roman lesen zu müssen. Es ist Komplex genug und 
wird auch so schon viel zu lang werden...

Und zwar habe ich eine Mutter-klasse (Mother) welche die abstrakte 
Kinder-klasse beaufsichtigen muss. Jedes Kind darf aber verschiedene 
Funktionen der Mutter abrufen. Zum Beispiel dürfen alle Jungs eine 
Funktion aufrufen (boysFunction()). Analog natürlich die Mädchen. Aber 
es gibt viel viel mehr Funktionen einer Mutter (weiß ja jeder). Ich hab 
nur exemplarisch mal zwei in mein Bild eingefügt. Im prinzip darf jedes 
Kind jede der vielen Funktion bedienen, wird es aber nicht wollen.

Ein Lösung mit Qt und dem Signal-Slot-System wäre ja einfach...
Darauf möchte ich gar nicht weiter eingehen, das ist trivial.

Mein Problem ist, dass die Kinder dynamisch erzeugt werden. Die Mutter 
erzeugt die Kinder mit einer Fabrik-Klasse, ich könnte sie jetzt 
Gebärmutter nennen, nach Vorgaben in einem String (nennen wir ihn DNA). 
Sie weiß also gar nicht ob sie einen Tom, eine Pia oder eine Lisa 
produziert. Die Fabrik klasse soll aber kein riesen if-else-cluster 
werden in dem je nach Kind die Signale und Slots verbunden werden.
Schöner wäre es dem Kind zu sagen, "das hier ist deine Mama" und das 
kind weiß wo es die Funktionen der Mutter findet.

Leider ergibt sich dadurch aber ein kleiner header-deadlock, da die 
Kinder wissen müssen wie die Mutter aussieht. Umgekehrt muss natürlich 
die Mutter auch die (abstrakten) Kinder kennen. Und schon hab ich das 
Henne-Ei Problem, naja der Compiler, ich nicht...

Kann mir jemand einen Tip geben wie man sowas geschickt lösen kann?
Oder war das jetzt zu abstrakt erklärt?

Viele Grüße
Luigi

von TestX (Gast)


Lesenswert?

Forward declaration heißt das Zauberwort

von Tom (Gast)


Angehängte Dateien:

Lesenswert?

Luigi schrieb:
> da die
> Kinder wissen müssen wie die Mutter aussieht. Umgekehrt muss natürlich
> die Mutter auch die (abstrakten) Kinder kennen.

Außer forward abstraction auch immer hilfreich ist es, Abhängigkeiten in 
einem Interface zu isolieren. In diesem Fall die Mutterfunktionen, die 
von den Kindern aufgerufen werden können (IMother). Dann müssen die 
Kinder nichts über die Mutter wissen, sie bekommen nur einen Pointer 
o.ä. auf eine IMother.
Manchmal sind auch mehrere Interfaces sinnvoll, die die Mutter 
implementiert. Die Mädchen wissen nur, dass die Mutter eine 
IZöpfeflechterin ist, die Jungs kennen nur die IFußballaufpumperin. Die 
Klischees bitte entschuldigen ;).

Die Mutter muss auch nichts über die Kinder wissen, sie kennt nur Child. 
Den Unterschied zwischen Junge, Mädchen und Alien kennt nur der Uterus.


Das alles kann man gnadenlos übertreiben, bis hinter der Abstraktion 
kein Programm mehr erkennbar ist, aber zum Verständnis sind übertriebene 
Beispiele mit zuviel Architektur hilfreich.

IMother.h:
1
#pragma once
2
3
#include <string>
4
5
class IMother
6
{
7
public:
8
    virtual void greet(const std::string& name) const = 0;
9
    virtual void feed(const std::string& name) const = 0;
10
};


Child.h:
1
#pragma once
2
3
#include <string>
4
5
class IMother;
6
7
class Child
8
{
9
protected:
10
    std::string name;
11
    const IMother& mom;
12
public:
13
    Child(const std::string& n, const IMother& mo);
14
15
    virtual void do_stuff() const = 0;
16
};


Child.cpp:
1
#include "Child.h"
2
3
Child::Child(const std::string& n, const IMother& mo): name {n}, mom {mo}
4
{
5
}


Girl.h:
1
#pragma once
2
3
#include "Child.h"
4
#include <string>
5
6
class IMother;
7
8
class Girl: public Child
9
{
10
public:
11
    Girl(const std::string& n, const IMother& mo);
12
    void do_stuff() const;
13
};


Girl.cpp:
1
#include "Girl.h"
2
#include "IMother.h"
3
4
Girl::Girl(const std::string& n, const IMother& mo): Child {n,mo}
5
{
6
}
7
8
void Girl::do_stuff() const
9
{
10
    mom.greet(name);    // maedchen werden gerne begruesst.
11
}


Boy.h:
1
#pragma once
2
3
#include "Child.h"
4
#include <string>
5
6
class IMother;
7
8
class Boy: public Child
9
{
10
public:
11
    Boy(const std::string& n, const IMother& mo);
12
    void do_stuff() const;
13
};


Boy.cpp:
1
#include "Boy.h"
2
#include "IMother.h"
3
4
Boy::Boy(const std::string& n, const IMother& mo): Child {n,mo}
5
{
6
}
7
8
void Boy::do_stuff() const
9
{
10
    mom.feed(name);    // jungs sind verfressen
11
}


Mother.h:
1
#pragma once
2
3
#include "IMother.h"
4
#include "Uterus.h"
5
#include <memory>
6
#include <vector>
7
8
class Child;
9
10
class Mother: public IMother
11
{
12
    Uterus my_uterus;
13
    
14
    std::vector<std::unique_ptr<Child> >  my_kids;
15
    
16
public:
17
    Mother();
18
    void new_kid();
19
    void do_things_with_kids();
20
    virtual void greet(const std::string& name) const ;
21
    virtual void feed(const std::string& name) const;
22
};


Mother.cpp:
1
#include "Mother.h"
2
3
#include "Child.h"
4
5
#include <iostream>
6
7
Mother::Mother()
8
{
9
}
10
11
void Mother::new_kid()
12
{
13
    my_kids.emplace_back(my_uterus.give_birth(*this));
14
}
15
16
17
void Mother::do_things_with_kids()
18
{
19
    for(const auto& kid: my_kids)
20
        kid->do_stuff();
21
}
22
23
24
25
26
void Mother::greet(const std::string& name) const
27
{
28
    std::cout << "Hallo, liebe(r) " << name << "!\n";
29
}
30
31
void Mother::feed(const std::string& name) const
32
{
33
    std::cout << "Lecker Essen fuer " << name << "!\n";
34
}


Uterus.h:
1
#pragma once
2
3
#include <memory>
4
5
class IMother;
6
class Child;
7
8
class Uterus
9
{
10
    int counter;
11
public:
12
    Uterus();
13
    std::unique_ptr<Child> give_birth(const IMother& mom);
14
};


Uterus.cpp:
1
#include "Uterus.h"
2
#include "IMother.h"
3
#include "Boy.h"
4
#include "Girl.h"
5
6
Uterus::Uterus(): counter {0}
7
{
8
}
9
10
std::unique_ptr<Child> Uterus::give_birth(const IMother& mom)
11
{
12
    if (counter == 0)
13
    {
14
        ++counter;
15
        return std::make_unique<Boy>("[some boy name]", mom);
16
    }
17
    else
18
    {
19
        counter = 0;
20
        return std::make_unique<Girl>("[some girl name]", mom);
21
    }
22
}


1.cpp:
1
#include "Mother.h"
2
#include "Child.h"
3
4
#include <iostream>
5
#include <string>
6
#include <memory>
7
8
int main()
9
{
10
    Mother my_mom;
11
    for (int i =0; i < 7; ++i)
12
        my_mom.new_kid();
13
        
14
    my_mom.do_things_with_kids();
15
}

von Luigi (Gast)


Lesenswert?

Wow,
vielen Herzlichen Dank!
Das hilft sehr viel weiter.
Danke für die sehr ausführliche und anschauliche Antwort.

von Tom (Gast)


Lesenswert?

Noch etwas Ergänzung und Korrektur:

Wenn eine Klasse Foo nur einen Pointer/Referenz auf ein Blubb enthält, 
reicht in Foo.h eine forward declaration "class Blubb;", und Blubb.h 
muss nicht includet werden. Das ist immer sinnvoll und löst auch hier 
das Problem der Kreis-Includes. In Foo.cpp muss Blubb.h includet werden, 
um mit dem Pointer etwas anfangen zu können, aber das führt nicht zu dem 
originalen Problem.

Damit bleiben beide Klassen eng gekoppelt und abhängig von der 
Implementierung der jeweils anderen. Wenn das nicht stört, ist das die 
einfachste und damit beste Lösung.





Ein anderer Ansatz ist das erwähnte Interface (forward-deklarieren kann 
und soll man trotzdem). Manchmal ergibt sich das ganz natürlich aus der 
Aufgabenstellung (anhand von weltfremden Beispielen kann man das nicht 
entscheiden) und man schreibt in die Kinder keine unnötigen 
Abhängigkeiten von unwichtigen Implementierungsdetails der Mutter 
hinein.

Wenn man am Ende trotzdem alle Methoden der Mutter in das Interface 
packt, hat man außer Komplexität nichts gewonnen; die späteren 
Änderungen, die durch das Interface leichter würden, passieren sowieso 
nie oder sind so anders als vorhergesehen, dass ein ganz anderer Ansatz 
notwendig wird. YAGNI.

Das gilt für mein Riesen-Beispiel oben, IMother kann man ohne Probleme 
rauswerfen, forward declaration ist völlig ausreichend. Wenn in Zukunft 
verschiedene Mütterklassen auftauchen und eine Basisklasse/Interfacce 
notwendig wird, kann man dies nachholen.


Die erwähnten mehrfachen Interfaces führen darüber hinaus zu viel Spaß 
und Erkenntnisgewinn auf dem Gebiet der Mehrfachvererbung, wenn es 
Überschneidungen zwischen den enthaltenen Methoden gibt.

Bitte melde dich an um einen Beitrag zu schreiben. Anmeldung ist kostenlos und dauert nur eine Minute.
Bestehender Account
Schon ein Account bei Google/GoogleMail? Keine Anmeldung erforderlich!
Mit Google-Account einloggen
Noch kein Account? Hier anmelden.