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.
Comments