2009-04-23

Prism AOP - 4

I’ve played a little more with Prism. I find it a little difficult to mentally code on two levels. Level one being the code I am writing for the aspect, and level two being the code I am writing which will executed by the target. Having said that, as soon as I ran my app and saw the output everything was worthwhile.

Here is my Person class

type
  [aspect: EcoAspects.BusinessClass(’DomainClasses.Package1’)]
  Person = public class
  private
    FFirstName: String;
    FLastName: String;
  protected
  public
    property FirstName : String read FFirstName write FFirstName;
    property LastName : String read FLastName write FLastName;
  end;


here is the code which uses that class

class method ConsoleApp.Main;
var
  P: Person;
begin
  P := new Person();
  for A in typeOf(Package1).GetCustomAttributes(true) do
    Console.WriteLine(a.ToString());
  
  P.FirstName := ’Peter’;
  P.LastName := ’Morris’;
  ShowGetValueByIndexResult(P as ILoopBack2, 0);
  ShowGetValueByIndexResult(P as ILoopBack2, 1);
  DoSetValueByIndex(P as ILoopBack2, 0, ’Hello’);
  DoSetValueByIndex(P as ILoopBack2, 1, ’There’);
  ShowGetValueByIndexResult(P as ILoopBack2, 0);
  ShowGetValueByIndexResult(P as ILoopBack2, 1);

  Console.ReadLine();
end;

class method ConsoleApp.ShowGetValueByIndexResult(Obj: ILoopBack2; I: Integer);
begin
  Console.WriteLine(Obj.GetValueByIndex(I).ToString());
end;

class method ConsoleApp.DoSetValueByIndex(Obj: ILoopBack2; I: Integer; Value: Object);
begin
  Obj.SetValueByIndex(I, Value);
end;


and finally, here is the output.

  Eco.UmlCodeAttributes.UmlMetaAttributeAttribute
  Peter
  Morris
  Hello
  There


Fantastic! Using a single line of code I am able to morph the Person class so that it acts as though I had written it like this (might not compile, I wrote this next source in notepad)

type
  Person = public class(Object, ILoopBack2)
  private
    FFirstName: String;
    FLastName: String;
  protected
    method GetValueByIndex(I: Integer): Object; virtual;
    method SetValueByIndex(I: Integer; Value: Object); virtual;
  public
    property FirstName : String read FFirstName write FFirstName;
    property LastName : String read FLastName write FLastName;
  end;

method Person.GetValueByIndex(I: Integer): Object;
begin
  case I of
    0: exit FirstName;
    1: exit LastName;
  end;
end;

method Person.SetValueByIndex(I: Integer; Value: Object): Object;
begin
  case I of
    0: FirstName := String(Value);
    1: LastName := String(Value);
  end;
end;


In addition my code will find a class DomainClasses.Package1 and add an attribute

