| EAA-dev Home |

WORK-IN-PROGRESS: - this material is still under development

Object Scoping

Put DSL code in a subclass of a class that provdes the DSL vocabulary.

Nested Function and (to an extent) Function Sequence help provide a nice DSL syntax, but in their basic forms they come with a serious cost in the form of global functions and (worse) global state.

Object Scoping alleviates these problems by using inheritance to provide functions that can be called simply yet within the context of a single object.

How it Works

One of the many useful properties of objects is that each object provides a contained scope for functions and data. Inheritance allows you to use this scope separately from where it's defined. A DSL can use this facility by defining DSL functions on a base class, and then allowing developers to write DSL programs in subclasses. The base class can also define fields to hold any parsing data that's required.

Using a base class like this is an obvious place for an Expression Builder. Clients then write DSL programs in a subclass of the Expression Builder. Using inheritance allows them to add additional DSL functions in the subclass or even to override base functions in the DSL if they need to.

Although inheritance is the most common mechanism to use for this kind of work, some languages provide other ways to use Object Scoping. A particularly useful example of this is Ruby which provides the facility take any program code and execute it within the context of a particular object (using the instance_eval method). This allows a DSL writer to write the DSL text without declaring any links to the base class that defines the language.

When to use it

Object Scoping solves the niggly problems of globalness within Nested Function and Function Sequence and as such is always worth considering. Using Object Scoping allows you have bare function calls in your DSL and have them resolve to instance methods on an object. Not just does this avoid messing with a global namespace, it also allows you to store parsing data in an Expression Builder. I find these advantages quite compelling, and thus would always suggest using Object Scoping if you can.

Sometimes, however, you can't. For a start you need to be using an object-oriented language to do this. I, of course, don't see that as problem since I prefer using OO languages anyway.

The more common issue is that Object Scoping puts constraints on where your DSL script can go. With the most common inheritance case it means you must put the DSL script within a method in a subclass of an Expression Builder. This isn't too much of problem for self-contained DSL scripts. Such scripts often sit in their own file and a well-separated from other code. In this case there is a little syntactic noise to set up the inheritance structure but it's not too obtrusive. (You can avoid even that syntactic noise with techniques like Ruby's instance_eval.) The real problem is with fragmentory DSLs, where using Object Scoping forces you into an inheritance relationship that may be awkward or even impossible.

Object Scoping is mostly an antidote to global functions, so it's worth remembering that the biggest problems of global functions come with modifying global data. A common case where you don't get this problem is when the global function just creates and returns a new object, such as Date.today(). Static methods, which are effectively global, can be very effective to return objects like this which can either be regular objects or Expression Builders. If you can arrange your bare functions to be like this, then there is much less need for Object Scoping.

Example: Security Codes (C#)

We have a building that all sorts of secret projects. As a result the building is divided into zones and each zone has security policies that govern what kinds of employees can enter a zone. As an employee approaches a door into the zone, the system checks the employee against the zone's policies and decides whether or not to admit her.

The DSL I'm going to build will support expressing rules like this:

    class MyZone : ZoneBuilder {
      protected override void doBuild() {
        Allow(
          Department("MF"),
          Until(2008, 10, 18));
        Refuse(Department("Finance"));
        Refuse(Department("Audit"));
        Allow(
          GradeAtLeast(Grade.Director),
          During(1100, 1500),
          Until(2008, 5, 1));
      }

Framework

The framework structure has a zone class with multiple admission rules. Each admission rule is either an allow rule (specifying conditions to let someone in) or a reject rule (which has conditions to refuse entry). The admission rule has a body (that we'll explore later) and the method to check is an employee can be admitted.

  abstract class AdmissionRule {
    protected RuleElement body;
    protected AdmissionRule(RuleElement body) {
      this.body = body;
    }
    public abstract AdmissionRuleResult CanAdmit(Employee e);
  }
  enum AdmissionRuleResult {ADMIT, REFUSE, NO_OPINION};

I handle the two kinds of admission rule through inheritance, each one provides an implementation of CanAdmit.

  class AllowRule : AdmissionRule {
    public AllowRule(RuleElement body) : base(body) {}
    public override AdmissionRuleResult CanAdmit(Employee e) {
      if (body.eval(e)) return AdmissionRuleResult.ADMIT;
      else return AdmissionRuleResult.NO_OPINION;
    }
  class RefusalRule : AdmissionRule {
    public RefusalRule(RuleElement body) : base(body) {}
    public override AdmissionRuleResult CanAdmit(Employee e) {
      if (body.eval(e)) return AdmissionRuleResult.REFUSE;
      else return AdmissionRuleResult.NO_OPINION;
    }

When asked to admit an employee, the zone class runs through the admission rules in order, seeing how they respond.

class Zone...
    private IList<AdmissionRule> rules = new List<AdmissionRule>();
    public void AddRule(AdmissionRule arg) {
      rules.Add(arg);
    }
    public bool WillAdmit(Employee e) {
      foreach (AdmissionRule rule in rules) {
        switch(rule.CanAdmit(e)) {
          case AdmissionRuleResult.ADMIT:
            return true;
          case AdmissionRuleResult.NO_OPINION:
            break;
          case AdmissionRuleResult.REFUSE:
            return false;
          default:
            throw new InvalidOperationException();
        }
      }
      return false;
    }

If none of the rules give an opinion, the method defaults to refusal (false).

The body of the admission rule is a composite structure of rule elements, essentially a Specification. The declared type is an interface

  internal interface RuleElement {
    bool eval(Employee emp);
  }

Various implementations and check various attributes of an employee. Here's a couple checking for grades and departments.

  internal class MinimumGradeExpr : RuleElement {
    private readonly Grade minimum;
    public MinimumGradeExpr(Grade minimum) {
      this.minimum = minimum;
    }
    public bool eval(Employee emp) {
      if (null == emp.Grade) return false;
      return emp.Grade.IsHigherOrEqualTo(minimum);
    }
  }

  internal class DepartmentExpr : RuleElement {
    private readonly string dept;
    public DepartmentExpr(string dept) {
      this.dept = dept;
    }
    public bool eval(Employee emp) {
      return emp.Department == dept;
    }
  }

I have a composite element so I can combine then into logical structures.

  class AndExpr : RuleElement {
    private readonly IList<RuleElement> elements;
    public AndExpr(params RuleElement[] elements) {
      foreach (RuleElement element in elements) {
        if (null == element) throw new ArgumentException("attempt to add null");
      }
      this.elements = elements;
    }
    public bool eval(Employee emp) {
      foreach (RuleElement element in elements)
        if (!element.eval(emp)) return false;
      return true;
    }

So if I want to admit someone who is a senior programmer in the K9 department, I can set up the zone like this.

      zone.AddRule(new AllowRule(
                     new AndExpr(
                       new MinimumGradeExpr(Grade.SeniorProgrammer), 
                       new DepartmentExpr("K9"))));

DSL

To use Object Scoping I create a builder superclass that I can inherit from to form the DSL. Here's the example subclass again that shows the kind of DSL I'm supporting.

    class MyZone : ZoneBuilder {
      protected override void doBuild() {
        Allow(
          Department("MF"),
          Until(2008, 10, 18));
        Refuse(Department("Finance"));
        Refuse(Department("Audit"));
        Allow(
          GradeAtLeast(Grade.Director),
          During(1100, 1500),
          Until(2008, 5, 1));
      }

This rule first allows anyone from the MF department in until a cut-off date next year. It then explicitly refuses access to anyone in the finance or audit departements (in separate refusal clauses), finally allowing any director in between fixed hours up to another cut-off date.

Although the underlying framework allows arbitrary boolean expressions, the DSL is simpler. Each admission rule is a conjuction (and) of it's clauses. Hence the fact that I need separate refuse statements for the two departments. If I put them in the same clause it would only refuse people who were in both departments.

Arbitrary boolean expressions are powerful, but often difficult for people, particularly non-nerds, to follow. So some form of simplified structure can be handy in a DSL.

The DSL is comprised of methods which are defined on the base builder class. This allows me to call them in the subclass without any qualification. The Allow method adds a new allow rule to the zone whose body is the conjunction of the method's arguments.

class ZoneBuilder...
    private Zone zone;
    public ZoneBuilder Allow(params RuleElement[] rules) {
      AndExpr expr = new AndExpr(rules);
      zone.AddRule(new AllowRule(expr));
      return this;
    }

I use a vararg method here as a Literal Collection Expression. Each argument is built up through further functions on the base builder.

class ZoneBuilder...
    internal RuleElement GradeAtLeast(Grade grade) {
      return new MinimumGradeExpr(grade);
    }
    internal RuleElement Department(String name) {
      return new DepartmentExpr(name);
    }

To add a new element to the system, I define a new expression for the framework and a function on the builder.

class ZoneBuilder...
    internal RuleElement Until(int year, int month, int day) {
      return new EndDateExpr(year, month, day);
    }
  internal class EndDateExpr : RuleElement {
    private readonly DateTime date;
    public EndDateExpr(int year, int month, int day) {
      date = new DateTime(year, month, day);
    }
    public bool eval(Employee emp) {
      return Clock.Date < date;
    }
  }

As well as adding rules for all users of a DSL, I can also extend the DSL for specific DSL programs. Let's imagine it's only my department that wants access restricted to certain hours. I can put that code directly in the subclass itself.

class DslTest...
      private RuleElement During(int begin, int end) {
        return new TimeOfDayExpr(begin, end);
      }

      private class TimeOfDayExpr : RuleElement {
        private readonly int begin, end;
        public TimeOfDayExpr(int begin, int end) {
          this.begin = begin;
          this.end = end;
        }
        public bool eval(Employee emp) {
          return (Clock.Time >= begin) && (Clock.Time <= end);
        }
      }

Object Scoping does help in reducing noise in the DSL, but one problem is that it does introduce noise in the code that declares the DSL class. The first two lines (and closing braces) are awkward noise. It could be a little worse. My natural way to use this class would be to pass the zone into the builder in the constructor, but that would force me to add a constructor declaration to the subclass. I avoided that by passing the zone with a separate method.

class ZoneBuilder...
    internal void Build(Zone zone) {
      this.zone = zone;
      doBuild();
    }
    protected abstract void doBuild();

I call it like this.

class DslTest...
      new MyZone().Build(zone);

It's a small thing, but saves me a bit of noise in the DSL text. These small things add up.

[TBD: Add a ruby example using instance_eval]

Significant Revisions

26 Aug 06: