5
\$\begingroup\$

I have a project which should be thought of split into two layers: an IO layer which consists of sensors (input) and effectors (output), and a core layer which contains logic for determining which actions are desired from particular observations. The IO layer is dependent on the core layer, and the core layer is ignorant of the details of particular sensors and effectors.

The flow is as follows: the IO layer gets some sensory input, translates that into an observation that the core understands. The core uses its logic to generate an action from that, which is sent back to the IO layer and translated into some instruction to the effectors. So for example, a camera sensor might sense an object becoming larger. This would be translated into a ApproachingObjectObservation. The core layer would translate this to a AmbulatoryAction, which would be sent to the WheelsEffector.

The logic for determining which action needs to be taken based on each observation is in "Instruction" objects. The class in the core layer coordinating all of this is called Agent, and the corresponding class in the IO layer is Robot.

Here's the core code:

public class Agent
{
    private readonly InstructionCheckerStorer _instructionCheckerStorer;

    public Agent(InstructionCheckerStorer instructionCheckerStorer)
    {
        _instructionCheckerStorer = instructionCheckerStorer;
    }

    public void Observe<T>(T observation)
    {
        var checkers = _instructionCheckerStorer.GetCheckers<T>();
        foreach(var checker in checkers)
            checker.Check(observation);
    }
}

public class InstructionCheckerStorer
{
    private readonly Dictionary<Type, List<object>> _storage = new Dictionary<Type, List<object>>();

    public IEnumerable<IInstructionChecker<TObservation>> GetCheckers<TObservation>()
    {
        if (!_storage.ContainsKey(typeof (TObservation)))
            return new List<IInstructionChecker<TObservation>>();
        return _storage[typeof (TObservation)].Cast<IInstructionChecker<TObservation>>();
    }

    public void Add<TObservation>(IInstructionChecker<TObservation> checker)
    {
        List<object> checkers;
        if(!_storage.TryGetValue(typeof(TObservation), out checkers))
        {
            checkers = new List<object>();
            _storage[typeof (TObservation)] = checkers;
        }
        checkers.Add(checker);
    }
}

public interface IInstructionChecker<TObservation>
{
    void Check(TObservation observation);
}

public class InstructionChecker<TObservation, TAction> : IInstructionChecker<TObservation>
{
    private readonly IInstructable _instructable;
    private readonly IInstruction<TObservation, TAction> _instruction;

    public InstructionChecker(IInstructable instructable, IInstruction<TObservation, TAction> instruction)
    {
        _instructable = instructable;
        _instruction = instruction;
    }

    public void Check(TObservation observation)
    {
        TAction action;
        if(_instruction.Check(observation, out action))
            _instructable.DoAction(action);
    }
}

public interface IInstructable
{
    void DoAction<TAction>(TAction action);
}

public interface IInstruction<TObservation, TAction>
{
    bool Check(TObservation observation, out TAction action);
}

public interface IObservationTemplate<T>
{
    bool Matches(T observation);
}

public abstract class Instruction<TObservation, TAction> : IInstruction<TObservation, TAction>
{
    private readonly IObservationTemplate<TObservation> _observationTemplate;

    protected Instruction(IObservationTemplate<TObservation> observationTemplate)
    {
        _observationTemplate = observationTemplate;
    }

    public bool Check(TObservation observation, out TAction action)
    {
        action = default(TAction);
        if (!_observationTemplate.Matches(observation))
            return false;
        action = CreateAction(observation);
        return true;
    }

    protected abstract TAction CreateAction(TObservation observation);
}

This seems really excessive, and the reason is that it is trying to deal with a particular tricky situation. Both observations and actions contain a lot of varied information depending on what they are. For example, a MovingObjectObservation will contain parameters stating what the object is, the distance, its motion, possibly more. Similarly a MoveToPositionAction will state where to move to, and what speed. For that reason I can't just have Observation and Action classes, generics are required (unless I just shove the properties in a Dictionary<string, object> or something, but I've been trying to avoid that)

The Robot class in the IO layer would ideally want to do something like:

private void ProcessObservation<TObservation>(TObservation observation)
{
    var action = _agent.Observe(observation);
    DoAction(action);
}

The problem here is that the type of action would vary depending on the instructions, so the Robot class when making this call will not know what type of action it is going to get back. So instead, Robot implements IInstructable, and instead of the action being passed back as a return value, it's passed all the way through and it's the InstructionChecker that ends up calling that method.

As a side-note, the abstract Instruction.CreateAction is there because parameters from the observation may be required in the action. For example, if the observation is an approaching object, the resulting action would need to know the location of that object so that it can work out which direction to move away from it. I'm not completely sure an abstract method is how I want to deal with this, so don't worry too much about that in the review.

Is this a reasonable way of dealing with this tricky generic situation? Would it be better to hold my nose and do a bit of casting (or reflection), or is there an alternative design that I haven't thought of?

Note

To be clear, there isn't a one-to-one mapping between most of these different concepts. The same observation may come from multiple different sensors (you might either see or hear something approaching, for example). One sensor can produce multiple different observations. One type of observation may lead to multiple different types of action depending on the instruction logic, and multiple different types of observation may lead to the same type of action.

The only concept where there is a direct mapping is from action to the effector which effects it. A given effector may be able to effect multiple different types of action, but each action only has a single effector which is relevant to it.

\$\endgroup\$
  • \$\begingroup\$ How is the observation created for use in ProcessObservation? What is its type? Do you have an example of a semi-functioning Robot that you could post. \$\endgroup\$ – AlanT Jun 18 '14 at 7:51
  • \$\begingroup\$ Maybe I've missed something completely here, but couldn't sensors just publish Observations while effectors subscribe to them and take the appropriate actions? \$\endgroup\$ – George Howarth Jun 18 '14 at 13:39
  • \$\begingroup\$ @GeorgeHowarth Effectors don't know how to respond to an observation. They need to be instructed with an action, and it's the core layer that contains the logic (in the form of instructions) for determining which action should be taken given a particular observation \$\endgroup\$ – Ben Aaronson Jun 18 '14 at 14:56
4
\$\begingroup\$

From my understanding of the problem we have,

  • On the input side - a set of arbitrary observations. The data in each of which can/will be different.
  • On the output side - a set of possible actions that can be effected. The action is decided based upon the observations and the actions can be parameterised based upon the observations.

This solution seems to work

Marker interfaces just to make the generics a bit better. I am dubious of using generics without constraints.

public interface IObservation {
}

public interface IAction {
}

Here is how we match up the observations and the actions. Sorry about the name changes but I got a bit lost with the various different interfaces.

public interface IRule {
    Type ObservationType { get; }
    bool Match(IObservation observation, out IAction action);
}

public interface IRule<TObservation, TAction> : IRule where TObservation : IObservation where TAction :  IAction {
}

public abstract class Rule<TObservation, TAction> : IRule<TObservation, TAction> where TObservation : IObservation where TAction : IAction {

    public Type ObservationType {  get { return typeof(TObservation); } }

    public virtual bool Match(IObservation observation, out IAction action) {
        var ret = false;
        action = default(TAction);

        if (OnMatch((TObservation)observation)) {
            action = CreateAction((TObservation)observation);
            ret = true;
        }

        return ret;
    }

    protected abstract bool OnMatch(TObservation observation);
    protected abstract TAction CreateAction(TObservation observation);

}

The store for the rules. The important bit here is checking only against the rules for a given observation type else we get a cast error.

public class ThreeRingBinder {
    private readonly List<IRule> _rules;

    public ThreeRingBinder() {
        _rules = new List<IRule>();
    }

    public void RegisterRule(IRule rule) {
        _rules.Add(rule);
    }

    public IAction CheckForAction(IObservation observation) {

        IAction action = null;
        foreach(var rule in _rules.Where(r => r.ObservationType == observation.GetType())) {
            if (rule.Match(observation, out action)) {
                break;
            }
        }
        return action;
    }

}

I have some test scenarios. We have observations of moving and static objects and depending upon the observations we either laugh (at the puny missile), dodge out of the way or shoot the sitting duck.

public class ApproachingObjectObservation : IObservation {
    public int Size { get; set; }
    public int Speed { get; set; }
}
public class StaticObjectObservation : IObservation {
    public int Size { get; set; }
    public int Distance { get; set; }
}
public class LaughAction : IAction {
    public string HowLoud { get; set; }
}
public class DodgeAction : IAction {
    public int HowFar { get; set; }
}
public class ShootAction : IAction {
    public int Power { get; set; }
}