type
  [Eco.UmlCodeAttributes.UmlMetaAttributeAttribute("ownedElement", typeof(Person)]
  Package1 = class
  end;


As you can see the implementation of the aspect "BusinessClass" is very specific to ECO and saves a lot of writing. What is good too is that I could easily remove the project’s reference to the BusinessClass aspect which generates ECO changes and replace it with a DLL which has a BusinessClass aspect for something else - maybe a blank one which does nothing so that you can use the same class definitions as data-transer-objects.

But my interest in this technology goes far beyond easily implementing an object relational mapper in an abstract way, supporting the ORM is only the first step towards achieving what I really want - archetypes. Once I have the experience to create all of the meta-information required for mapping I can start creating my models out of patterns. I do this a lot at the moment, but all manually. For example in one project I had to allow my Employee, Van, and VendingMachineColumn classes to all hold stock. Each of these would need to hold stock, record stock adjustments such a stock found/lost during stock checks, and also due to stock transfers.

It would be a bad design to descend all of these classes from a StockHolder class. Holding stock is something you DO, and not something you ARE, so inheritance here is wrong. What I would typically do here is

1: Create a StockHolder class which holds the stock + adjustment history.
2: Employee, Van, and VendingMachineColumn would all own an instance of StockHolder.
3: Each class would implement

public interface IStockHolder
{
  StockHolder GetStockHolder();
}


This is a one-way relationship, if I needed for example to find all stock holders with X amount of a certain stock item so that I could request a transfer this would not be sufficient. In which case I would introduce an abstract method to StockHolder

  object GetOwner();


Then I’d have a descendant of StockHolder for each owner. EmployeeStockHolder, VanStockHolder, VendingMachineColumnStockHolder; each would have an association back to their owning object (Employee, Van, VendingMachineColumn) which they would return by overriding GetOwner. Now this is not a lot of work, but it is repetitive. You see the pattern in the UML but do you instantly recognise it? Is it really self-descriptive?

My AOP goal is to be able to do something like this

type
  [aspect: BusinessClass(’MyNameSpace.Domain.MyPackageName’)]
  [aspect: StockHolder]
  Employee = class
  public
    ...
  end;

  [aspect: BusinessClass(’MyNameSpace.Domain.MyPackageName’)]
  [aspect: StockHolder]
  Van = class
  public
    ...
  end;

  [aspect: BusinessClass(’MyNameSpace.Domain.MyPackageName’)]
  [aspect: StockHolder]
  VendingMachineColumn = class
  public
    ...
  end;


The StockHolder aspect would create the descendant (TargetClassName)StockHolder with an association back to the target, and override GetOwner. The thing is, HOW the aspect is implemented is not relevant, all I am saying is "this holds stock". It is short, descriptive, and instantly understandable. It’s also only a few seconds of work to make a class hold stock.

type
  [aspect: BusinessClass(’MyNameSpace.Domain.MyPackageName’)]
  [aspect: StockHolder] //Holds stock
  [aspect: ContactDetailsHolder] //Has personal contact information
  [aspect: Auditable] //Can make employees subject to an internal audit
  [aspect: TaskAssignable] //Can assign tasks to this employee
  [aspect: CustomerRole] //Employees can purchase goods
  Employee = class
  public
    ...
  end;


That’s the kind of thing I’d like to end up with. Much more descriptive than UML I think :-)

Here is the code. It’s just proof of concept code at the moment. I’ve decided to prefix Type_ Method_ Property_ etc to the start of types, methods, and property definitions where they are referring to values from the model the aspect is being applied to; this was something I decided to do to help me to mentally split the "this code" scenario and "target code" scenario.


namespace EcoAspects;

interface

uses
  System.Collections.Generic,
  System.Linq,
  RemObjects.Oxygene.Cirrus,
  System.Text;

type
  BusinessClassAttribute = public class(System.Attribute, ITypeInterfaceDecorator)
  private
    FPackageName: String;
    method AddClassToPackage(Services: IServices; aType: ITypeDefinition);
    method AddILoopBack2Interface(Services: IServices; aType: ITypeDefinition);
    method AddILoopBack2GetValueByIndex(Services: IServices; aType: ITypeDefinition);
    method AddILoopBack2SetValueByIndex(Services: IServices; aType: ITypeDefinition);
    property PackageName: String read FPackageName;
  protected
  public
    constructor (PackageName: String);
    method HandleInterface(Services: IServices; aType: ITypeDefinition);
  end;

implementation

uses
  System.Windows.Forms,
  Eco.ObjectImplementation,
  RemObjects.Oxygene.Cirrus.Statements,
  RemObjects.Oxygene.Cirrus.Values;

constructor BusinessClassAttribute(PackageName: String);
begin
  FPackageName := PackageName;
end;

method BusinessClassAttribute.HandleInterface(Services: RemObjects.Oxygene.Cirrus.IServices; aType: RemObjects.Oxygene.Cirrus.ITypeDefinition);
begin
  AddClassToPackage(Services, aType);
  AddILoopBack2Interface(Services, aType);
end;

method BusinessClassAttribute.AddClassToPackage(Services: IServices; aType: ITypeDefinition);
var
  Type_PackageReference: ITypeReference;
  Type_PackageDefinition: ITypeDefinition;
  Type_UmlMetaAttributeAttribute: IAttributeDefinition;
begin
  Type_PackageReference := Services.FindType(PackageName);
  if (Type_PackageReference = nil) then
  begin
    Services.EmitError(’Package class not found: ’ + PackageName);
    exit;
  end;

  //If it is an ITypeDefinition that means it is declared as source in the current
  //binary and we can therefore modify it - so we can attach .NET attributes.
  //If it isn’t an ITypeDefinition but only an ITypeReference then it is immutible
  //and we cannot change it.
  Type_PackageDefinition := Type_PackageReference as ITypeDefinition;
  if (Type_PackageDefinition = nil) then
  begin
    Services.EmitError(’Package class cannot be modified, it is not part of the same project: ’ + PackageName);
    exit;
  end;

  Type_UmlMetaAttributeAttribute := Type_PackageDefinition.AddAttribute();
  Type_UmlMetaAttributeAttribute.Type := Services.FindType(’Eco.UmlCodeAttributes.UmlMetaAttributeAttribute’);
  Type_UmlMetaAttributeAttribute.AddParameter(’ownedElement’);

  //The value we use for Type must be a TypeOfValue based on aType.
  Type_UmlMetaAttributeAttribute.AddParameter(new TypeOfValue(aType));
end;

method BusinessClassAttribute.AddILoopBack2Interface(Services: IServices; aType: ITypeDefinition);
var
  Type_ILoopBack2: IType;
begin
  Type_ILoopBack2 := Services.FindType(’EcoSupport.ILoopBack2’);
  if (Type_ILoopBack2 = nil) then
  begin
    Services.EmitError(’EcoSupport.ILoopBack2 not found, are you missing an assembly reference?’);
    exit;
  end;

  aType.AddInterface(Type_ILoopBack2);
  AddILoopBack2GetValueByIndex(Services, aType);
  AddILoopBack2SetValueByIndex(Services, aType);
end;

method BusinessClassAttribute.AddILoopBack2GetValueByIndex(Services: IServices; aType: ITypeDefinition);
var
  Type_ILoopBack2: IType;
  Method_ILoopBack2_GetValueByIndex: IMethod;
  Method_GetValueByIndex: IMethodDefinition;
  Statement_CaseIndexOf: CaseStatement;
begin
  //Find ILoopBack2 and ILoopBack2.GetValueByIndex
  Type_ILoopBack2 := Services.FindType(’EcoSupport.ILoopBack2’);
  Method_ILoopBack2_GetValueByIndex := Type_ILoopBack2.GetMethods(’GetValueByIndex’)[0];
  
  //Implement GetValueByIndex on the target
  Method_GetValueByIndex := aType.AddMethod(’GetValueByIndex’, Services.GetType(’System.Object’), false);
  Method_GetValueByIndex.AddParameter(’I’, ParameterModifier.In, Services.GetType(’System.Int32’));
  Method_GetValueByIndex.Virtual := VirtualMode.Virtual;
  Method_GetValueByIndex.Visibility := Visibility.Protected;

  //Explicitly tie our GetValueByIndex to ILoopBack2.GetValueByIndex. This ensures they are linked
  //even though our method is protected. This hides the method when using code-completion on the
  //target (because it is protected), but exposes it via the interface - much cleaner!
  aType.AddImplements(Method_GetValueByIndex, Type_ILoopBack2, Method_ILoopBack2_GetValueByIndex);

  //Build case statement
  Statement_CaseIndexOf := new CaseStatement();
  Statement_CaseIndexOf.What := Method_GetValueByIndex.GetParameter(’I’);

  //As a CaseIndex for each property on the class
  var CaseIndex: Integer := 0;
  for PropertyIndex : Integer := 0 to aType.PropertyCount - 1 do
  begin
    var Prop : IProperty := aType.GetProperty(PropertyIndex);
    //Ignore properties which cannot be read
    //Ignore properties which take parameters (properties with indexers);
    if (Prop.ParameterCount = 0) and (Prop.ReadMethod <> nil) then
    begin
      //Create an expression which is basically Self.Property.Read
      var Property_Read : ProcValue := new ProcValue(new SelfValue(), Prop.ReadMethod);

      //Create an exit statement which is basically - exit Self.Property.Read
      //So that we exit the method, returning the result of reading the property value
      var Statement_Exit : ExitStatement := new ExitStatement(Property_Read);

      //Create the CaseIndex which consists merely of the Statement_Exit
      var CaseItem_Index : CaseItem := new CaseItem(Statement_Exit, CaseIndex);

      //Add the CaseIndex to the Statement_CaseIndexOf, and increment the case index
      Statement_CaseIndexOf.Items.Add(CaseItem_Index);
      CaseIndex := CaseIndex + 1;
    end;
  end;

  //Set the body of the GetValueByIndex method we created. Normally we can just write the exact
  //code we need between the begin/end identifiers, but in this case we have generated the statements
  //to execute dynamically, so we need to "unquote" them - which basically means "expand" or "compile".  
  Method_GetValueByIndex.SetBody(Services,
    method begin
      unquote(Statement_CaseIndexOf);
    end);
end;

//This method is very similar to GetValueByIndex, so I will only describe the setter
method BusinessClassAttribute.AddILoopBack2SetValueByIndex(Services: IServices; aType: ITypeDefinition);
var
  Type_ILoopBack2: IType;
  Method_ILoopBack2_SetValueByIndex: IMethod;
  Method_SetValueByIndex: IMethodDefinition;
  Statement_CaseIndexOf: CaseStatement;
begin
  Type_ILoopBack2 := Services.FindType(’EcoSupport.ILoopBack2’);
  Method_ILoopBack2_SetValueByIndex := Type_ILoopBack2.GetMethods(’SetValueByIndex’)[0];

  //SetValueByIndex
  Method_SetValueByIndex := aType.AddMethod(’SetValueByIndex’, nil, false);
  Method_SetValueByIndex.AddParameter(’I’, ParameterModifier.In, Services.GetType(’System.Int32’));
  Method_SetValueByIndex.AddParameter(’Value’, ParameterModifier.In, Services.GetType(’System.Object’));
  Method_SetValueByIndex.Virtual := VirtualMode.Virtual;
  Method_SetValueByIndex.Visibility := Visibility.Protected;

  //Make explicit interface
  aType.AddImplements(Method_SetValueByIndex, Type_ILoopBack2, Method_ILoopBack2_SetValueByIndex);

  //Build case statement
  Statement_CaseIndexOf := new CaseStatement();
  Statement_CaseIndexOf.What := Method_SetValueByIndex.GetParameter(’I’);

  var CaseIndex: Integer := 0;
  for PropertyIndex : Integer := 0 to aType.PropertyCount - 1 do
  begin
    var Prop : IProperty := aType.GetProperty(PropertyIndex);
    if (Prop.ParameterCount = 0) and (Prop.WriteMethod <> nil) then
    begin
      //Here we need 2 statements for every CaseItem. So we need a BeginStatement
      //which is basically a begin/end block
      var Statement_CaseItemBegin : BeginStatement := new BeginStatement();

      //Create an expression which is equivalent to Self.Property.Set(Value);
      var Property_Write : ProcValue := new ProcValue(new SelfValue(), Prop.WriteMethod, Method_SetValueByIndex.GetParameter(’Value’));

      //Create a statement based on this expression. We can use AssignementStatement without passing a value because we have
      //already specified the value in the previous expression.
      var Statement_SetPropertyValue : AssignmentStatement := new AssignmentStatement(Property_Write);

      //Add this assignment to the Begin/End block statement
      Statement_CaseItemBegin.Add(Statement_SetPropertyValue);

      //And add a plain "Exit" after it within the Begin/End block.
      var ExitMethod : ExitStatement := new ExitStatement();
      Statement_CaseItemBegin.Add(ExitMethod);

      //Craete a CaseItem for the current property index which will execute our Begin/End block.
      var CaseItem_Index : CaseItem := new CaseItem(Statement_CaseItemBegin, CaseIndex);

      //Add the Begin/End block statement to the Case statement.
      Statement_CaseIndexOf.Items.Add(CaseItem_Index);
      CaseIndex := CaseIndex + 1;
    end;
  end;

  //Set the "unquoted" statement block as the method’s body.  
  Method_SetValueByIndex.SetBody(Services,
    method begin
      unquote(Statement_CaseIndexOf);
    end);

end;

end.

No comments: