Monday, May 27, 2013

Templates, Strategies, Factories and Dart

‹prev | My Chain | next›

I don't remember where I saw it, but I seem to recall that windowing systems are one of the few times in which regular inheritance actually works. Well, the dialog classes in the Dart version of the ICE Code Editor are kinda like a windowing system. Maybe regular inheritance will work with them...

All of the dialogs classes follow a similar structure. The constructor accepts an instance of the full-screen version of the editor and sets the same three instance variables. Each dialog has a menu item getter named el. This el is not the DOM element that contains the dialog, rather it is the menu item element that is inserted in the main menu. After that, there is a method that opens the dialog and one or more subsequent methods that perform an action for the dialog.

The OpenDialog class, which provides a dialog for opening existing projects, looks like:
class OpenDialog {
  var parent, ice, store;

  OpenDialog(Full full) {
    parent = full.el;
    ice = full.ice;
    store = full.store;
  }

  Element get el {
    return new Element.html('<li>Open</li>')
      ..onClick.listen((e)=> _hideMenu())
      ..onClick.listen((e)=> _openProjectsMenu());
  }

  _openProjectsMenu() { /* ... */ }

  _openProject(title) { /* ... */ }
}
OK, maybe this isn't the prototypical GUI windowing system, but it still seems worth exploring inheritance.

I can extract the common elements into a base class:
class Dialog {
  var parent, ice, store;

  Dialog(Full full) {
    parent = full.el;
    ice = full.ice;
    store = full.store;
  }

  Element get el {
    return new Element.html('<li>$menuLabel</li>')
      ..onClick.listen((e)=> _hideMenu())
      ..onClick.listen((e)=> open());
  }

  String get menuLabel;
  void open();
}
This forces me to define the menuLabel and open() methods in any of the sub-classes:
class OpenDialog extends Dialog {
  OpenDialog(full): super(full);

  get menuLabel => "Open";

  open() { /* ... */ }

  _openProject(title) { /* ... */ }
}
This saves me a bit of work and provides a nice template for all of the dialog classes in ICE. But the template pattern of inheritance is not the end-all be-all of object oriented programming.

One of the problems with this approach is that these dialogs are performing two functions: the menu item and the subsequent dialog that opens. An alternate approach would be to create a MenuItem class:
class MenuItem {
  var dialog;
  MenuItem(this.dialog);
  Element get el {
    return new Element.html('<li>${dialog.name}</li>')
      ..onClick.listen((e)=> _hideMenu())
      ..onClick.listen((e)=> dialog.open());
  }
}
A MenuItem is then solely responsible for serving as the thing that opens a dialog. The dialog, in turn, is then solely responsible for being a dialog:
class OpenDialog {
  String name = "Open";

  var parent, ice, store;
  OpenDialog(Full full) { /* ... */ }

  open() { /* ... */ }
  _openProject(title) { /* ... */ }
}
To put this all together, I have to inject the dialog into the menu item:
new MenuItem(new OpenDialog(this)).el;
This is a little more verbose than the original, combined menu item and dialog:
new OpenDialog(this).el;
Then again, this is the benefit of helper methods:
get _openDialog=> new MenuItem(new OpenDialog(this)).el;
This does have the advantage of being explicit about the menu item's strategy. In this case, it is perfectly clear that the strategy for this particular menu item is to create an open project dialog. I really like that approach.

That said, Dart affords me a few other options. I could add a factory constructor to the OpenDialog class:
class OpenDialog {
  String name = "Open";

  var parent, ice, store;
  OpenDialog(Full full) { /* ... */ }
  factory OpenDialog.menu_item(full) {
    var dialog = new OpenDialog(full);
    return new MenuItem(dialog);
  }

  open() { /* ... */ }
  _openProject(title) { /* ... */ }
}
This would make constructing objects a little more compact:
get _openDialog=> new OpenDialog.menu_item(this).el;
Now that I think about it, I could even return the DOM Element from a factory:
class OpenDialog {
  String name = "Open";

  var parent, ice, store;
  OpenDialog(Full full) { /* ... */ }
  factory OpenDialog.menu_element(full) {
    var dialog = new OpenDialog(full),
        menu_item = new MenuItem(dialog);
    return menu_item.el;
  }

  open() { /* ... */ }
  _openProject(title) { /* ... */ }
}
Then constructing a menu with a specific dialog strategy becomes:
get _openDialog=> new OpenDialog.menu_element(this);
I really like that. It is nice and compact. It conveys the meaning well. It also keeps the menu item and dialog responsibilities separate.

That said, there are drawbacks. If I opt for a Dialog template that really is just a dialog template, then I would need to define this factory constructor in each concrete class. I will sleep on the ultimate decision, but it is nice to have so many palatable options available to me.


Day #764

No comments:

Post a Comment