AJS Object User-Guide

Passing Arguments to Interceptees

Arguments can be passed to an intercepted function in the usual (non-intercepted) fashion, and these pass through the interception mechanism transparently, as long as any prefixes that are attached do not manipulate them in any way (see the points below about the arguments array).

Contents Passing Arguments to Interceptees
Interceptee Return-Values
Interceptees and Exceptions
Affixes and Exceptions
Interceptee Arguments and Affixes
Interceptee Return-Values and Suffixes
Argument Mutation/Injection

Example 13 demonstrates this.


 // -- Example 13 -------------------------------------------------------------

 var MyObj =
    {
    method_A : function (Value)
       {
       console.log ("method_A executed. Argument passed: " + Value);
       }

    };

 function prefixFunc () { console.log ("prefixFunc executed"); }

 AJS  .addPrefix (MyObj, "method_A", prefixFunc);

 MyObj.method_A  (42);

 -- Output --------------------------------------------------------------------

 prefixFunc executed
 method_A executed. Argument passed: 42
            

Interceptee Return-Values

Interceptees can return values to their callers in the same way as any other function. As with prefixes and interceptee-arguments, the value/reference that an interceptee returns passes back up through the interception mechanism transparently, as long as any suffixes that are attached do not manipulate it or return a value of their own (see below).

Example 14 illustrates this.


 // -- Example 14 -------------------------------------------------------------

 var MyObj =
    {
    method_A : function (Value)
       {
       console.log ("method_A executed");

       return 42;

       }

    };

 function prefixFunc () { console.log ("prefixFunc executed"); }

 AJS.addPrefix (MyObj, "method_A", prefixFunc);

 var Value = MyObj.method_A ();

 console.log ("Function returned. Value: " + Value);

 -- Output --------------------------------------------------------------------

 prefixFunc executed
 method_A executed
 Function returned. Value: 42
            

Interceptees and Exceptions

The interception mechanism is also transparent to the propagation of exception objects. This means that exceptions that arise from within the interceptee pass up the call chain to its callee, where they can be caught in the usual manner.

The diagram elaborates on this point.

Diagram showing execution path when an interceptee raises an exception.

Given this, Example 15 demonstrates catching an exception thrown from within an intercepted method, and shows that it is no different to exception handling in any other context.


 // -- Example 15 -------------------------------------------------------------

 function prefixFunc () { console.log ("prefixFunc executed"); }

 var MyObj =
    {
    method_A : function (Value)
       {
       console.log ("method_A executed, throwing exception...");

       throw new Error ("method_A Exception");

       }

    };

 AJS.addPrefix (MyObj, "method_A", prefixFunc);

 try { MyObj.method_A (); }
 catch (E)
    {
    console.log ("Exception caught. Message contents: " + E.message);
    }

 -- Output --------------------------------------------------------------------

 prefixFunc executed
 method_A executed, throwing exception...
 Exception caught. Message contents: method_A Exception
            

Note that, if a method has a suffix, and that method throws an exception, the suffix will not execute, as you would expect, and as Example 16 demonstrates.


 // -- Example 16 -------------------------------------------------------------

 function suffixFunc () { console.log ("suffixFunc executed"); }

 var MyObj =
    {
    method_A : function (Value)
       {
       console.log ("method_A executed, throwing exception...");

       throw new Error ("method_A Exception");

       }

    };

 AJS.addSuffix (MyObj, "method_A", suffixFunc);

 try { MyObj.method_A (); }
 catch (E)
    {
    console.log ("Exception caught. Message contents: " + E.message);
    }

 -- Output --------------------------------------------------------------------

 method_A executed, throwing exception...
 Exception caught. Message contents: method_A Exception;
            

Affixes and Exceptions

Exceptions that are thrown by prefixes and suffixes also propagate up to the interceptee's caller, and are not caught within the interception mechanism.

In Example 17, invocation of method_A causes prefixFunc to execute, which throws an exception. This escapes prefixFunc's scope, and is caught at the scope of the interceptee's caller, meaning that the interceptee never executes.

