vendredi 5 février 2021

C++ API Design Patterns

I have an API implemementation / design question to ask. I need to design a modern, usable DLL API, but I am unsure which approach is best and why. I'm not even sure if what I have below could be considered either!

My implementation of the classes needs to be abstracted from the client, but the client also needs to be able to extend and define additional (supported) classes as needed. Many of the internal classes of the library will need references to these external components, necessating the use of pure virtual classes or polymorphism.

I'm not too worried about GNU/Clang compatability - that may come later (as in much later or possibly never) but the primary need is in the ABI layers of MSVC 15/17.

The two ways I can see to implement this is to either use to use a Pimpl idiom or an external Factory method. I'm not a big fan of either (PIMPL adds to code complexity and maintainability, Factory abstracts class creation) so a well thought out third option is a possibility if viable.

For the sake of the simplicity in this post, I've put the "implementations" in the headers below, but they would in their own respective CPPs. I've also cut out any implied constructors, destructors or includes.

I'll define two pure virtual classes here for the meantime

//! IFoo.h

class IFoo
{
    virtual void someAction() noexcept = 0;
}
//! IBar.h

class IBar
{
public:
    virtual void Foo(IFoo& other) noexcept = 0;
};

First up, we have the PIMPL approach

//! BarImpl.h (Internal)

class BarImpl : public IBar
{
public:
    virtual void Foo(IFoo& foo) noexcept
    {
        foo.someAction();
    }
};
//! Bar.h (External)

class API_EXPORT Bar : public IBar
{
private:
    BarImpl* impl;

public:
    virtual void Foo(IFoo& foo) noexcept
    {
        impl->Foo(foo);
    }
};

Ideally, the client code therefore looks alittle something like this:

//! client_code.cpp
class CustomFoo : public IFoo;

int
main(int argc, char** argv)
{
    auto foo = CustomFoo();
    auto bar = Bar();
    bar.Foo(foo); // do a barrel roll
}

Secondly, we have a Factory approach

//! BarFactory.h (External)

class API_EXPORT BarFactory
{
private:
    Bar impl;

public:
    virtual IBar& Allocate() noexcept
    {
        return Bar; // For simplicity
    }
}

The resulting client code looking something like:

//! client_code.cpp
class CustomFoo : public IFoo;

int
main(int argc, char** argv)
{
    auto foo = CustomFoo();
    auto barfactory = BarFactory();
    IBar& bar = barfactory.Allocate();
    bar.Foo(foo); // do a barrel roll
}

In my eyes, both approaches have their own merits. The PIMPL method, although double abstracted and possibly slower due to all the virtualisation, brings a humble simplicity to the client code. The factory avoids this but necessitates a seperate object creation / management class. As these are external functions, I don't think I can easily get away with templating these functions as you might expect to with an internal class, so all this extra code will have to be written either way.

Both methods seem to ensure that the interface remains stable between versions - ensuring binary compatibility between future minor releases.

Would anyone be able to lend a hand to the conundrum I have created for myself above? It would be much appreciated!

Many Thanks, Chris

Aucun commentaire:

Enregistrer un commentaire