And the rules are

// If something is approaching then if it is small, we laugh at it (the smaller it is the louder we laugh)
//  if it is big, we dodge out of the way, the bigger it is, the farther we dodge

// If something is sitting there, we see if it is in range, if it is we shoot it, if not we ignore.
//   The bigger it is, the more power we hit it with.

// laugh at small missiles
public class ApproachingRuleOne : Rule<ApproachingObjectObservation, LaughAction> {

    protected override bool OnMatch(ApproachingObjectObservation observation) {
        return observation.Size < 100;
    }

    protected override LaughAction CreateAction(ApproachingObjectObservation observation) {
        var ret = new LaughAction();
        if (observation.Size < 10) {
            ret.HowLoud = "VERY";
        } else if (observation.Size < 50) {
            ret.HowLoud = "very";
        } else{
            ret.HowLoud = "a bit";
        }
        return ret;
    }
}

// dodge large ones
public class ApproachingRuleTwo : Rule<ApproachingObjectObservation, DodgeAction> {

    protected override bool OnMatch(ApproachingObjectObservation observation) {
        return observation.Size >= 100;
    }

    protected override DodgeAction CreateAction(ApproachingObjectObservation observation) {
        var ret = new DodgeAction();
        if (observation.Size >= 1000) {
            ret.HowFar = 1000;
        } else if (observation.Size >= 500) {
            ret.HowFar = 500;
        } else {
            ret.HowFar = 100;
        }
        return ret;
    }
}

// shoot (nearby) targets
public class LoiteringRule : Rule<StaticObjectObservation, ShootAction> {

    protected override bool OnMatch(StaticObjectObservation observation) {
        return observation.Distance < 100;
    }

    protected override ShootAction CreateAction(StaticObjectObservation observation) {
        var ret = new ShootAction();
        if (observation.Size >= 1000) {
            ret.Power = 1000;
        } else if (observation.Size >= 500) {
            ret.Power = 500;
        } else {
            ret.Power = 100;
        }
        return ret;
    }
}

I stuck them into some test cases and they seem to work well.

    [TestMethod]
    public void CheckRules() {
        var rules = new ThreeRingBinder();
        rules.RegisterRule(new ApproachingRuleOne());
        rules.RegisterRule(new ApproachingRuleTwo());
        rules.RegisterRule(new LoiteringRule());



        // if a small object is approaching, laugh
        IObservation ob = new ApproachingObjectObservation { Size = 1 };
        IAction act = rules.CheckForAction(ob);

        var laughAct = act as LaughAction;
        Assert.IsNotNull(laughAct);
        Assert.AreEqual("VERY", laughAct.HowLoud);


        // if a large object, dodge
        ob = new ApproachingObjectObservation { Size = 1000 };
        act = rules.CheckForAction(ob);

        var dodgeAct = act as DodgeAction;
        Assert.IsNotNull(dodgeAct);
        Assert.AreEqual(1000, dodgeAct.HowFar);


        // if a close object, shoot
        ob = new StaticObjectObservation {  Distance = 50, Size = 1000 };
        act = rules.CheckForAction(ob);

        var shootAct = act as ShootAction;
        Assert.IsNotNull(shootAct);
        Assert.AreEqual(1000, shootAct.Power);


        // if far away, ignore
        ob = new StaticObjectObservation { Distance = 500, Size = 1000 };
        act = rules.CheckForAction(ob);

        Assert.IsNull(act);
    }

To me it seems simpler, but I am not sure if it meets all the requirements. The two 'tricks' are IRule<T,U> inheriting from IRule so we can use them in the rules collection and the exposure of the ObservationType on the rules to allow filtering when matching.

\$\endgroup\$
  • \$\begingroup\$ This is a pretty convincing demonstration that a bit of well-placed casting is the best option here, and the thoroughness of the tests and example actions/observations is appreciated. The only piece still missing is how to get the action back to the effector (in this case speakers/gun/wheels) which can actually carry it out. \$\endgroup\$ – Ben Aaronson Jun 19 '14 at 15:43

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.