[C++] ABC's implementeren in meerdere classes

Pagina: 1
Acties:

  • madwizard
  • Registratie: Juli 2002
  • Laatst online: 26-10-2024

madwizard

Missionary to the word of ska

Topicstarter
Het project waar ik mee bezig ben bestaat uit een hoofdprogramma en een static library die daardoor gebruikt wordt. Om de interface van die lib mooi gescheiden te houden van de implementatie binnen de lib heb ik wat verschillende methodes bekeken om voor de gebruikte classes een interface te maken.

Nu heb ik bijvoorbeeld een abstracte class IFolder die als interface dient voor de interne class Folder die de implementatie bevat, en extra methodes die alleen intern aangeroepen mogen worden. Het enige is dat intern steeds Folder wordt gebruikt, en bij de interface alleen parameters als IFolder meegegeven kunnen worden. Als er dus een methode is als addChild(IFolder *pChild), moet pChild eerst gecast worden naar een Folder* voordat deze intern wordt opgeslagen. Op zich is dit geen probleem want de classes worden op 1 na nooit buiten de lib aangemaakt en dus zal een IFolder ook altijd een Folder zijn.

Het wordt pas een probleem bij het implementeren van meerdere interfaces die van elkaar erven. Er is bijvoorbeeld ook een ISubFolder interface (+SubFolder met implementatie), die inherit van IFolder, evenals IRootFolder. De interface IFolder wil ik implementeren met Folder, alle extra methodes die er in subinterfaces bijkomen moeten geimplementeerd worden in de bijbehorende classes, die allemaal weer een subclass zijn van Folder.

In java zou dit prima gaan:
Java:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
interface IFolder
{
    String getName();
}
interface ISubFolder extends IFolder
{
    IFolder getParent();    
}
class Folder implements IFolder
{
    String getName() { impl; }
}
class SubFolder extends Folder implements ISubFolder
{
    IFolder getParent() { impl; }
}

De implementatie van IFolder staat in Folder, die van ISubFolder (-IFolder) in ISubFolder en SubFolder voldoet aan de specs van beide interfaces.

Nu hetzelfde in C++, de interfaces kunnen met een abstracte class gemaakt worden. Door de hierarchie komt IFolder twee keer als superclass voor, maar als deze virtual wordt gemaakt zou het in principe moeten werken:
C++:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class IFolder
{
public:
    virtual std::string getName() const = 0;
};

class ISubFolder : public virtual IFolder
{
public:
    virtual IFolder * getParent() const = 0;
};

class Folder : public virtual IFolder
{
public:
    virtual std::string getName() const { impl; }
};

class SubFolder : public Folder, public ISubFolder
{
public:
    virtual IFolder * getParent() const { impl; }
};

VC geeft bij deze code een warning (C4250, inheritance via dominance), maar die schijnt puur informatief te zijn (g++ geeft em niet). Dit zou de oplossing moeten zijn maar er kwamen weer problemen zodra de interface gecast moest worden naar zijn implementatie class. Zoals ik al eerder zei zijn er methodes die een interface pointer als parameter krijgen om een bepaald object aan te duiden. Bijvoorbeeld addChild:

C++:
1
2
3
4
5
6
(in Folder)
    virtual void addChild(IFolder *pIFolder)
    {
        // Cast naar hele object voor intern gebruik
        Folder *pf = static_cast<Folder*>(pIFolder);
    }

Dit gaat natuurlijk niet goed omdat IFolder een *virtual* base class is en dus niet gecast kan worden.
Hoe kan ik dit het beste oplossen? Ik kan ook nog wel kiezen voor een andere interface/implementatie scheiding maar de hierarchie van interfaces en classes zoals hierboven moet wel kunnen.

www.madwizard.org


  • Zoijar
  • Registratie: September 2001
  • Niet online

Zoijar

Because he doesn't row...

