Here is finally the last part of this serie. I will now create the last layer of Mihai Corlan’s application replacement.
Just to remind you, in the 1st part, I built a prototype of Acceleo template to generate the Doctrine2 php entities from a UML model. In the 2nd part, I built an Acceleo template to generate the skeletons of the php services that use these entities. In the 3rd part, I used the 1st entity UML diagram to generate the actionscript Value Objects from another Acceleo template. This lead to a fully usable as3 code layer, no additionnal code was required in this layer… but the as3 Value Objects layer is actually usable only by the last layer that we are about to generate with a 4th template, the actionscript service wrappers.
The service model
The service model presented in the 2nd part for the php services has to be slightly updated. I actually added a return type to the methods that return an array of Value Objects (getStudents() for example, which returns an array of Students).
I decided to model the Value Objects (VOs) arrays by specifying the type of the array items (VOs) and a cardinality (* for multiple), indicating that more than 1 VO is returned. This is my choice and you could define a different rule as defining a new data type in your diagram and use it as a return type. This choice, of course, has consequences on how the template is defined.
The approach
In all tutorials about remoting in Flex, you can see <mx:RemoteObject> tags embeded in an application. This is very convenient but not that convenient when you are in code generation process and I wanted to generate my RemoteObjects in as3 separated classes instead of mxml. After a few tries, I also decided to define a base RemoteObject class to be extended by other service wrappers. Here is the class:
package org.corlan.services { import mx.collections.ArrayCollection; import mx.messaging.Channel; import mx.messaging.ChannelSet; import mx.messaging.channels.AMFChannel; import mx.rpc.AbstractOperation; import mx.rpc.AsyncToken; import mx.rpc.remoting.RemoteObject; /** * */ public class BaseServiceWrapper extends RemoteObject { private const SERVER_NAME:String = 'localhost'; private const APP_PATH:String = 'Student_Flex_Php_Acceleo'; private const GATEWAY_FILE:String = 'gateway.php'; /** * CONSTRUCTOR */ public function BaseServiceWrapper() { showBusyCursor = true; endpoint = 'http://' + SERVER_NAME + '/' + APP_PATH + '/' + GATEWAY_FILE; convertResultHandler = resultHandler; // Define the channelset of amf communication var internal_channelSet:ChannelSet = new ChannelSet(); internal_channelSet.addChannel(new AMFChannel('zendamf', endpoint)) channelSet = internal_channelSet; } private function resultHandler( result:*, op:AbstractOperation):* { if (result is Array) { var temp:Array = []; var l:int = result.length; // parse the result array and convert each item to the type set on the operation for (var i:int = 0; i < l; i++) temp[i] = new op.resultElementType( result[i] ); result = new ArrayCollection(temp); } return result; } } } |
The class define the common properties of the RemoteObjects to connect to the php services using through an AMF channel. It also defines the convertResultHandler property for all the children classes. So if you need to change this function, you make it in only one place. You also need to update the private constant with your own environment parameters.
Let’s go now on the Acceleo template.
Defining the as3 service template
Create a new Module file, name it ‘generateAS2Services‘ for example and register it in the main Module file for as3 code generation.
Copy/paste the following code:
[comment encoding = UTF-8 /] [module generateAS3Services('http://www.eclipse.org/uml2/3.0.0/UML')/] [template public generateAS3Services(c : Class) ? (c.getAppliedStereotype('LocalProfile::Service')<>null)] [comment @main /] [file ('/org/corlan/' + c._package.name + '/' + c.name + '.as', false, 'UTF-8')] package org.corlan.[c._package.name/] { import mx.rpc.AbstractOperation; import mx.rpc.remoting.Operation; import mx.rpc.AsyncToken; import org.corlan.entities.*; /** * [c.ownedComment._body/] */ public class [c.name/] extends BaseServiceWrapper { /* * CONSTRUCTOR */ public function [c.name/]() { super(); source = '[c.name/]'; destination = '[c.name/]'; // Define operations of the service var internal_operations:Object = new Object(); var operation:Operation; [for (op : Operation | c.getAllOperations()->sortedBy(name))] [if (op.visibility.toString() = 'public')] // Add the [op.name/] operation operation = new Operation(null, '[op.name/]'); [comment In the case where a '*' cardinality is defined as return type/] [if (op.upperBound() = -1)] operation.resultElementType = org.corlan.entities.[op.type.name/]; [else] operation.resultType = Object; [/if] internal_operations['['/]'[op.name/]'] = operation; [/if] [/for] operations = internal_operations; } /* * OPERATIONS */ [for (op : Operation | c.getAllOperations()->sortedBy(name))] [comment Get only public methods from the remote service /] [if (op.visibility.toString() = 'public')] [comment Return an AsynToken to be caught by the CallResponder in the main application/] public function [op.name/]([setServiceParameters(op)/]):AsyncToken { var internal_operation:AbstractOperation = getOperation('[op.name/]'); var internal_token:AsyncToken = internal_operation.send([setSendParameters(op)/]) ; return internal_token; } [/if] [/for] } } [/file] [/template] [template public setServiceParameters( o:Operation )] [for (p : Parameter | o.ownedParameter->sortedBy(name)) separator (', ')][if (p.direction.toString()='in')][p.name/]:[p.type.name/][/if][/for] [/template] [template public setSendParameters( o:Operation )] [for (p : Parameter | o.ownedParameter->sortedBy(name)) separator (', ')][if (p.direction.toString()='in')][p.name/][/if][/for] [/template] |
The template is applied only on classes that are stereotyped as Service. The class name [c.name/] is used for the class itself, of course, the constructor and for the source and destination properties as well. The class will also extend the BaseServiceWrapper.
In the constructor, I loop on the class methods (c.getAllOperations) to add them in the operations property of the RemoteObject. The way it is done, is clearly inspired by the FB4 generated code. For each operation, I check the upperBound - corresponding to the cardinality of the return type - and set a resultElementType or just an Object as resultType according to the result. This is ok in the context of this sample application.
Then in the Operations section, I loop again on class methods to build the corresponding methods of the service, returning an AsyncToken, which will be addressed by the CallResponder in the main application. The method parameters are processed in a separated function defined at the end of the template. These functions just check that we have ‘in’ parameters before adding them as arguments of the methods.
Done.
Generating the code
As usual, you have noticed that the path for generated files is defined in the first file tag. You just need to run your main Module file to (re)generate the whole as3 client code.
The StudentService class should look like this:
package org.corlan.services { import mx.rpc.AbstractOperation; import mx.rpc.remoting.Operation; import mx.rpc.AsyncToken; import org.corlan.entities.*; /** * This is the php service class managing the Student entity */ public class StudentsService extends BaseServiceWrapper { /* * CONSTRUCTOR */ public function StudentsService() { super(); source = 'StudentsService'; destination = 'StudentsService'; // Define operations of the service var internal_operations:Object = new Object(); var operation:Operation; // Add the deleteStudent operation operation = new Operation(null, 'deleteStudent'); operation.resultType = Object; internal_operations['deleteStudent'] = operation; // Add the getStudents operation operation = new Operation(null, 'getStudents'); operation.resultElementType = org.corlan.entities.Student; internal_operations['getStudents'] = operation; // Add the saveStudent operation operation = new Operation(null, 'saveStudent'); operation.resultType = Object; internal_operations['saveStudent'] = operation; operations = internal_operations; } /* * OPERATIONS */ public function deleteStudent(student:Student):AsyncToken { var internal_operation:AbstractOperation = getOperation('deleteStudent'); var internal_token:AsyncToken = internal_operation.send(student) ; return internal_token; } public function getStudents():AsyncToken { var internal_operation:AbstractOperation = getOperation('getStudents'); var internal_token:AsyncToken = internal_operation.send() ; return internal_token; } public function saveStudent(student:Student):AsyncToken { var internal_operation:AbstractOperation = getOperation('saveStudent'); var internal_token:AsyncToken = internal_operation.send(student) ; return internal_token; } } } |
Conclusion
In this last part, the last layer of Mihai’s application has been generated and the generated code can actually been used as is. Which is really great ! One click to generate the code and it is ready for use. And from my own perspective again, the generated code is more simple and totally manageable. Moreover, you will probably be interessed to know that this Acceleo generated code is faster than the code generated by FB4. On my laptop, the init() method of the main application, after a few manual measures, shows a performance gain of more than 30% (15-19 ms with my templates against 26-29 ms for FB4 generated code).
After the 4 parts of the serie, I think that I achieved by objectives:
- "Start from a UML diagram, modelling the application"
- I have 2 schemas in my model: one for the Entities/Value Objects, one for the services.
- 1 model to rule all code layers !
- And I get the gobal view of the application.
- "Generate and maintain Doctrine2 entities that I don’t want to care"
- Doctrine2 entities can be generated (some options remain to be addressed - just the template to be improved).
- If the model evolves, I just need to run the Acceleo modules again and I get my entities updated (synchronization with the db must not be forgotten).
- With the same model, I get my as3 Value Objects as well.
- "Generate the server-side services (at least a skeleton of methods)"
- With the Service diagram, both server and client side services are generated.
- The main work remains in the php service, of course, but the template could be tuned to minimize the manual code implementation.
- "Replace the code generated by FB4 on the client side"
- As mentionned above, the client side code is fully generated, replacing the code generated by FB4.
- No additionnal manual code is required.
- When the model evolves, the code is just regenerated to reflect the change.
- The generated code is also faster.
So, what else ?
… lot’s of things could be envisaged… PHPUnit tests, FlexUnit tests generation… Forms generation ?
I just hope that you have found this serie interesting and you would like to try Acceleo to make your own templates for model driven development. If you are already an expert of the MDD method, I hope I haven’t told too much mistakes, don’t hesitate to send me feedbacks and advices as I am new in this domain, this serie was just an experiment for me but it makes me confident to go further in a bigger project.
Related posts
Comments are closed.