Note that if a suffix throws an exception then the interceptee is, by definition, guaranteed to have executed already and is therefore unaffected.


 // -- Example 17 -------------------------------------------------------------

 var MyObj =
    {
    method_A : function () { console.log ("method_A executed"); }
    };

 function prefixFunc ()
    {
    console.log     ("prefixFunc executed, throwing exception...");
    throw new Error ("prefixFunc Exception");
    }

 AJS.addPrefix (MyObj, "method_A", prefixFunc);

 try
    {
    MyObj.method_A ();
    }

 catch (E)
    {
    console.log ("Exception caught. Message contents: " + E.message);
    }

 -- Output --------------------------------------------------------------------

 prefixFunc executed, throwing exception...
 Exception caught. Message contents: prefixFunc Exception
            

Interceptee Arguments and Affixes

Any arguments passed in the call to the interceptee are also passed on to affix functions. These are passed as a single argument that represents the 'arguments' array that results from the call to the interceptee. Given that this is an array-like object (although not a true JavaScript array), affixes can use array syntax to access each of the elements, and Example 18 illustrates this idea.

This is clearly of use in design-by-contract programming, as it permits the enforcement of pre-conditions during a method call, and AJS_Validator exploits this principle to the full. See the relevant sections in the AJS_Validator user guide for more information.


 // -- Example 18 -------------------------------------------------------------

 function prefixFunc (IArgs)
    {
    console.log ("prefixFunc executed");

    console.log ("IArgs[0] = " + IArgs[0]);
    console.log ("IArgs[1] = " + IArgs[1]);
    console.log ("IArgs[2] = " + IArgs[2]);

    }

 var MyObj =
    {
    method_A : function (A, B, C)
       {
       console.log ("method_A executed");

       console.log ("A = " + A);
       console.log ("B = " + B);
       console.log ("C = " + C);

       }

    };

 AJS  .addPrefix (MyObj, "method_A", prefixFunc);

 MyObj.method_A  ("Fred", "Barney", "Wilma");

 -- Output --------------------------------------------------------------------

 prefixFunc executed
 IArgs[0] = Fred
 IArgs[1] = Barney
 IArgs[2] = Wilma
 method_A executed
 A = Fred
 B = Barney
 C = Wilma
            

As the first section above alludes, access to an interceptee's arguments also allows any prefixes it possesses to change their value before the interceptee ever sees them, and Example 19 illustrates this principle.


 // -- Example 19 -------------------------------------------------------------

 function prefixFunc (IArgs)
    {
    console.log ("prefixFunc executed");

    IArgs[0].Name_1 = "Fred";
    IArgs[0].Name_2 = "Barney";

    }

 var MyObj =
    {
    method_A : function (Names)
       {
       console.log ("method_A executed");

       console.log ("Names.Name_1: " + Names.Name_1);
       console.log ("Names.Name_2: " + Names.Name_2);

       }

    };

 var Names =
    {
    Name_1 : "Homer",
    Name_2 : "Marge"
    };

 AJS  .addPrefix (MyObj, "method_A", prefixFunc);

 MyObj.method_A  (Names);

 -- Output --------------------------------------------------------------------

 prefixFunc executed
 method_A executed
 Names.Name_1: Fred
 Names.Name_2: Barney
            

Further to this, it is possible for prefixes to add arguments to the call to the interceptee, which is to say that you can 'inject' additional arguments into a given method call (as opposed to mutating existing arguments, as explored above). However, given that the arguments array is not a true Array type, it does not support methods such as push and pop. It is necessary therefore to manipulate the length property explicitly in order to append extra arguments.

Example 20 demonstrates this process.

