Come implementare Progressive Disclosure in C ++ API

0

Seguendo l'articolo I programmatori sono persone troppo di Ken Arnold, ho cercato di implementare il < schema di> divulgazione progressiva per un'API.

Fondamentalmente, l'idea menzionata nel testo è di suddividere l'API in categorie e presentare all'utente solo ciò di cui ha bisogno. Il resto è nascosto, riducendo la complessità per qualcuno che utilizza l'API. Nel testo, presenta la sua idea con la classe JButton e i suoi oltre 100 metodi:

[...] we could use progressive disclosure to help reduce the complexity of that JButton class: put the expert stuff in an object returned by a getExpertKnobs() method, and the graphics subsystem hooks in an object returned by a getIntegrationHooks() method, and you would be left with a button API that had just a handful of methods—the basic methods we all need.

Ho cercato di implementare un esempio minimale di ciò usando C ++. Finora, non ho ottenuto risultati accettabili. Quello che voglio è questo comportamento e semplicità:

Innanzitutto, la classe contenente solo i metodi di base e ampiamente diffusi:

struct Base
{
    Base()
    {
        // Initialize attributes (possibly many)
    }

    void simpleMethod()
    {
        // Do something simple...
    }

    ExtBase ext() 
    {
        // Access to advanced features;
        // this is the tricky part for me...
    }

private:
    // Attributes...
};

Quindi, la classe contenente le cose avanzate / esperte:

struct ExtBase
{
    ExtBase()
    {
        // Not sure of what goes here...
    }

    void advancedMethod()
    {
        // Do something complicated...
    }
};

Idealmente, questa "API" potrebbe essere utilizzata semplicemente in questo modo dall'utente:

int main ()

{
    Base aBase;

    aBase.simpleMethod();           // OK
    aBase.advancedMethod();         // Fails. Not available by default.

    aBase.ext().simpleMethod();     // Possibly OK, not sure...
    aBase.ext().advancedMethod();   // OK

    return 0;
}

Ho cercato sul Web per trovare esempi di questo, ma finora non è venuto fuori nulla di veramente interessante. Ho trovato un esempio in Java da questa libreria ma non ero in grado di replicarlo in C ++.

Qualcuno ha qualche idea su come questo potrebbe essere fatto?

    
posta BobMorane 25.11.2016 - 14:35
fonte

4 risposte

1

Quello che segue è un esempio di codice che crea due oggetti "gemelli" in cui la durata è strettamente correlata l'una con l'altra.

Tecnicamente, Extra è un membro di Base , ma Extra non consiste nient'altro che un puntatore (riferimento) all'istanza di Base che lo contiene. Pensala come un nodo figlio che ha un puntatore a un nodo genitore.

Ho usato questo modello con Visual C ++ (in particolare durante la programmazione in ambienti con modelli a oggetti componenti). Tuttavia, non so se questo modello è valido in C ++ Standard.

Ho sentito che, in alcuni ambienti del compilatore C ++, l'acquisizione e la memorizzazione dell'indirizzo di *this durante l'esecuzione del costruttore è illegale, il che può infatti restituire indirizzi diversi durante e dopo l'esecuzione del costruttore.

Si noti che questo schema rompe diverse cose: Base non è banalmente copiabile, non banalmente mobile e non banalmente assegnabile. Se hai bisogno di queste cose devi implementare esplicitamente queste operazioni.

class Base
{
public:
    class Extra
    {
        friend class Base;

    public:
        // able to access Base members via m_refBase
        void extraMethod();

        // if Base is const-qualified, the Extra& that is 
        // returned from Base::extra() is also const-qualified,
        // therefore the const-qualified Extra::extraMethod() 
        // will be selected
        // (and non-const methods on Extra will not be callable) 
        void extraMethod() const;

    private:
        // constructor - to be called by Base constructor
        Extra(Base& refBase)
            : m_refBase(refBase)
        {
        }

    private:
        Base& m_refBase;
    };

    friend class Extra;

public:
    void baseMethod();

public:
    Extra& extra() { return m_extra; }
    const Extra& extra() const { return m_extra; }

public:
    Base()
        : m_someOtherData(...)
        , m_extra(*this)
    {
        m_someOtherData.otherInitializations();
    }

private:
    SomeOtherData m_someOtherData;
    Extra m_extra; 
};
    
risposta data 25.11.2016 - 16:57
fonte
1

Questo è abbastanza semplice, tutto considerato. Quello che devi fare è creare una classe (nidificata o meno) che è amica di quella principale. Verrà creato su richiesta quando si accede all'API "Avanzata". Il codice generale assomiglia a questo:

class Main
{
private:
    class Advanced
    {
    public:
        void AdvancedMethod() {/*directly access Main's internals*/}

    private:
        Advanced(Main &m) :data_(m) {}

        Main &data_;

        friend class Main;
    };

    friend class Advanced;
public:

    Advanced Adv() {return Advanced(*this);}
};

Tieni presente che creiamo un nuovo Advanced ogni volta che chiami Adv . Potremmo avere una variabile membro, ma questo ci permette di:

  • Non disturbare la dimensione / layout di Main . Advanced occupa un po 'di spazio, quindi aggiungendolo come variabile membro di Main , facciamo in modo che% co_de occupi più spazio.
  • Non disturbare la funzionalità di Main . Se abbiamo Main memorizzi un riferimento a Advanced , allora Main non sarebbe banalmente copia / mobile. E quindi, qualsiasi classe che lo memorizza come membro non sarebbe banalmente copia / mobile. Restituendo un oggetto costruito, consentiamo a Advanced di essere copiato / spostato in modo semplice indipendentemente da come viene implementato Main .

Certo, ci sono aspetti negativi:

  • Perdiamo Advanced -corretta. Nota come const non ha Adv versione. Questo perché in realtà non può avere una versione const . Dato che restituiamo un oggetto anziché un riferimento a uno, un utente può sempre farlo:

    Advanced var = some_main.Adv();
    

    Funzionerà anche se const è some_main . Se const era membro, il Advanced potrebbe propagarsi correttamente.

risposta data 25.11.2016 - 17:22
fonte
1

Non sono sicuro che sia il modo migliore di fare le cose, ma questo potrebbe essere un luogo in cui puoi utilizzare (o abusare, a seconda del tuo punto di vista) ereditarietà multipla:

class basic_button {
    virtual void basic_func1() = 0;
    virtual void basic_func2() = 0;
};

class graphic_button { 
    virtual void graphic_func1() = 0;
    virtual void graphic_func2() = 0;
};

class expert_knobs {
    virtual void expert_func1() = 0;
    virtual void expert_func2() = 0;
};

class button : public basic_button, public graphic_button, public expert_knobs {
// ...
};

In questo modo la classe del pulsante stesso è un oggetto semplice e diretto che abbiamo segmentato l'interfaccia in pezzi, e possiamo (per esempio) avere documentazione separata per basic_button , graphic_button e così via. Allo stesso tempo, l'oggetto reale che creiamo è solo un singolo oggetto semplice, quindi non è necessario cercare di coordinare la durata di più oggetti e tale da ottenere un comportamento corretto.

Allo stesso tempo, non posso fare a meno di pensare che l'idea di base qui sia ampiamente infranta. Questo mi sembra un problema di documentazione in gran parte, che dovrebbe essere affrontato attraverso una documentazione decente che ha solo una serie di sezioni:

Operazioni di base

documentation of basic operations here

Operazioni avanzate

documentation of advanced operations here

, ecc.

    
risposta data 25.11.2016 - 18:10
fonte
1

Utilizza la separazione delle interfacce per questo scopo

Crea classi di interfacce separate, una con funzionalità di base e amp; l'altro con funzionalità avanzate. Generalmente l'Avanzato potrebbe ereditare dall'interfaccia di base, tuttavia non è obbligatorio.

Implementa una classe concreta che eredita da questi due e implementa entrambe le interfacce. A seconda dell'interfaccia fornita, l'utente sarà limitato a una modalità specifica.

Come nel codice seguente, diverse interfacce offrono all'utente diversi segmenti della funzionalità.

#include <iostream>
using namespace std;

struct BasicMaths
{
    virtual int Sum(int a, int b) = 0;
    virtual int Difference(int a, int b) = 0;
    virtual int Product(int a, int b) = 0;
    virtual float Division(int a, int b) = 0;
};

struct TrigoMaths
{
    virtual double Sine(double angle) = 0;
    virtual double Cos(double angle) = 0;
    virtual double Tan(double angle) = 0;
};

struct AdvancedMaths: public BasicMaths, public TrigoMaths
{
};

class Math:public AdvancedMaths
{
public:

    int Sum(int a, int b)
    {return a+b;}
    int Difference(int a, int b)
    {return a-b;}
    int Product(int a, int b)
    {return a*b;}
    float Division(int a, int b)
    {return a/b;}
    double Sine(double angle)
    {return angle;}
    double Cos(double angle)
    {return angle;}
    double Tan(double angle)
    {return angle;}
};

int main()
{
    BasicMaths* bm = new Math();

    //// Basic Maths ///////////////////////////////////////////////////////
    //These will work fine
    cout << "Sum : " << bm->Sum(10, 5) << endl;
    cout << "Difference : " << bm->Difference(10,5) << endl;
    cout << "Product : " << bm->Product(10, 5) << endl;
    cout << "Division : " << bm->Division(10, 5) << endl;

    //These won't work as 'bm' doesn't have these interfaces
    //cout << "Sine : " << bm->Sine(90) << endl;
    //cout << "Cos : " << bm->Cos(90) << endl;
    //cout << "Tan : " << bm->Tan(90) << endl;


    ////Trigo Maths  ///////////////////////////////////////////////////////
    //These will work fine
    TrigoMaths* tm = dynamic_cast<TrigoMaths*>(bm);

    cout << "Sine : " << tm->Sine(90) << endl;
    cout << "Cos : " << tm->Cos(90) << endl;
    cout << "Tan : " << tm->Tan(90) << endl;

    //These won't work as 'tm' doesn't have these interfaces
    //cout << "Sum : " << tm->Sum(10, 5) << endl;
    //cout << "Difference : " << tm->Difference(10,5) << endl;
    //cout << "Product : " << tm->Product(10, 5) << endl;
    //cout << "Division : " << tm->Division(10, 5) << endl;


    //// Advanced Maths  ///////////////////////////////////////////////////////
    AdvancedMaths* am = dynamic_cast<AdvancedMaths*>(tm);

    //All interfaces work fine here
    cout << "Sum : " << am->Sum(10, 5) << endl;
    cout << "Sine : " << am->Sine(90) << endl;

    return 0;
}

. . .

    
risposta data 14.12.2016 - 15:49
fonte

Leggi altre domande sui tag