Common Design Patterns

Adaptor

The concept interface can appear limited and may not fit well into existing Java interfaces. This can be resolved by creating a behaviour adaptor. Entities will implement all interfaces on each of its behaviours. By creating annotated abstract methods on the behaviour to store the property values, it can adapt these values to an established Java interface.

An example of this are the RDF collection behaviours, implementing java.util.List, described in the Section called RDF Collections.

Aspects

In AOP, cross-cutting-concerns are separated into aspects that contain a small amount of behaviour used throughout a domain model. These aspects are then weaved into the primary model at matching point-cuts. Elmo includes support for aspects as a single behaviour or interceptor class, which declare their own point-cuts in the role annotations, method declaration, and @intercepts annotation.

Chain of Responsibility

Behaviour methods are chained together if they have the same signature. The chain is broken when a method returns a non-null value, true, or a non-zero primitive. Chained methods with a void return type cannot break the chain until all behaviours in the chain are evaluated.

public class SupportAgentEmailReader {
    public boolean readEmail(Message message) {
        if (message.getToEmailAddress().equals("help@support.example.com")) {
            // process email here
            return true;
        } else {
            return false;
        }
    }
}
public abstract class PersonalEmailReader implements EmailUser {
    public boolean readEmail(Message message) {
        String un = getUserName();
        if (message.getToEmailAddress().equals(un + "@example.com")) {
            // process email here
           return true;
        } else {
           return false;
        }
    }
}

String agentType = "http://www.example.com/rdf/2008/model#SupportAgent";
String userType = "http://www.example.com/rdf/2008/model#User";
ElmoModule module = new ElmoModule();
module.addRole(SupportAgent.class, agentType);
module.addRole(SupportAgentEmailReader.class, agentType);
module.addRole(PersonalEmailReader.class, userType);
module.addRole(EmailUser.class, userType);
factory = new SesameManagerFactory(module);
manager = factory.createElmoManager();

QName id = new QName(NS, "E340076");
manager.designate(User.class, id);
manager.designate(SupportAgent.class, id);
EmailUser user = (EmailUser) manager.find(id);
user.setUserName("john");

Message message = manager.designate(Message.class);
message.setToEmailAddress("john@example.com");
if (user.readEmail(message)) {
    // email has been read
}

Cohesion

Typical OO design tends to implement communicational cohesion - that is, the methods are grouped together into a class because they operate on the same data. With Elmo the higher level of functional cohesion is achieved by separating the class even further into functional behaviours. Test driven development becomes much easier at this level of cohesion, making each unit test small and easily understood. This allows the design to meet the single responsibility principle, giving each behaviour class one, and only one, reason to change.

Context Specific Data

Elmo also supports context specific entity managers. We have already demonstrated how a entity can implement both a SalesRep and an Engineer, but what if someone is a SalesRep only within a specific context and not otherwise?

Entity managers can be created with multiple inclusive contexts. The ElmoModule class is optionally associated with a context, described as a QName. When these modules include other modules they also inherit a read-only view of the included contexts. New factories created against a module with a context will only read property values from that context and any of the included contexts. When a property is changed to a new value, the old value is removed only if it is in the primary context. When using contexts, each triple statement is stored as a 4-tuple with the primary context as the fourth value.

In the example below we can see that the employee E340076 is a SalesRep in the first period and an Engineer in the second with a different salary. Any manager with the common context will have E340076 as a employee named John, but only in period1 will John be a SalesRep with a salary of 90, and only in period2 will John be an Engineer with a salary of 100. In this way contexts can also be used to setup security domains around sensitive data.

QName c = new QName(NS, "Period-common");
QName p1 = new QName(NS, "Period-1");
QName p2 = new QName(NS, "Period-2");
ElmoModule module = new ElmoModule();
module.addRole(Employee.class);
module.addRole(SalesRep.class);
module.addRole(Engineer.class);
module.addRole(SalesRepBonusBehaviour.class);
module.addRole(EngineerBonusBehaviour.class);
module.setContext(c);
ElmoModule m1 = new ElmoModule().setContext(p1).includeModule(module);
ElmoModule m2 = new ElmoModule().setContext(p2).includeModule(module);
repository = new SailRepository(new MemoryStore());
repository.initialize();
factory = new SesameManagerFactory(module, repository);
factory1 = new SesameManagerFactory(m1, repository);
factory2 = new SesameManagerFactory(m2, repository);
common = factory.createElmoManager();
period1 = factory1.createElmoManager();
period2 = factory2.createElmoManager();