Note also that, while an interceptee's arguments are also passed to any suffixes that it may have, any operation that a given suffix performs on a given argument is of no consequence to the interceptee because it executes before the suffix. Nevertheless, this too is invaluable in using the AJS object to apply design-by-contract techniques, because it allows you to compare the value that a given method returns against the arguments that were passed in, or against a value from some predetermined schema, and this is explored in the next section.


 // -- Example 20 -------------------------------------------------------------

 function prefixFunc (IArgs)
    {
    console.log ("prefixFunc executed - adding argument...");

    IArgs[IArgs.length] = 3;
    IArgs.length++;

    }

 var MyObj =
    {
    method_A : function (A, B, C)
       {
       console.log ("method_A executed: " + A + " " + B + " " + C);
       }

    };

 AJS  .addPrefix (MyObj, "method_A", prefixFunc);

 MyObj.method_A  (1, 2);

 -- Output --------------------------------------------------------------------

 prefixFunc executed - adding argument...
 method_A executed: 1 2 3
            

Interceptee Return-Values and Suffixes

As with prefixes, suffixes are passed the interceptee's arguments array, but they are also passed the value that the interceptee returns.

This allows a suffix to compare what went into its interceptee with what came out, and this is clearly of use in testing a given method's functionality, and in policing conformity to post-conditions, which is also an element of the design-by-contract approach (and which AJS_Validator reifies in the form of postValidator functions).

Example 21 demonstrates this by testing the value returned from a function that is supposed to calculate the area described by the Width and Length parameters that it is passed, but which has a deliberate bug.


 // -- Example 22 -------------------------------------------------------------

 function suffixFunc (IArgs, IRtn)
    {
    console.log ("suffixFunc executed");

    if (IArgs[0] * IArgs[1] !== IRtn)
       {
       console.log ("calcArea is flawed!");
       }

    }

 var UsefulObj =
    {
    CalcArea : function (Width, Length)
      {
      return Width + Length;            // Wrong - it should be Width * Length.
      }

    };

 AJS      .addSuffix (UsefulObj, "CalcArea", suffixFunc);

 UsefulObj.CalcArea  (10, 20);

 -- Output --------------------------------------------------------------------

 suffixFunc executed
 calcArea is flawed!
            

Argument Mutation/Injection

As mentioned above, an important consequence of the fact that affixes are passed an interceptee's arguments and return-values (in the case of suffixes) is that they may mutate them, or even 'inject' entirely original values. This is a technique of considerable interest, and the section on preValidators and Argument Injection/Mutation in the AJS_Validator user guide explores in depth one particular and valuable application of this idea.

In a wider context, it could be applied to set arguments to critical values when debugging, thus yielding automated 'scenario repetition' – valuable when trying to track down a particularly elusive bug. You could also use it to provide alternative default values for a method's optional arguments, where the caller supplies no argument in a given place, but where the callee receives one nevertheless, as if it had been provided by the caller.

Additionally, you could manipulate the values of a method's arguments in order to 'normalise' them in some way, perhaps to reduce the number of decimal places if they are numeric, or to convert them to integers. Alternatively, one might wish to constrain the state space within which a method executes, or to ensure that a method never executes within a particular region of that space.

There is also the potential to inject functions as well as data. I.e. in the way that one passes a reference to a comparison-function to the sort method of the JavaScript Array-type, you could provide a call-back to an interceptee that replaces the actual call-back the the interceptee's caller provided, thus modifying that method's behaviour. This would yield a form of late-binding that was the very-latest possible in programming, because it would defer any decision as to which function to inject until after the method in question was called. The 'binding' simply cannot occur later than that because the next phase for the excution thread is entry into the callee itself.

In the light of these points, it would be possible to create an additional AJS-object client (to accompany AJS_Validator, AJS_Logger and AJS_ODL) that would exist solely to manipulate and/or inject arguments and return-values. Here, the equivalent of a Validation-/Log-/Load-Def would be an 'InjectionDef' – an object defining the argument(s) that must be injected into a given method, and/or the value that must be returned to the caller.

Such InjectionDefs could also possess 'preInjectors' and 'postInjectors', which would form analogues of pre-/post-Validators/Loggers (and which, surely, would take the prize for being the sexiest identifiers in all of programming). Indeed, such a client component may be added to AspectJS at some point with, most likely, the name 'AJS_Injector'.

