0

Following the article Programmers Are People Too by Ken Arnold, I have been trying to implement the progressive disclosure pattern for an API.

Basically, the idea mentioned in the text is to break the API into categories and only present the user with what he or she needs. The rest is hidden, reducing complexity for someone using the API. In the text, he presents his idea with the JButton class and its 100+ methods:

[...] 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.

I have been trying to implement a minimal example of this using C++. So far, I have not gotten acceptable results. What I want is this behavior and simplicity:

First, the class containing only the basic and widely spread methods:

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...
};

Then, the class containing the advanced/expert stuff:

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

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

Ideally, this "API" could be used simply like this by the user:

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;
}

I have been looking around the web to find examples of this, but so far, nothing really interesting came up. I did find an example in Java from this library but I was not able to replicate it in C++.

Does anyone have any idea on how this could be done?

  • "Progressive disclosure" is a UI pattern, not an API pattern. Perhaps that's why you're having difficulty finding examples. Does db40 implement it as a product feature (in your opinion), or does its API implement it as a programming convenience? What you may be looking for is HATEOAS. – Robert Harvey Nov 25 '16 at 15:27
  • 3
    The linked article does talk about programming languages from UX point of view, i.e. seeing programmers as the users of programming languages. – rwong Nov 25 '16 at 15:39
  • In the article, it clearly says: "You see progressive disclosure fairly often in GUI designs, typically as an Advanced or Expert button. This might expose settings for Web proxies in a browser or a rarely needed configuration for adjusting color balances on a printer. We could do the same thing with APIs." – BobMorane Nov 25 '16 at 15:51
  • Keep in mind that there is a guideline that has higher priority than progressive disclosure, namely "Interface Segregation Principle" – rwong Nov 25 '16 at 16:00
1

The following is a code example that creates two "twin" objects where the lifetime is closely related to each other.

Technically, Extra is a member of Base, but Extra consists of nothing but a pointer (reference) to the instance of Base that contains it. Think of it as a child node that has a pointer to a parent node.

I have used this pattern with Visual C++ (in particular when programming in Component Object Model environments). However, I do not know whether this pattern is valid in C++ Standard.

I have heard that, in some C++ compiler environments, the taking and storing of the address of *this during constructor execution is illegal, which may in fact return different addresses during and after constructor execution.

Note that this pattern breaks several things: Base is not trivially-copyable, not trivially-moveable, and not trivially-assignable. If you need those things you have to explicitly implement these operations.

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; 
};
  • "If you need those things you have to explicitly implement these operations." You cannot "explicitly implement" trivial operations. The closest you can do is = default them, and even that will delete them in your case. – Nicol Bolas Nov 25 '16 at 16:00
  • @NicolBolas by "implement" I mean providing the non-trivial (or, user-defined) implementations for those operations, where operations are "copy, move and assign". The object would still be "non-trivially-X". – rwong Nov 25 '16 at 16:02
  • 1
    Since the Extra holds no state, it would be also possible to create a new Extra instance per call: Extra extra() { return Extra(*this); }. That actually removes one level of indirection. However, returning an Extra& makes it clear that the Extra does not have an independent lifetime and is only valid together with the Base object. – amon Nov 25 '16 at 16:23
  • I compiled this and it works. This solves it, thanks. I'm not sure if all that added complexity is worth it though... – BobMorane Nov 25 '16 at 16:37
1

This is fairly simple, all things considered. What you need to do is create a class (nested or not) which is a friend of the main one. It will be created on-demand when you access the "advanced" API. The overall code looks like this:

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);}
};

Note that we construct a new Advanced every time you call Adv. We could have it be a member variable, but this permits us to:

  • Not disturb the size/layout of Main. Advanced takes up some space, so by adding it as a member variable of Main, we make Main take up more space.
  • Not disturb the functionality of Main. If we have Advanced store a reference to Main, then Advanced would not be trivially copy/moveable. And therefore, any class that stores it as a member would not be trivially copy/moveable. By returning a constructed object, we allow Main to be trivially copy/moveable regardless of how Advanced is implemented.

Of course, there are downsides:

  • We lose const-correctness. Notice how Adv has no const version. That's because it really can't have a const version. Since we're returning an object rather than a reference to one, a user can always do this:

    Advanced var = some_main.Adv();
    

    This will work even if some_main is const. If Advanced were a member, then the const could propagate correctly.

  • This works if you include friend class Main in class Advanced. Thanks for the clarification! – BobMorane Nov 25 '16 at 16:48
1

I'm not sure it's the best way to do things, but this might be a place where you could use (or abuse, depending on your viewpoint) multiple inheritance:

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 {
// ...
};

This way the button class itself is a fairly simple, straightforward object we've segmented the interface into pieces, and we can (for example) have separate documentation for basic_button, graphic_button, and so on. At the same time, the actual object we create is just a single, simple object so we don't need to try to coordinate lifetimes of multiple objects and such to get correct behavior.

At the same time, I can't help thinking that the basic idea here is largely broken. This strikes me as largely a documentation problem, which should be dealt with via decent documentation that just has a number of sections:

Basic Operations

documentation of basic operations here

Advanced Operations

documentation of advanced operations here

etc.

  • I'm not sure I see how this can solve it... Doesn't the Button class get all methods from everyone (from inheritance)? As far as documentation is concerned, we could say the same about UX... – BobMorane Nov 25 '16 at 17:54
  • @BobMorane; there is some difficulty in deciding whether we've solved "it", when "it" has only been vaguely defined at best. As far as UI/UX goes, I think there's a fairly fundamental difference: it's a lot more reasonable to expect a programmer to have at least some minimal understanding of the system. – Jerry Coffin Nov 25 '16 at 18:07
  • "As far as UI/UX goes, I think there's a fairly fundamental difference: it's a lot more reasonable to expect a programmer to have at least some minimal understanding of the system." Yes, I agree. I meant my previous comment according to what is presented in the article. You are also right in saying that the expectations could be a bit more well-defined. This article is a bit frustrating in this sense and my question is only my understanding of it. Furthermore, literature on this topic is nearly impossible to find elsewhere. – BobMorane Nov 25 '16 at 18:16
  • This is the straightforward application of interface segregation principle (ISP), which is more important than progressive disclosure. The basic idea is that the client has to request the proper interface according to its needs, and then use the methods available on the interface. In other words, for ISP to work, the client will have to obey the tenets of ISP as well. – rwong Nov 25 '16 at 18:21
  • 1
    @rwong: yeah, I'm probably prejudiced on the subject, but I can't help thinking that if you followed other well known rules for decent design, most of the problem this is intended to cure just wouldn't have been there to start with. – Jerry Coffin Nov 25 '16 at 18:28
1

Use interface segregation for this purpose

Create separate interface classes, one with Basic functionality & the other with Advanced functionality. Generally the Advanced might inherit from Basic interface, not mandatory though.

Implement a concrete class which inherits from these two and implements both interfaces. Depending on which interface is provided, the user will be restricted to a specific mode.

As in the below code, different interfaces offer different segments of the functionality to the user.

#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;
}

. . .

  • Hi @BobMorane , I'm curious to know if this approach helps your goal, or were you looking for a different answer? – Ramakant Dec 20 '16 at 7:03

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.