Hmm meestal als je moet down-casten is je ontwerp niet goed. (waar je overigens dynamic_cast voor gebruikt en niet static). Denk dat je je moet afvragen waarom je een Folder nodig hebt, en of je de benodigde functionaliteit niet op IFolder kan opleggen. Als dat niet gaat kan je altijd nog een soort visitor gebruiken. De standaard structuur voor folders met children etc is een composite pattern, met daarop een visitor pattern icbm een iterator om alle nodes af te gaan.

  • madwizard
  • Registratie: Juli 2002
  • Laatst online: 26-10-2024

madwizard

Missionary to the word of ska

Topicstarter
Zoijar schreef op 29 december 2002 @ 20:39:
Hmm meestal als je moet down-casten is je ontwerp niet goed.
Het downcasten zal nog wel vaker voorkomen, hoe zou je het dan beter kunnen doen bij bijvoorbeeld het volgende simpelere voorbeeldje? In de lib zijn verzamelingen Artist's en Album's opgeslagen, een Album heeft optioneel een pointer naar z'n primary artist. Die moet van buiten de lib geset kunnen worden. Als ik het model met de IAlbum en IArtist interfaces zou houden krijg je toch dat de setPrimaryArtist method van IAlbum een IArtist pointer meekrijgt, omdat de caller geen beschikking heeft/mag hebben over de Artist class met de implementatie:

C++:
1
2
3
IArtist *pArtist = blah.getArtistOnName("fiets");
IAlbum  *pAlbum  = blah.getSomeAlbum();
pAlbum->setPrimaryArtist(pArtist);


Intern wordt een Artist pointer gebruikt om ook toegang te hebben tot de interne methodes, dus wordt een downcast onvermijdelijk hier.
Denk dat je je moet afvragen waarom je een Folder nodig hebt, en of je de benodigde functionaliteit niet op IFolder kan opleggen.
Ik wil de IFolder implementatie in Folder hebben omdat die vrijwel gelijk is voor alle soorten folders. De subclasses hebben alleen wat extra methodes.
Als dat niet gaat kan je altijd nog een soort visitor gebruiken. De standaard structuur voor folders met children etc is een composite pattern, met daarop een visitor pattern icbm een iterator om alle nodes af te gaan.
Het is geen algemene folder structuur maar het moet een deel van het filesystem representeren, dwz de structuur wordt uiteindelijk volledig 'gekopieerd' als echte directorystructuur. Daarom heb ik ook duidelijk onderscheid gemaakt tussen een RootFolder (die het pad van de fysieke folder bevat waar de hele structuur begint), en SubFolder's, die daaronder hangen. Folder heeft methodes die voor elke folder toepasbaar zijn, zoals het absolute pad, naam, etc.

Het model voor de folders kan ik eventueel wel aanpassen, al leek mij dit zelf het handigst. Het downcasten zie ik liever ook niet, wie een oplossing heeft mag het zeggen.

[ Voor 11% gewijzigd door madwizard op 29-12-2002 21:32 ]

www.madwizard.org


  • whoami
  • Registratie: December 2000
  • Laatst online: 09:26
Ik vraag me af waarom je in de definitie van jouw pure virtual function die 'const' gebruikt?

Waarom niet gewoon:
code:
1
virtual void MyVirtFunc() = 0;

:?

En waarom inherit je 'virtual' van IFolder?
Waarom niet:
code:
1
class Folder : public IFolder

:?
Als je dat zo doet, dan kan je die abstracte class IFolder nog altijd abstract houden toch? Als je minstens één pure virtuele functie in die IFolder class hebt, is die per definitie abstract.

https://fgheysels.github.io/


  • madwizard
  • Registratie: Juli 2002
  • Laatst online: 26-10-2024

madwizard

Missionary to the word of ska

Topicstarter
whoami schreef op 29 December 2002 @ 22:47:
Ik vraag me af waarom je in de definitie van jouw pure virtual function die 'const' gebruikt?

Waarom niet gewoon:
code:
1
virtual void MyVirtFunc() = 0;

