Doc Center‎ > ‎Samples‎ > ‎

Samples - Task

This sample demonstrates how to handle complex tree operations with simple logic. We define two entities: Project and Task. Projects contain Tasks, which, in turn, can contain other Tasks (thus forming a tree).

We have a created a simple web application to demonstrate this. It looks like this:


This application is available as a war file, which can be deployed to most modern containers (Tomcat, JBoss, Jetty, etc...), or as a Maven project. To run the Maven project, you can just unzip it, open a command line in the newly created directory, and execute mvn tomcat:run

In both cases, once the application is running, you can connect to it from your browser at http://localhost:8080/BusLogicSamplesTask

The application allows you to create, update and delete nodes in the tree (there is a right-click menu), including moving nodes around.


The data model behind this is fairly basic:


Requirements

Our requirements for this project are (seemingly) simple:
  1. A Project's totalHours is the sum of all the tasks it contains, recursively
  2. A Task's globalName attribute represents its position in the tree (e.g. 2.5.3)
  3. A Task can be moved from any position to any other position in any project
Now let's think about what it would take to code the business logic using normal coding techniques (regardless of language). Requirement 3 is of course the killer, because it means that requirements 1 and 2 have to be fulfilled no matter how Tasks are moved around. It's not that it's impossible, or even terribly difficult, but it clearly would take some thinking to get this right, a fair amount of code to implement, and probably quite a bit of debugging to get it right in all cases.

Fortunately, we're using declarative logic.

The logic

Requirement 1 is fairly simple: we define Project's totalHours as the sum of the tasks it contains:

@Sum("tasks.totalHours")
public void deriveTotalHours() {  }

However, the requirements specify that this should be the sum of all subtasks, recursively, and not just the top-level tasks. We therefore need to roll up the subtasks' hours. We do this by defining three attributes in Task:
  • numHours is the number of hours for the task itself
  • childrenHrs is the total number of hours in the child tasks
  • totalHours is the sum of numHours and childrenHrs

With this set up, we can then define two simple annotations to implement this logic for Task:

@Sum("children.totalHours")
public void deriveChildrenHrs() { }
@Formula("numHours + childrenHrs")
public void deriveTotalHours() { }

These three annotations takes care of requirement 1: the Project will now contain the total number of hours of all its subtasks, recursively.


Requirement 2 seems more complicated. We must find a way for each Task to somehow know its position in the tree, so it can determine its name. The easiest way to do this is to define a relationship among Tasks at the same level, effectively making them a linked list, e.g.:



We're also going to define two attributes for Task:
  • localName will hold the order of the Task within its containing parent, starting with 1
  • globalName will be the complete path of the Task (e.g. 1.2.2)

With this in place, this requirement can be satisfied with two simple logic methods for Task:

@Formula
public int deriveLocalName() {
  if (task.getPrevious() == null)
  return 1;
  return task.getPrevious().getLocalName() + 1;
}
@Formula
public String deriveGlobalName() {
  if (task.getParent() == null)
  return "" + task.getLocalName();
  return task.getParent().getGlobalName() + "." + task.getLocalName();
}

Let's look at this in a little more detail.
  • deriveLocalName is simply saying that, if a Task does not have another Task before it, then by definition its number is 1, otherwise it's the previous task's number plus one.
  • deriveGlobalName says that, if the Task is top-level (does not have a parent), then its globalName is simply its localName. Otherwise, the globalName is equal to the parent's globalName, with localName appended to it.

That takes care of requirement 2. Now on to the most difficult one...

Requirement 3 should cause a bit of wincing among all programmers. If a Task is moved, let's say from one Project to a different Project, there is quite a lot of work to do:
  • we have to recompute the moved Task's globalName to reflect its new position.
  • we may also have to recompute the globalName of a number of other Tasks in the original Project, if their position in the tree has been affected by the move.
  • finally, we have to recompute the totalHours for both Projects

Here's where declarative programming really shines: we're done! By declaring the five bits of logic above, we have taken care of the business logic for all use cases. We are literally done, because the logic will be automatically enforced by ABL, for all use cases.

Conclusion

I hope it's clear from this example that declarative logic can be a tremendous time-saver for a whole class of problems.

Comments