Were this to happen, a ValidationDef would be provided too, which would use AJS_Validator to validate calls to the methods of AJS_Injector (as AJS_ODL.val.js and AJS_Logger.val.js do for AJS_ODL and AJS_Logger), an idea that underlines the high degree of modularity amd separation of concerns that is possible with AspectJS.

A little thought tells us also that the ability to control the execution order of affix functions enables us to use AJS_Validator with an argument-injecting prefix, such that the argument injector executes before the validation-prefix. This would yield the benefit of having AJS_Validator scrutinise injected argument-values before a given method executes to ensure that our injection scheme was not out of line with our design parameters. That is we could stop ourselves from injecting 'ridicoulous' or unreasonable values inadvertently.


 // -- Possible InjectionDef-Composition (pseudo code) ------------------------

    {
    MethodNames  : [  ...                   ],
    CallDef      : [ ArgDef_0, ... ArgDef_n ],
    RtnDef       : { },

    preInjector  : function (Args, ...)      { },
    postInjector : function (Args, ..., Rtn) { }

    }
            

 // -- Possible Arg-/Rtn-Def Composition (pseudo code) ------------------------

 Either:

   { }                                      // No action.

 Or:

   {
   Substitute : Num/Str/FuncRef/ObjRef Etc. // Set arg/rtn to Num/Str/FuncRef/ObjRef.
   Execute    : FuncRef                     // Set arg/rtn to value returned from calling Func-Ref (passing arg/rtn).
   Elide      : BooleanVal                  // If true, remove arg/rtn (i.e. set to undefined).

   Trunc      : NumberVal                   // Truncate string or array arg/rtn to length NumberVal.
   Extend     : { Value : V, Len : N }      // Pad string or array arg/rtn with N instances of V.
   SubSet     : { Start : S, End : E }      // Set arg/rtn to sub-set of string- or array-arg/rtn from S to E.

   AddProp    : { Prop  : Val        }      // Add 'Prop : Val' property to Object arg/rtn.

   Round      : BooleanVal                  // If true, round numeric arg/rtn up or down.
   Abs        : BooleanVal                  // If true, set numeric arg/rtn to absolute value of arg/rtn.
   Mult       : NumberVal                   // Set arg/rtn to product of arg/rtn times NumberVal.
   Div        : NumberVal                   // Set arg/rtn to quotient of numeric arg/rtn divided by NumberVal.
   Add        : NumberVal                   // Set arg/rtn to sum of arg/rtn plus NumberVal.
   Sub        : NumberVal                   // Set arg/rtn to sum of arg/rtn minus NumberVal.
   Mod        : NumberVal                   // Set numeric arg/rtn to modulus of NumberVal.

   Ceil       : NumberVal                   // Set arg/rtn to smallest integer >= NumberVal.
   Floor      : NumberVal                   // Set arg/rtn to largest integer <= NumberVal.

   Max        : NumberVal                   // Set arg/return to NumberVal if greater than NumberVal.
   Min        : NumberVal                   // Set arg/return to NumberVal if less than NumberVal.

   AND        : BooleanVal                  // Set arg/rtn to BooleanVal logical-AND boolean arg/rtn.
   OR         : BooleanVal                  // Set arg/rtn to BooleanVal logical-OR  boolean arg/rtn.
   XOR        : BooleanVal                  // Set arg/rtn to BooleanVal logical-XOR boolean arg/rtn.

   Escape     : BooleanVal                  // If true, set string arg/rtn to escaped equivalent of arg/rtn.
   Unescape   : BooleanVal                  // If true, set string arg/rtn to unescaped equivalent of arg/rtn.

   Replace    : RegExVal                    // Replace character sequence in string arg/rtn according to RegExVal.
   Extract    : RegExVal                    // Replace arg/rtn with sequence extracted from arg/rtn using RegExVal.

   }