:?
Het is een method die de class data niet wijzigt, waarom zou ik em dan niet const maken? :? const achter een method geeft aan dat de method geen data binnen de class mag wijzigen. Niet verplicht maar is wel zo netjes en vaak ook nodig.
En waarom inherit je 'virtual' van IFolder?
Waarom niet:
code:
1
class Folder : public IFolder

:?
Als je dat zo doet, dan kan je die abstracte class IFolder nog altijd abstract houden toch? Als je minstens één pure virtuele functie in die IFolder class hebt, is die per definitie abstract.
'Virtual' inheritten heeft niets te maken met de aanwezigheid van virtuele functies in IFolder, virtual voor een class in de superclass lijst geeft aan dat je maar 1 instantie van die class in je hierarchie wilt hebben. Als ik virtual zou weglaten zouden er 2 IFolder instanties in een SubFolder object zijn, 1 via Folder en 1 via ISubFolder. Sterker nog, het geheel zou niet compilen omdat ik dan niet voor beide IFolder's een complete implementatie zou hebben.

[ Voor 3% gewijzigd door madwizard op 30-12-2002 01:00 ]

www.madwizard.org


  • MSalters
  • Registratie: Juni 2001
  • Laatst online: 21-08 17:14
whoami, de const is onderdeel van de method signature. Zonder die const zijn de concrete implementaties van getName() en getParent() niet gerelateerd aan de interface, omdat ze dan verschillende signatures hebben. Dat betekent weer dat je concrete bedoelde classes abstract worden.

Om het originele probleem te tacklen:
Dump de implementation inheritance. Als class SubFolder gebruik kan maken van class Folder, maak het dan een private member. Clients gebruiken toch geen class SubFolder of Folder, en in de implementatie van SubFolder kun je gewoon een pointer maken naar je private member this->folder_part, als je Folder gedrag wilt.

Man hopes. Genius creates. Ralph Waldo Emerson
Never worry about theory as long as the machinery does what it's supposed to do. R. A. Heinlein


  • madwizard
  • Registratie: Juli 2002
  • Laatst online: 26-10-2024

madwizard

Missionary to the word of ska

Topicstarter
MSalters schreef op 30 December 2002 @ 02:51:
Om het originele probleem te tacklen:
Dump de implementation inheritance. Als class SubFolder gebruik kan maken van class Folder, maak het dan een private member. Clients gebruiken toch geen class SubFolder of Folder, en in de implementatie van SubFolder kun je gewoon een pointer maken naar je private member this->folder_part, als je Folder gedrag wilt.
Ik denk dat alleen IFolder als interface ook wel voldoet, ISubFolder zal niet veel methodes toevoegen, het is meer om het type te kunnen onderscheiden van andere *Folder's. In de implementatie overchrijft SubFolder nu nog alleen wat methodes van Folder, dat kan wel zo blijven.

