How to Use Abstract Factory on Interacting Widgets

abstract-factorydesign-patternsobject-oriented

Context

In many GUI frameworks, it is very common that widgets are placed in a container to arrange them visually in a window. For example, in Gtkmm, a window with a label and a button (side by side) will look like this:

int main(int argc, char *argv[])
{
    auto app = Gtk::Application::create(argc, argv, "no.abstract.factory");

    // Create widgets to appear in the window:
    Gtk::Label label{"A label"};
    Gtk::Button button{"A button"};

    // Create a container to hold widgets and
    // add widgets inside the container at
    // specific locations. This gives control
    // on where widgets will be located in the
    // window when displayed to users.
    Gtk::Grid container;
    container.attach(label, 0, 0, 1, 1);
    container.attach(button, 1, 0, 1, 1);

    // Create the window and add the container
    // to it:
    Gtk::Window window;
    window.add(container);

    window.show_all();

    return app->run(window);
}

To avoid depending too much on the GUI framework, the abstract factory pattern could be used, as described in the book Design Patterns, by Gamma and al. Using this pattern, interfaces (or abstract classes) are created to represent the widgets and constructor calls are hidden behind factory:

IGUIFactory* factory = new GtkmmGUIFactory();  // Or QtGUIFactory() ...

// Later in the code (we now have no idea what is the
// underlying GUI framework) ...

ILabel* label = factory->CreateLabel();
IButton* button = factory->CreateButton();

// ...

With this approach, the application code does not depend on the GUI framework, except for the factory, which means there is minimal work to be done if another GUI framework is to later be adopted.

Question

In the Gtkmm example above not only are widgets created, but they are also added to a container for arrangement in a window. The container is then itself added to the window.

With the abstract factory patten approach, however, with interfaces hiding away all of the GUI framework's details, how can I actually arrange my widgets in the container? If I create an interface to abstract the container, The call to Gtk::Grid::attach will never be accessible because the widgets it takes as its first argument will be of the interface type, not the Gtkmm type. I could also use casting, but that would mean the framework's widget interfaces is derived from publicly and I want to avoid that.

So in this context, how can GUI elements interact with each other once they are created and the GUI handles are lost? For example, how could we re-write the Gtkmm example above but in an abstract way, so as to not depend too much on the framework?

Note: my example is in C++ because it is the language I will ultimately be using, but I don't mind if the answer is not, as long as the answer is clear.

Best Answer

You solve this with composition, not inheritance (you can even call it a "facade" if you want to). Excuse broken syntax, I haven't written C++ for a while...

class IGrid {
  public:
    void attachLabel(ILabel *label, <other parameters>) = 0;
}

class GtkmmGrid : public IGrid {
  public:
    GtkmmGrid(Gtk::Grid underlying) : _underlying(underlying) { }

    void attachLabel(ILabel *label, <other parameters>) {
      _underlying.attach(label, <other parameters>)
    }

  private:
    Gtk::Grid _underlying;
}

Next, add IGrid* createGrid() to your abstract factory, and you can now do:

IGrid *grid = factory->createGrid();
ILabel *label = factory->createLabel();

grid.attachLabel(grid, <other parameters>);

In theory, you are now insulated from changes to your GUI framework. In practice, this never happens - your other GUI framework might take different parameters on the attachLabel call, or in might not even have the concepts of grids and labels. This to me is a clear case of YAGNI - start worrying about this problem when you actually want to support a second GUI framework and know what the shared abstractions between the two frameworks are. At the moment, you risk writing a lot of code which doesn't actually bring you any value in the long run.

What is even more likely in my experience is that you never change to a second GUI framework.