WORK-IN-PROGRESS: - this material is still under development
A list of tasks linked by dependency relationships. To run a task you invoke its dependencies, running those tasks if pre-requisites.
Building a software system is a common predicament for software developers. At various points there are various things that you want to do, you may want to just compile the program, or you may want to run tests. If you want to run tests you need to make sure your compilation is up to date first. In order to compile, you need to ensure you've carried out some code-generation.
A Dependency Network organizes functionality into a network of tasks and dependencies on other tasks. In the case above we would say that the test task is dependent upon the compilation task and the compilation task is dependent upon the code-generation task. When you request a task, you first carry out any dependent tasks and ensure they are executed first, if needed. You can navigate through a dependency network like this to ensure that all the pre-requisite tasks that are necessary for a requested task are executed. We can also make sure that even if a task crops up more than once through different dependency paths it's still only executed once.
In the opening example above, I described things in a task-oriented way: describing the network as a set of tasks with dependencies between them. An alternative way of doing this is a product-oriented style where we focus on the products we want to create and the dependencies between them. I'll illustrate the difference by considering the case where we carry out a build by doing some code generation and then compiling. In the task-oriented approach we would say that we have a code-generation task and a compilation task with the compilation task depending on the code-generation task. In the product-oriented style we would say we have an executable which is created by a compilation process, and some generated source files that are created by code-generation. We then state the dependencies by saying the code-generated source files are a pre-requisite to building the executable. The difference between these two may seem over-subtle at the moment, but hopefully will get clearer as I continue.
The way we start to run dependency network is by requesting that we either run a task (process-oriented) or build a product (product-oriented). We typically refer to this requested product or task as the target. The system then finds all the pre-requisites of the target and continues finding the pre-requisites of the pre-requisites of the... until it has a full list of all the transitive pre-requisites that need to be built. It invokes each task once using the the dependency relationships to ensure no task is invoked before its pre-requisites. An important property of this is that no task is executed more than once, even if traversing the network means you run into the same item several times.
To help talk about this, I'll introduce a slightly larger example that also allows me to get away from the ever-present example of software builds. Let's consider a production facility for magical potions. Each potion has ingredients which are substances that often need to be made themselves from other substances. So in order to create a health potion, we need clarified water and octopus essence (I'm ignoring quantities here). To create the octopus essence we need an octopus and clarified water. To create the clarified water we need dessicated glass. We can state the links between these products (I'm using a product-oriented approach here) as a series of dependencies.
In this case we want to ensure that the task to produce clarified water is only run once when we request a health potion - even though the there are multiple dependencies running into it.
It's often easy to think of physical things in this way, where the clarified water product is a something that fits in a metal bucket. The same notion however also makes sense for information products. In this case we could build a production plan that includes information about what's needed to produce each substance. In this case we don't want to produce a production plan for clarified water unless we need to - it's a lot of computing resources when you're running your programs on a hamster-powered auto-abacus.
With dependency networks there are two main errors that can come up. The most serious error is a missed pre-requisite - something we ought to build that we don't build. This is a serious error as it can result in an erroneous answer. It's also nasty because it can be hard to spot - everything looks like it works correctly but the data is all wrong because we didn't get a pre-requisite. The other error is an unnecessary build, such as calculating the production plan for clarified water twice. In most cases this only results in a slower execution than needed as often the tasks are idempotent. It can cause more serious errors if they aren't.
A common feature of a Dependency Network, particularly the product-oriented case, is that each product keeps a track of when it was last updated. This can further help to reduce unnecessary builds. When we request that a product be built it only actually executes the process if the output product's last modified date is earlier than any of the pre-requisites. For this to work the pre-requisites need to be invoked first so they can rebuild if necessary.
In order to talk about this I'm making a distinction here between invoking a task and executing it. Every transitive pre-requisite is invoked, but a pre-requisite is only executed if it's necessary. So if we invoke octopusEssence it invokes octopus and clarifiedWater (which itself invokes dessicatedGlass). Once all the invocations have finished then octopusEssence compares the last modified dates of the clarifiedWater and octopus production plans and only executes itself if either of those pre-requisites is later than octopusEssence's production plan's last modified date.
In a task-oriented network, we often don't use last-modified dates. Instead each task keeps track of whether it's already executed during this target request and only executes on the first invocation.
The fact that it's easier to work with persistent last-modified dates is a strong reason to prefer the product-oriented style to the task-oriented. You can use last-modified information in a task-oriented system, but to do this each task has to handle this responsibility itself. Using product-orientation with last-modified dates allows the network itself to decide on execution. This capability doesn't come for free, it only works if the output will always be the same if none of of pre-requisites change. Thus everything that could make a change to the output needs to be declared in pre-requisites.
The task/product distinction surfaces in build automation systems. The traditional Unix make command is product-oriented (where the products are files), while the java system, ant, is task oriented. One potential issue with product-oriented systems is that often there isn't always a natural product. Running tests is a good example of this. In which case you need to make something like a test report that keeps track of things. Sometimes the outputs are only there to fit in with the dependency system. A good example of such a pseudo-output is what's often referred to as a touch file - an empty file that's only there for its last-modified date.
[TBD: Need to consider how data-flow models fit here. Do I consider it as the same model (eager vs lazy) or a different model that also uses a similar structure?]
A Dependency Network matches problems where you can divide up the computation into tasks with well defined inputs and outputs. The ability of a Dependency Network to only execute tasks which are needed, makes it very suitable for resource-intensive tasks, or tasks which are an effort to get going - such as remote operations.
As with any alternative model like this, it's often tricky to debug when things go wrong and as a result it's usually important to log invocations and executions so you can see what's going on. Coupled with the desire to only execute when needed this leads me to a recommendation to favor relatively coarse grained tasks in the network.
Here I'll take the exercise of building a production plan for potions and explore some options for implementing it as an internal DSL in C#. I shall use a product-oriented approach, treating the sub-plan for a specific substance as the product. We'll assume that calculating each of these substance plan elements is computationally expensive, so we want to only calculate them when we need to. A real potion production system (for some value of real) needs lots of things, but for this example I'll just focus on one thing - the fastest time to produce the substance. You would use this to answer a question of how quickly you could product a particular potion from scratch if you dropped everything for a rush order.
As usual I'll start with the DSL for the potions.
class PotionScript...
public Substances octopusEssence, clarifiedWater, octopus, dessicatedGlass, healthPotion;
override protected void doBuild() {
healthPotion
.Needs(octopusEssence)
.Needs(clarifiedWater)
.Time(Always(3))
;
octopusEssence
.Needs(clarifiedWater, octopus)
.Time(Always(8))
;
clarifiedWater
.Needs(dessicatedGlass)
.Time(NewMoon(4).FullMoon(6).Otherwise(7));
;
octopus
.Time(Always(0))
;
dessicatedGlass
.Time(Always(0))
;
While a real system would have much more in it to describe the
potions, here I'm just showing what I need for the dependencies and
the way we calculate how long it takes to brew them. I indicate the
dependencies with the Needs clause and the how we figure
out the time to produce with the Time clause. It appears
there are many different factors that alter how long it takes to
produce a substance. For this example I'll just consider one: the
broad phase of the moon.
Introducing the phase of the moon into problem complicates the Dependency Network, as the plan for a particular substance depends not only on the pre-requisite substances but also upon elements in the environment. When it comes to figuring out the pre-requisites for the dependency analysis I need to take both factors into account.
The framework has two aspects woven together - the domain that describes production plans for substances and the Dependency Network.
[TBD: Talk about issues in more cleanly separating the two aspects]I'll start with the domain view of things. We have an overall production plan class that represents a single plan that we've made for assessing production. The plan consists of various elements, the ones we are interested in are those elements that represent the various substances that are used in the plan. I'll keep these in a dictionary indexed by the name of the substance.
class ProductionPlan...
private Dictionary<String, SubstanceElement> elements = new Dictionary<string, SubstanceElement>();
public void AddElement(SubstanceElement arg) {
elements[arg.Name] = arg;
}
The domain information that we're tracking here is the name of the substance, the necessary inputs for that substance, and the ability to calculate the fastest time for production.
class SubstanceElement...
public readonly String Name;
private readonly ProductionPlan plan;
public SubstanceElement(String name, ProductionPlan plan) {
this.Name = name;
this.plan = plan;
}
private List<SubstanceElement> inputs = new List<SubstanceElement>();
public void AddInputs(params SubstanceElement[] args) {
inputs.AddRange(args);
}
private TimeCalculator productionTime = new ConstantTimeCalculator(0);
public TimeCalculator ProductionTime {
get { return productionTime; }
set { productionTime = value; }
}
Notice I said the ability to calculate the fastest time. The element takes a time calculator that can figure out the fastest time. This is part of the computationally expensive calculation. I like to separate the data that gets calculated as part of this into a separate data class. This makes it clear what's calculated as part of that computation.
class SubstanceElement...
private SubstanceElementData data;
class SubstanceElementData...
public int FastestTime;
From the external view, however, I just make it a regular property of the substance element. There's no need for clients of this class to have to worry about the way things are calculated internally.
class SubstanceElement...
public int FastestTime { get { return data.FastestTime; } }
This plan data may need to be calculated when I invoke that element of the production plan, which will happen whenever it's requested.
class ProductionPlan...
public SubstanceElement Element(String key) {
var result = elements[key];
result.Invoke();
return result;
}
class SubstanceElement...
public void Invoke() {
log("Invoking");
foreach (SubstanceElement p in inputs) p.Invoke();
if (isOutOfDate) execute();
}
To build a substance element, I first request that all inputs are invoked and then I execute only if the substance element product is out of date.
Checking whether something is out of date involves using the last modified time. I need to execute if I've never executed or if any of the pre-requisites have been modified since I last executed.
class SubstanceElement...
private bool isOutOfDate {
get {
if (wasNeverCalculated) return true;
return Prerequisites.Any(e => e.ModifiedSince(lastModified)); }
}
private bool wasNeverCalculated {
get { return (null == data); }
}
private DateTime lastModified = DateTime.MinValue;
public bool ModifiedSince(DateTime arg) {
return lastModified > arg;
}
protected virtual void execute() {
log("execute");
data = new SubstanceElementData();
updateFastestTime();
lastModified = Clock.now;
}
When I do execute, I need to update the last modified date. The only part of the calculation I have here is to update the fastest time.
I calculate the fastest time by adding the time of this substance to the slowest of the input substances.
class SubstanceElement...
private void updateFastestTime() {
var prerequisiteTime = (0 == inputs.Count()) ? 0
: inputs.ConvertAll<int>(e => e.FastestTime).Max();
data.FastestTime = prerequisiteTime + ProductionTime.Value;
}
At this point I want to stress that this whole rigmarole is a pedagogical invention. If all I wanted to do was calculate a bunch of data like the fastest time, I wouldn't go to all the trouble of installing a Dependency Network. The cost of complex debugging just isn't worth it. It's only worth it if the overall calculation of the substance element data is awkward. I don't want to go into the complexity of putting such a real example together because the point of this example is to show the Dependency Network, not to show some complicated calculation that needs it. So we'll just assume the fiction of that hamster powered abacus doing the calculating and thus assume it's slow (and tiring for the hamster).
One of the complications of this setup is that as well as handling the input substances, I also have to provide environment information, such as the phase of the moon. In particular I have to ensure that any change to an environment value triggers recalculations in any substance element that uses it. To do this neatly I need to weave environment values into the dependency structure.
In order for the dependency structure to work, I only use two
operations on pre-requisites: Invoke and
ModifiedSince. I can formalize this by defining these in
an interface:
interface IDependencyNode {
bool ModifiedSince(DateTime arg);
void Invoke();
}
I can then make a tracked value class that implements that interface.
class TrackedValue<T> : IDependencyNode {
private T _value;
private static readonly DateTime INITIAL_TIME = DateTime.MinValue;
private DateTime lastModified = INITIAL_TIME;
public T Value {
get {
if (wasNeverSet) throw new InvalidOperationException("Value was never set");
return _value;
}
set {
lastModified = Clock.now;
_value = value;
}
}
public bool ModifiedSince(DateTime arg) {
return lastModified > arg;
}
public void Invoke() { } // no action as no dependencies
private bool wasNeverSet {
get { return lastModified == INITIAL_TIME;}
}
In order to preserve as much static type information as possible I'm using generics so that the tracked value has a static type.
I can then make an environment class which is a container of these tracked values. The most convenient way to access environment values like this would be to just make them properties of the environment class. However I also need to be able to use and pass around the names of these properties, for instance when I declare dependencies (as we'll see in a moment). If I want to refer to property names in .NET the only way is to use strings, but then I lose the advantages of static typing - in particular the IDE assistance which I want to retain. I can get around this by declaring the environment properties as a enum.
class Environment...
public enum Keys { PhaseOfMoon, MinimumTemperature };
If I mainly access the environment values through the keys, the natural route to go is to store them in a dictionary. However this doesn't allow me to statically type each value differently. By defining them as fields, I can type each one separately.
class Environment...
private TrackedValue<MoonPhases> PhaseOfMoon = new TrackedValue<MoonPhases>();
I can then provide a look-up function based on the key that uses reflection to get the correct property.
class Environment...
public T Value<T>(Keys key) {
return TrackedProperty<T>(key).Value;
}
public TrackedValue<T> TrackedProperty<T>(Keys key) {
FieldInfo field = GetField(key);
if (field == null) throw new InvalidOperationException(String.Format("No property for {0}", key));
TrackedValue<T> result = (TrackedValue<T>)field.GetValue(this);
return result;
}
public FieldInfo GetField(Keys key) {
return GetType().GetField(key.ToString(), BindingFlags.Instance | BindingFlags.NonPublic);
}
It feels like a rather roundabout way of doing it, in particular I have duplication between the enums and the field names. But it's necessary if I'm to treat both the properties and the names of properties as first class, statically typed objects.
The next part of the framework is the time calculators. The calculation method varies for each instance of potion. To handle this I'll use the strategy pattern. Here's the abstract superclass.[TBD: check terminology with GOF when I get home.]
abstract class TimeCalculator {
public readonly SubstanceElement substance;
protected TimeCalculator(SubstanceElement substance) {
this.substance = substance;
}
public abstract int Value { get; }
virtual public IEnumerable<Environment.Keys> EnvironmentReferences {
get { return new List<Environment.Keys>(); }
}
}
There are two methods on the abstract class, one to return the value and one to return a list of environment keys that the calculator needs to reference. I need to know the environment keys so they can be added to the dependency network. I'll show how we do that later on.
The simplest kind of time calculator just returns a constant time.
class ConstantTimeCalculator : TimeCalculator {
private int timeInHours;
public ConstantTimeCalculator(int time)
: base(null) {
this.timeInHours = time;
}
public override int Value {
get { return timeInHours; }
}
}
The more complicated example is the one that depends on the moon phase
class MoonPhaseCalculator : TimeCalculator {
public override IEnumerable<Environment.Keys> EnvironmentReferences {
get {
Environment.Keys[] result = {Environment.Keys.PhaseOfMoon} ;
return result;
}
}
private Dictionary<MoonPhases, int> times = new Dictionary<MoonPhases, int>();
public MoonPhaseCalculator(SubstanceElement substance, int newTime, int mid, int full)
: base(substance) {
times[MoonPhases.Full] = full;
times[MoonPhases.Mid] = mid;
times[MoonPhases.New] = newTime;
}
public override int Value {
get {
var value = substance.EnvironmentValue <MoonPhases>(Environment.Keys.PhaseOfMoon);
return times[value];
}
}
}
The moon phase calculator depends on the environment value for moon phase, so needs to return that key when asked for its references. In order to access the value, it goes via the substance element that it's part of, rather than accessing the environment object directly. I do this to avoid a missed pre-requisite error. By using the substance element as an intermediary I can check that any environment value requested of the substance is one that that substance has registered as one of its pre-requisites.
class SubstanceElement...
public T EnvironmentValue<T>(Environment.Keys key) {
if (!environmentReferences.Contains(key))
throw new InvalidOperationException(String.Format("{0} is not declared in {1}", key, this));
return plan.environment.Value<T>(key);
}
The error will be caught at run-time, which isn't ideal and it complicates the framework to put this check in. However it's a vital check as without it the system would give inaccurate data. Inaccurate data is always a bad thing, especially when our customers carry big swords.
This leads me to talk about how I determine the pre-requisites for a substance element. There are two kinds of pre-requisite that I need to look for - input substances and any environment references. I determine environment references by combining any direct references in the substance and any references used by the time calculator.
class SubstanceElement...
private IEnumerable<Environment.Keys> environmentReferences {
get {
return productionTime.EnvironmentReferences
.Concat<Environment.Keys>(storedEnvironmentReferences);
}
}
private List<Environment.Keys> storedEnvironmentReferences = new List<Environment.Keys>();
I can then determine the pre-requisites by combining those based on environment references with those based on inputs.
class SubstanceElement...
public IEnumerable<IDependencyNode> Prerequisites {
get {
return inputPrereqs
.Concat<IDependencyNode>(environmentPrereqs);
}
}
private List<IDependencyNode> inputPrereqs {
get {return inputs.ConvertAll<IDependencyNode>(e => e);}
}
private List<IDependencyNode> environmentPrereqs {
get {
return new List<Environment.Keys>(environmentReferences)
.ConvertAll<IDependencyNode>(e => plan.environment.TrackedValue(e));
}
}
This framework is a bit more complicated that I'd ideally like to have as a book example. The complication stems from the fact that I have two sources of dependencies (substance inputs and environment values), the awkwardness of setting up the environment values so we can check they are declared as a pre-requisite and do all this with static typing.
As it's been a while since we've seen it, here is the DSL fragment again
class PotionScript...
public Substances octopusEssence, clarifiedWater, octopus, dessicatedGlass, healthPotion;
override protected void doBuild() {
healthPotion
.Needs(octopusEssence)
.Needs(clarifiedWater)
.Time(Always(3))
;
octopusEssence
.Needs(clarifiedWater, octopus)
.Time(Always(8))
;
clarifiedWater
.Needs(dessicatedGlass)
.Time(NewMoon(4).FullMoon(6).Otherwise(7));
;
octopus
.Time(Always(0))
;
dessicatedGlass
.Time(Always(0))
;
The potion script uses Object Scoping and is a
subclass of ProductionPlanBuilder. The
Substances type defines a builder for each substance
element, using the potion scripts fields as a statically typed
Symbol Table. I initialize the symbol table in the
constructor for the production plan builder
class ProductionPlanBuilder...
public ProductionPlan Plan = new ProductionPlan();
public ProductionPlanBuilder() {
InitializeSubstanceBuilders();
}
private void InitializeSubstanceBuilders() {
foreach (var f in SubstanceFields)
f.SetValue(this, new Substances(f.Name, this));
}
private List<FieldInfo> SubstanceFields {
get {
var fields = this.GetType().GetFields(BindingFlags.Public | BindingFlags.Instance);
return Array.FindAll<FieldInfo>(fields, f => f.FieldType == typeof(Substances)).ToList();
}
}
The initialization routine looks for each field of type
Substances (ie is a substance element builder) and
initializes it with a new instance.
class Substances...
public String name { get { return this.SubstanceElement.Name; } }
public readonly SubstanceElement SubstanceElement;
public readonly ProductionPlanBuilder plan;
public Substances(String name, ProductionPlanBuilder plan) {
this.SubstanceElement = new SubstanceElement(name, plan.Plan);
this.plan = plan;
}
In most contexts I'd say "Substances" is a bad name for a class
like this - I'd prefer SubstanceElementBuilder. However
I'm using "Substances" here because that reads better in the DSL.
Once the production plan builder is created, building the Dependency Network is a two stage affair. First I run the DSL script then I add all the substance elements that were created into the underlying production plan.
class ProductionPlanBuilder...
public ProductionPlan Build() {
doBuild();
return BuildNetwork();
}
protected abstract void doBuild();
private ProductionPlan BuildNetwork() {
foreach (var r in SubstanceBuilders)
Plan.AddElement(r.SubstanceElement);
return Plan;
}
private List<Substances> SubstanceBuilders {
get {
return SubstanceFields.ConvertAll<Substances>(e => (Substances)e.GetValue(this));
}
}
Now I've gone though the overall build process it's time to look at
each element of the DSL. The Needs clause sets up input
dependencies for a substance.
class Substances...
public Substances Needs(params Substances[] prereqs) {
SubstanceElement.AddInputs(Array.ConvertAll<Substances, SubstanceElement>(
prereqs, e => e.SubstanceElement));
return this;
}
I've used a vararg parameter so I can use one Needs clause with multiple parameters or separate Needs clauses.
The Time clause is more complicated as I'm using nested function calls to set up the calculator. The nested methods resolve to the scope of the production plan builder. The simplest case is the constant time calculator.
class Substances...
public Substances Time(TimeCalculatorBuilder arg) {
SubstanceElement.ProductionTime = arg.Build(SubstanceElement);
return this;
}
class ProductionPlanBuilder...
protected TimeCalculatorBuilder Always(int arg) {
return new WrappingTimeCalculatorBuilder(new ConstantTimeCalculator(arg));
}
This is a bit more complicated than it would be if it were only this
simple case. I need always to return an Expression Builder for the time calculator, rather than the
time calculator itself. This is because in the moon phase case, I want
to use further method chaining to set up the moon phase calculator. So
for simple cases I make a time calculator Expression Builder that just wraps the underlying time
calculator.
abstract class TimeCalculatorBuilder {
abstract public TimeCalculator Build(SubstanceElement substanceElement);
}
class WrappingTimeCalculatorBuilder : TimeCalculatorBuilder {
private readonly TimeCalculator subject;
public WrappingTimeCalculatorBuilder(TimeCalculator subject) {
this.subject = subject;
}
public override TimeCalculator Build(SubstanceElement substanceElement) {
return subject;
}
}
The moon phase case shows why this indirection is useful. The production plan builder starts off the process with an inherited method that returns a builder for a moon phase calculator.
class ProductionPlanBuilder...
protected MoonPhaseCalculatorBuilder NewMoon(int arg) {
return new MoonPhaseCalculatorBuilder(arg);
}
The moon phase calculator builder gathers up the various values and
creates the correct calculator for the Substances.Time
method to add into the substance element.
class MoonPhaseCalculatorBuilder : TimeCalculatorBuilder {
private int newTime, fullTime, midTime;
public MoonPhaseCalculatorBuilder(int newTime) {
this.newTime = newTime;
}
public MoonPhaseCalculatorBuilder FullMoon(int arg) {
fullTime = arg;
return this;
}
public MoonPhaseCalculatorBuilder Otherwise(int arg) {
midTime = arg;
return this;
}
public override TimeCalculator Build(SubstanceElement substanceElement) {
return new MoonPhaseCalculator(substanceElement, newTime, midTime, fullTime);
}
}
In doing this I felt tempted to try and work it without making a specific builder for the time calculator. However my experience told me to just go ahead and create a builder as soon as I feel the need for it. Although setting up builders for simple cases, like the wrapping builder, is a bit involved it makes everything else much easier.
For the previous example I used objects to represent the time calculations. For this kind of problem you can also use closures. To illustrate this I'll use the same basic example and include some variations to include C#'s closure support.
Here's what the script for the potions I used before looks like using closures for the time calculations:
public Substances octopusEssence, clarifiedWater, octopus, dessicatedGlass, healthPotion;
override protected void doBuild() {
healthPotion
.Needs(octopusEssence)
.Needs(clarifiedWater)
.Time((env) => 5)
;
clarifiedWater
.Needs(dessicatedGlass)
.Uses(Environment.Keys.PhaseOfMoon)
.Time((env) => (env.PhaseOfMoon == MoonPhases.New) ? 4 : 7)
;
octopusEssence
.Needs(clarifiedWater)
.Needs(octopus)
.Time((env) => 8)
;
octopus
.Time((env) => 0)
;
dessicatedGlass
.Time((env) => 0)
;
The difference here is that the Time clause takes a
lambda instead of an expression to build a calculator. Sometimes I
need to pass in an environment, so I have to always pass one in whether or
not I need it.
For the cases where the time is constant, all I need is a simple expression that just returns that value. The clarified water uses the phase of the moon so here it's a simple boolean expression.
To change the previous example to use closures like this I needed two make two broad changes. Firstly I needed to replace time calculators with lambdas in the framework and builder. Secondly I needed to provide a more convenient way for the code in the closures to access values from the environment.
Modifying the previous code to use a closure affects both the framework and the builder. I'll start with the framework. Here the big change is changing what is stored in the substance element and how it's used.
To use a lambda here I need to define a delegate which defines the type information for the lambda. I then make a field and property for that type.
class SubstanceElement...
public delegate int TimeCalculation(EnvironmentReader substance);
private TimeCalculation productionTime = e => 0;
public TimeCalculation ProductionTime {
get { return productionTime; }
set { productionTime = value; }
}
I pass in an environment reader, a class I've added to make access to the environment values easier - I'll talk about that later. I initialize the field to return zero, as I did previously.
As before I use the calculator to update the time, here invoking it in the usual style for C# lambdas.
class SubstanceElement...
private void updateTime() {
var prerequisiteTime = (0 == _inputs.Count()) ? 0
: _inputs.ConvertAll<int>(e => e.FastestTime).Max();
data.FastestTime = prerequisiteTime
+ ProductionTime.Invoke(new EnvironmentReader(Environment, environmentReferences));
}
As I discussed in the example above, one of the wrinkles with
accessing values in the environment is that I want to check that I
only access environment values that I've declared that I use so that
the substance element data is recalculated when those values
change. This is even more important if I'm going to use closures to
put arbitrary code into the DSL scripts. So again I don't want the
closures to directly access the environment class - I need to add some
indirection so that I can check accesses. In the previous example I
passed in the substance to do this. The awkward thing here is that in
order to retain the type information, it's quite a convoluted
statement to access the environment value:
substance.EnvironmentValue
<Environment.MoonPhases>(Environment.Keys.PhaseOfMoon). This
is convoluted enough to write in regular code - it's far too messy to
write in DSL code. As a result I created a special environment reader
class that acts can both check accesses to the environment and also
provide a nicer interface to make the DSL more readable.
I create an environment reader with an environment object and a list of legal keys.
class EnvironmentReader...
private readonly Environment environment;
private readonly IEnumerable<Environment.Keys> allowedKeys;
public EnvironmentReader(Environment env, IEnumerable<Environment.Keys> allowedKeys) {
this.environment = env;
this.allowedKeys = allowedKeys;
}
Any access to a value can checks the access against the supplied list.
class EnvironmentReader...
public T Value<T>(Environment.Keys key) {
if (!allowedKeys.Contains(key))
throw new InvalidOperationException(String.Format("Undeclared key: {0}", key));
return environment.Value<T>(key);
}
I then add particular methods so that the code inside the closures can avoid the verbose value method
class EnvironmentReader...
public MoonPhases PhaseOfMoon {
get { return Value<MoonPhases>(Environment.Keys.PhaseOfMoon); }
}
So far I've shown using closures as an alternative to time calculator strategies. However there's no reason why you can't use both at the same time. This allows me to use subclass time calculators for the common cases, but use closures whenever I want a script that needs something that's more unique.
Here's an example where we might want such a more involved time clause.
newtsBreath
.Uses(Environment.Keys.MinimumTemperature, Environment.Keys.PhaseOfMoon)
.Time(env => {
if ((env.MinimumTemperature <= 5) && (env.PhaseOfMoon == MoonPhases.New))
return 7;
else
return 9;
})
;
This is a classic case of the situation where I need to express a more complex clause, but am reluctant to complicate the DSL in order to do it. Using a closure here enables me to deal with this, at the cost of introducing some raw C# into the DSL script.
To make this work, I need a new time calculator strategy that takes a suitable lambda.
class LambdaTimeCalculator : TimeCalculator {
public delegate int TimeCalculation(EnvironmentReader environment);
readonly public TimeCalculation calculation;
public LambdaTimeCalculator(SubstanceElement substance, TimeCalculation lambda)
: base(substance) {
this.calculation = lambda;
}
public override int Value {
get {
return calculation.Invoke(substance.EnvironmentReader);
}
}
}
class SubstanceElement...
public EnvironmentReader EnvironmentReader {
get { return new EnvironmentReader(plan.environment, environmentReferences); }
}
This strategy defines a delegate, stores an instance of the delegate in a field, and invokes it when asked for a value.
The builder works with this by having a second Time
clause, overloaded to take a suitable delegate.
class Substances...
public Substances Time(LambdaTimeCalculator.TimeCalculation func) {
SubstanceElement.ProductionTime = new LambdaTimeCalculator(SubstanceElement, func);
return this;
}
[TBD: Consider showing code for visualization.]