Maar het probleem met het meegeven van interface pointers om het hele object aan te duiden (zie m'n 2e post) blijft... Al zit ik persoonlijk niet echt met die downcast, natuurlijk is het niet optimaal maar aangezien de hele library al eist dat er geen instanties van z'n classes buiten gemaakt mogen worden is het op zich veilig. Voor de zekerheid kan ik er wel een dynamic_cast van maken, als er dan iets fout gaat is het echt een bug in de code van de caller.

www.madwizard.org


  • MSalters
  • Registratie: Juni 2001
  • Laatst online: 21-08 17:14
Ehm, het cast-probleem moet je niet proberen op te lossen. Als je een IFolder krijgt, gebruik dan die IFolder. Dat is de interface van Folder, hoe je een Folder gebruikt.

Als de IFolder van een SubFolder is, dan kun je in Folder::addChild niet met de Folder internals van een SubFolder gaan rotzooien. Wat als je later besluit dat een betere implementatie van SubFolder helemaal geen Folder implementatie meer gebruikt? Dan kun je dus helemaal geen IFolder* ->Folder* conversie meer doen. En het hele idee van een aparte interface is nou juist dat je in de implementatie dan kunt doen wat je wil.

Man hopes. Genius creates. Ralph Waldo Emerson
Never worry about theory as long as the machinery does what it's supposed to do. R. A. Heinlein


  • madwizard
  • Registratie: Juli 2002
  • Laatst online: 26-10-2024

madwizard

Missionary to the word of ska

Topicstarter
MSalters schreef op 31 December 2002 @ 02:15:
Ehm, het cast-probleem moet je niet proberen op te lossen. Als je een IFolder krijgt, gebruik dan die IFolder. Dat is de interface van Folder, hoe je een Folder gebruikt.
Dat is waar, maar vaak is de interface IFolder te beperkt om een operatie uit te voeren zonder de extra methodes van zijn eigen Folder object te gebruiken. Bijvoorbeeld bij het Artist / Album probleem. Tussen die twee bestaat een 1-to-many relatie zodat een Album een bepaalde Artist heeft. Om het zoeken een beetje efficient te maken bevat Album niet alleen een Artist pointer maar Artist ook een vector van Album pointers. Anders zou ik alle Albums af moeten om alle albums van een bepaalde Artist op te zoeken.
Als je nu zoals in m'n eerdere post setPrimaryArtist aanroept op een Album moeten er dus 2 dingen gebeuren. 1) De Artist * member moet naar de nieuwe Artist verwijzen. 2) Aan de Album* vector in het Artist object zelf moet het album toegevoegd worden.

Als de artist als IArtist* meegegeven wordt zal deze voor 1 al gecast moeten worden naar een Artist*. Op zich kan dit wel anders door de member zelf ook een IArtist* te laten zijn.

Bij 2 moet er voor het Artist object een methode zijn die een (I)Album* aan z'n vector toevoegd. Zo'n methode kan ik best maken maar ik wil zeker niet dat deze beschikbaar is in de IArtist interface want dan zou van buitenaf de structuur ongeldig gemaakt kunnen worden. Mijn idee was dus zo'n methode in Artist te stoppen maar niet in IArtist, en met een cast de interface naar het hele object te casten zodat de methode aangeroepen kan worden.

Een soortgelijk probleem krijg je met alle methodes in de interface die een andere interface pointer retourneren, zoals IArtist Album::getPrimaryArtist(). Zo'n methode heb je intern ook nodig maar dan wil je eigenlijk een Artist en niet een IArtist, omdat daar weer extra methodes in zitten die je nodig hebt. Je kunt natuurlijk gewoon casten of netter, een extra methode schrijven die hetzelfde doet maar dan een Artist retourneerd (methode moet dan ook meteen een andere naam krijgen...) maar dat is dubbel werk.

Ik zit er over te denken om een Handle class (/template) voor de objecten te maken, die meegeven kan worden in plaats van een interface pointer. De -> operator kan dan een client interface pointer retourneren zodat de class feitelijk als interface pointer werkt. Dan heb je alleen nog een nette methode nodig om het hele (interne) object te krijgen uit die handle, iets wat natuurlijk alleen door de library zelf gedaan mag worden. Zoiets dus:
C++:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// concreet voorbeeld, moet uiteraard als template
class IBlah
{
public:
    virtual void test() = 0;
};

class Blah : public IBlah
{
public:
    void internalMethod() {}
    void test() {}
};

class BlahHandle
{
public:
    BlahHandle(Blah *ptr) : _ptr(ptr) {}
    IBlah * operator->() { return _ptr; }
private:
    Blah *_ptr;
};

En voor de BlahHandle class een friend function (waar alleen de library beschikking over heeft) die de _ptr eruit kan trekken als dat nodig is..
Zo kan de client ook de handles gebruiken als verwijzing naar een bepaald object zonder dat de implementatie die hoeft te casten.

www.madwizard.org

Pagina: 1