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
here is the code which uses that class
and finally, here is the output.
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)
In addition my code will find a class DomainClasses.Package1 and add an attribute
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
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
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
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.
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.
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.
Comments