Employee emp;
Object obj;
QName id = new QName(NS, "E340076");

emp = common.designate(Employee.class, id);
emp.setName("John");

SalesRep slm = period1.designate(SalesRep.class, id);
slm.setTargetUnits(10);
slm.setUnitsSold(15);
slm.setSalary(90);

Engineer eng = period2.designate(Engineer.class, id);
eng.setBonusTargetMet(true);
eng.setSalary(100);

obj = common.find(id);
assertTrue(obj instanceof Employee);
assertFalse(obj instanceof SalesRep);
assertFalse(obj instanceof Engineer);
emp = (Employee) obj;
assertEquals("John", emp.getName());
assertEquals(0.0, emp.getSalary());

obj = period1.find(id);
assertTrue(obj instanceof Employee);
assertTrue(obj instanceof SalesRep);
assertFalse(obj instanceof Engineer);
emp = (Employee) obj;
assertEquals("John", emp.getName());
assertEquals(90.0, emp.getSalary());
assertEquals(6.75, emp.calculateExpectedBonus(0.05), 0);

obj = period2.find(id);
assertTrue(obj instanceof Employee);
assertFalse(obj instanceof SalesRep);
assertTrue(obj instanceof Engineer);
emp = (Employee) obj;
assertEquals("John", emp.getName());
assertEquals(100.0, emp.getSalary());
assertEquals(5, emp.calculateExpectedBonus(0.05), 0);

Data Localization

Elmo supports seamless data localization for String values. This is activated on concept properties with the @localized annotation. By setting the locale of the ElmoManager, the property values will be retrieved and stored for the closest language that matches the manager's locale.

factory = new SesameManagerFactory(new ElmoModule());
english = factory.createElmoManager(Locale.ENGLISH);
french = factory.createElmoManager(Locale.FRENCH);

QName id = new QName(NS, "D0264967");
document = (DcResource) english.find(id);
document.setTitle("Elmo User Guide");

document = (DcResource) french.find(id);
assert "Elmo User Guide".equals(document.getTitle());
document.setTitle("Elmo Guide de l'Utilisateur");
assert "Elmo Guide de l'Utilisateur".equals(document.getTitle());

english = factory.createElmoManager(Locale.ENGLISH);
document = (DcResource) english.find(id);
assert "Elmo User Guide".equals(document.getTitle());

Hyperslices

Hyperslices are used to capture multi-dimensional separation-of-concerns. They can be thought of as aspects or program-slices managed at a higher level of concern. A hyperslice is a complete object behaviour hierarchy intended to encapsulate concerns in dimensions other than the dominant one. The behaviour classes within a hyperslice are weaved using the annotations defined in the Section called Role Annotations.

Figure 3. [1]

Above we can see examples of three dimensional hyperslices. The top hyperslice shows an object hierarchy defining the salary range and adequate resources behaviours. These behaviours use polymorphism to extend or override the super-classes behaviour. The bottom left captures the complex concern of calculating bonuses. In this hyperslice, each department maintains its own calculation for assigning bonuses, which can differ within a department and by job. The hyperslice on the right shows how a third dimension could also be captured within this domain model.

Without hyperslicing, classes would have to be created for each combination of the dimensions (experience level, department, and job). For example SeniorAccountingManager, JuniorShippingEngineer, ..etc. The behaviours described above might potentially be scattered throughout the hierarchy. However, by using hyperslicing in Elmo, these individual concerns are separated into distinct manageable packages, simplified to only the relevant dimension, and weaved together as needed.

Dependency Injection

Not all behaviours are self sufficient; some need additional support from other objects or components. In the example below we can see the behaviour BankAccountSupport delegating the method calls to the injected BankAccountService.

@rdf("http://www.example.com/rdf/2008/model#BankAccount")
public interface BankAccount extends BankAccountBehaviour {
  @rdf("http://www.example.com/rdf/2008/model#accountNumber")
  public long getAccountNumber();
  public void setAccountNumber(long number);
}
public interface BankAccountBehaviour {
  public double getBalance();
  public void withdraw(double amount);
  public void deposit(double amount);
}
public class BankAccountSupport implements BankAccountBehaviour {
  private AccountReference account;
  private BankAccountService service;
  public void setAccountReference(AccountReference account) {
    this.account = account;
  }
  public void setBankAccountService(BankAccountService service) {
    this.service = service;
  }
  public double getBalance() {
    return service.getBalanceOfAccount(account);
  }
  public void withdraw(double amount) {
    service.withdrawFormAccount(account, amount);
  }
  public void deposit(double amount) {
    service.depositToAccount(account, amount);
  }
}
@factory
@rdf("http://www.example.com/rdf/2008/model#BankAccount")
public class BankAccountFactory {
  @factory
  public BankAccountBehaviour createBankAccountBehaviour(BankAccount account) {
    BankAccountSupport behaviour = new BankAccountSupport();
    long number = account.getAccountNumber();
    assert number > 0;
    BankAccountService service = BankAccountService.getInstance();
    behaviour.setAccountReference(service.getAccountReference(number));
    behaviour.setService(service);
    return behaviour;
  }
}

ElmoModule module = new ElmoModule();
module.addRole(BankAccount.class);
module.recordFactory(BankAccountFactory.class);
factory = new SesameManagerFactory(module);
manager = factory.createElmoManager();
long number = 10000001;
QName qname = new QName(NS, Long.toString(number));
BankAccount account = manager.designate(BankAccount.class, qname);
account.setAccountNumber(number);
double balance = account.getBalance(); // will call accountService

In the example above the BankAccount interface is the Elmo concept with the mapped concept property "accountNumber", and the BankAccountBehaviour interface is the behaviour interface of BankAccountSupport. This behaviour has the factory BankAccountFactory, which is responsible for creating the support class instances and injecting them with the BankAccountService instance.

Observer

When static events need to be triggered from methods called on a entity, such as their property values changing, an interceptor or behaviour should be created. Interceptors are described in the Section called Interceptors. For dynamic observers that are interested in property modification, the PropertyChangeNotifierSupport interceptor can be used by registering it with the subject types that need to be observed.

The PropertyChangeNotifierSupport interceptor implements the PropertyChangeNotifier interface, tracking PropertyChangeListeners (observers) and notifying them when a setter property method has been called on the entity or a Set has been modified. If the notifier repository is used and the manager is not in auto-flush mode when the property is modified, the entity will not inform its listeners until the connection's state has changed (rollback or commit), otherwise the listeners will be informed before the setter method returns. Only when the event is fired from within the setter method is the listener informed of the property and new value of the change. This behaviour is not active by default, and can be activated at run-time (shown below) or placed into a resource file "META-INF/org.openrdf.elmo.roles" as stated in the Section called Registering Concepts & Behaviours.

ElmoModule module = new ElmoModule();
module.addRole(PropertyChangeNotifierSupport.class,
        "http://www.example.com/rdf/2008/model#Employee");
repository = new SailRepository(new MemoryStore());
repository = new NotifyingRepositoryWrapper(repository, true);
repository.initialize();
factory = new SesameManagerFactory(module, repository);
manager = factory.createElmoManager();

QName id = new QName(NS, "E340076");
Employee employee = manager.designate(Employee.class, id);
TestListener listener = new TestListener();
((PropertyChangeNotifier) employee).addPropertyChangeListener(listener);

manager.setAutoFlush(false);
employee.setName("john");
assertFalse(listener.isUpdated());
manager.flush();
assertTrue(listener.isUpdated());

Rules

There are two ways to implement rules in Elmo. One way is to used goal driven, or backward chaining, approach. A virtual read-only property is created on an interface that is implemented by a behaviour rule. The rule is executed whenever the property is read and the state of the property is calculated and returned. By using Elmo's build-in method chaining, these rules can be executed in turn until the result is complete. A good example of this are validation rules. Take for example the following example, a rule that states a team must have a team lead before it can be given a project.

public abstract class TeamHasNoTeamLeadRule implements Team {
  public boolean isRejectingProjects() {
    return getTeamLead() == null;
  }
  public void collectReasonsForRejectingProjects(List<String> reasons) {
    if (getTeamLead() == null) {
      reasons.add("Team does not have a team lead.");
    }
  }
}
public abstract class TeamAlreadyHasProjectRule implements Team {
  public boolean isRejectingProjects() {
    return getProject() != null;
  }
  public void collectReasonsForRejectingProjects(List<String> reasons) {
    if (getProject() != null) {
      reasons.add("Team already has project.");
    }
  }
}

Another technique that is used is called data driven, or forward chaining. These types of rules have the advantage of persisting their state and can also be used in a filter of a query. Below is a behaviour that is triggered when the expenses change on a project, if the expenses are over budget the overbudget state is modified. This triggers the overbudget rules, which will alert the manager using the injected messaging service.

@rdf("http://www.example.com/rdf/2008/model#Project")
public interface Project {
  @rdf("http://www.example.com/rdf/2008/model#manager");
  public Manager getManager();
  public void setManager(Manager manager);
  @rdf("http://www.example.com/rdf/2008/model#expenses");
  public double getExpenses();
  public void setExpenses(double expenses);
  @rdf("http://www.example.com/rdf/2008/model#budget");
  public double getBudget();
  public void setBudget(double budget);
  @rdf("http://www.example.com/rdf/2008/model#overBudget");
  public boolean isOverBudget();
  public void setOverBudget(boolean over);
}
public abstract class ExpensesOverBudgetRule implements Project {
  public void setExpenses(double expenses) {
    if (expenses > getBudget()) {
      setOverBudget(true);
    } else if (isOverBudget()) {
      setOverBudget(false);
    }
  }
  public void setBudget(double budget) {
    if (budget < getExpenses()) {
      setOverBudget(true);
    } else if (isOverBudget()) {
      setOverBudget(false);
    }
  }
}
public class AlertManagerRule implements Project {
  public void setOverBudget(boolean over) {
    MessagingService messaging = MessagingService.getInstance();
    if (over) {
      messaging.alert(getManager(), "Project is over budget");
    } else {
      messaging.inform(getManager(), "Project is within budget");
    }
  }
}

Serialization

Entities are not serializable. They store all their properties in the repository and are just a facade to the underlying connection. To store and retrieve an Entity, the method getQName will return a QName (or null if an anonymous entity), which is serializable. This can later be used to access the entity with find(QName) method. By serializing the QName of the entity instead of the entity itself, the entity can be restored by another manager on another connection.

Below is an example of how to serialize and deserialize the entire repository. For more information about how to selectively export or to export in other formats like XML, please see the Sesame User Guide.

File file = new File("repository.rdf");
Writer writer = new FilteWriter(file);
manager.getConnection().exportStatements(new RDFXMLWriter(writer));
writer.close();

File file = new File("repository.rdf");
manager.getConnection().add(file, "", RDFFormat.RDFXML);

Strategy

Elmo dynamically includes appropriate behaviours for entities based on subject type mapping. In the example below, the calculateExpectedBonus method on Employee has two implementations, one for SalesRep and one for Engineer. Elmo will include the appropriate strategy (or behaviour) for the given subject.

public abstract class SalesRepBonusSupport implements SalesRep {
    public double calculateExpectedBonus(double percent) {
        int units = getUnitsSold();
        int target = getTargetUnits();
        if (units > target) {
            return percent * getSalary() * units / target;
        } else {
            return 0;
        }
     }
}
public abstract class EngineerBonusSupport implements Engineer {
    public double calculateExpectedBonus(double percent) {
        boolean target = isBonusTargetMet();
        if (target) {
            return percent * getSalery();
        } else {
            return 0;
        }
    }
}

ElmoModule module = new ElmoModule();
module.addRole(SalesRep.class);
module.addRole(Engineer.class);
module.addRole(SalesRepBonusSupport.class);
module.addRole(EngineerBonusSupport.class);
factory = new SesameManagerFactory(module);
manager = factory.createElmoManager();

QName id = new QName(NS, "E340076");
Engineer eng = manager.designate(Engineer.class, id);
eng.setBonusTargetMet(true);
eng.setSalery(100);

Employee employee = (Employee) manager.find(id);
double bonus = employee.calculateExpectedBonus(0.05);
assertEquals(5.0, bonus);

Notes

[1]

Hyperslices Example