An Example of Data-driven Programming using Common Lisp

by Jason Kantz


Data-driven programming is an implementation technique for making modular distinctions between program data and the procedures that operate on that data. The goal is to design programs where one can make customizations through additions and changes to tables of data, not the program.   Consider the simple problem of designing a menu with sections that expand and collapse, where the currently selected item is displayed in bold.  

 

The menu will be used by various users, and not all users should have access to all commands on the menu. Users are organized into groups (super-user, administrator, user) and access to each menu item is specified by a user group. So a function that displays the menu would need to know the applicable group and the selected menu-item and decide whether to display it or not.

A totally code-driven design would approach this problem with a tangle of conditional print statements.

(defun print-menu (group selected)
  (cond
   ;; SYSTEM not selected
   ((and (has-permission group 'system)
         (not (eql selected 'system)))
    (print "SYSTEM" stream))
   ;; SYSTEM is selected
   ((and (has-permission group 'system)
         (eql selected 'system))
    (print "<b>SYSTEM</b>" stream)
    ;; sub-items of SYSTEM
    (print "<blockquote>" stream)
    (cond
     ;; START-UP not selected
     ((and (has-permission group 'start-up)
           (not (eql selected 'start-up)))
      (print "START-UP" stream))
     ;; START-up is selected
     ((and (has-permission group 'start-up)
           (eql selected 'start-up))
      (print "<b>START-UP</b>" stream)
      ... and so on)))
   ;; USER not selected
   ((and (has-permission group 'user)
         (not (eql selected 'user)))
    (print "USER" stream))
        ... and so on))

A data-driven design separates the commands in the menu, from the structure of the menu, from the procedure of displaying the menu. Separating the commands from the code involves choosing a data structure to store the commands and their attributes. The data structure below is just an example. One could just as well put this information in a text file or a database.

(defparameter *menu-items*
  '((system    allow (super))
    (start-up  allow (super))
    (shut-down allow (super))
    (reset     allow (super))
    (user      allow (super admin user))
    (search    allow (super admin user))
    (by-name   allow (super admin user))
    (by-address allow (super admin user))
    (by-id     allow (super admin user))
    (log       allow (super admin user))
    (open      allow (super admin user))
    (admin     allow (super admin))
    (clean-up  allow (super admin))
    (rotate-logs allow (super admin))
    (rotate-user-logs allow (super admin))
    (rotate-system-logs allow (super admin))
    (view-status allow (super admin))))
(defun has-permission (group menu-item)
  (member group (getf (cdr (assoc menu-item *menu-items*)) 'allow)))

The layout of a menu can be also be represented with a data structure. Below is a list of menu items. A symbol in a list is the name of a command. When a list occurs within the list, the first item is the heading, and the rest of the list makes up another list of menu items below the heading.

(defparameter *menu-structure*
  '((system
     start-up
     shut-down
     reset)
    (user
     (search
      by-name
      by-address
      by-id)
     log
     open)
    (admin
     clean-up 
     (rotate-logs 
      rotate-user-logs
      rotate-system-logs)
     view-stats)))

Display functions can now be written to use the command table and the menu structure to render the appropriate menu. We can walk the menu structure, which contains the names of the commands, and display the menu hierarchy. The test for permission is performed by a table look-up using has-permission. Since this is just a toy example, the actual commands are left out. However just as has-permission uses a table-lookup, one could also look up an associated command or url in the table.

(defun print-menu (stream group selected)
  (when (has-permission group selected)
    (html-print stream *menu-structure* group selected)))

(defun html-print (stream structure group selected)
  (dolist (node structure)
    (html-do-node node group selected stream)))

(defun html-do-node (node group selected stream)
  (cond ((null node))
        ((atom node) 
         (when (has-permission group node)
           (when (eql node selected) (princ "<b>" stream))
           (princ node stream)
           (when (eql node selected) (princ "</b>" stream))
           (princ "<br>" stream)))
        ((listp node)
         (when (has-permission group (car node))
           (html-do-node (car node) group selected stream)
           (when (tree-find selected node)
             (princ "<blockquote>" stream)
             (html-print stream (cdr node) group selected)
             (princ "</blockquote>" stream))))))

With the data-driven design, the menu structure and command table can be added to and changed without modifying any existing functions. When changing a code-driven design, there is the risk that an algorithm might be unintentionally changed, and the chances for unintended consequences are higher. Through this example, one can see that a data-driven design anticipates the need for changes by clearly separating data structures from algorithms. With such a design there is less chance of introducing new problems, provided the consistency and modularity of the data structures are understood and maintained.