Cross Platform Code Generation

This is the seventh in the sequence of blog posts regarding some of experiences in creating Trading and eCommerce platforms at Adaptive. The previous post can be found here.

In this post we look at how we generate Client- and Server-side code for multiple platforms. The approach we take is to write the majority of the code for each platform in a generic manner as a layered stack that can be extensively tested and to automatically generate a thin layer that sits on top of that stack to encapsulated the specifics of each Service.

Layered Messaging Stack

The diagram below shows the layered messaging stack that we implement for each platform. Points to note are that the stack has both Client- and Server-side components centered around the Middleware.

Layered Messaging Stack

The Client-side components are written for every platform. Whereas, the Server-side components are only written for key platforms (currently Java and .Net) that will expose Services. There is a high level of symmetry between the Client- and Server-side layers, so we can discuss both at the same time.

The lowest layer of the stack is the Middleware itself. This is then adapted to the Middleware Abstraction. On top of this are built the Publisher, Subscriber and Message implementations. These are in turn used by the Messaging Protocols of Request/Response, Request Only, Streaming, Request Stream and Publish Subscribe. Each Messaging Protocol has its corresponding Client and Server implementations.

The highest layer in the stack is the Service layer on the Server-side and the Proxy layer on the Client-side. The ServiceBase and ProxyBase components are abstract classes that encapsulate the commonality all Services and their Proxies.

On the Server-side, the concrete Service derives from ServiceBase, implements the Service Interface and is hosted by the ServiceHost. All of these components are automatically code generated.

On the Client-side, the concrete Proxy derives from ProxyBase and implements the Proxy Interface. Again, all of these components are code generated.

By adopting this architecture we are able to target any number of Client-side platforms by writing a relatively small amount of code that is readily testable.

Code Generation

In order to generate the code for each platform, we use a Templating Library. We looked at numerous different libraries such as String Template and Apache Velocity. These have various advantages and disadvantages, but in the end, we settled on using FreeMarker. In our opinion, this provided the level of functionality, flexibility, ease-of-use, and quality of documentation that we needed.

Templates

Some example templates are given in the following sections. It should be noted that the templates operate directly on the Service Domain Model already discussed.

Proxy Interface

The template below is for the Proxy Interface for the Web platform. It should be noted that we're generating TypeScript code which we then transpile into JavaScript. This extra step is optional, but we find that the compile-time type checking proves invaluable in catching errors as early as possible. Here you can see that we're creating an Interface that has a method for each operation defined in the Service Contract.


    /* Code Generated - do not edit by hand */

    module ${namespace.htmlNamespace} {

        export interface I${service.name}Proxy extends ISubscription {
            <#list service.operations as operation>
                <#if operation.type == "REQUEST_RESPONSE">
            ${operation.name}(request:${printMsgRef(operation.requestMessage)}, responseHandler:IResponseHandler<${printMsgRef(operation.responseMessage)}>):void;
                <#elseif operation.type == "REQUEST_ONLY">
            ${operation.name}(request:${printMsgRef(operation.requestMessage)}):void;
                <#elseif operation.type == "STREAM">
            ${operation.name}(updateHandler:IUpdateHandler<${printMsgRef(operation.updateMessage)}>):ISubscription;
                <#elseif operation.type == "REQUEST_STREAM">
            ${operation.name}(request:${printMsgRef(operation.requestMessage)}, streamHandler:IStreamHandler<${printMsgRef(operation.updateMessage)}>):ISubscription;
                <#elseif operation.type == "PUBLISH_SUBSCRIBE">
            ${operation.name}(request:${printMsgRef(operation.requestMessage)}, streamHandler:IStreamHandler<${printMsgRef(operation.updateMessage)}>):ISubscription;
                
            
        }

    }

    <#function printMsgRef message>
        <#if message.type == "COMPLEX">
            <#return "I${message.name?cap_first}"/>
        <#else>
            <#return "${message.name?cap_first}"/>
        
    

Proxy Implementation

The template below is for the Proxy Implementation for the .Net platform. Here you can see that we generate a concrete class that derives from the abstract Proxy base class and implement the Proxy Interface that was also generated. Then for each operation, we define a field of the Client type for that particular Messaging Protocol. Finally, we define methods that call onto the Proxy base class.


    /* Code Generated - do not edit by hand */
    namespace ${namespace.dotnetNamespace}
    {

        public class ${service.name?cap_first}Proxy : Proxy, I${service.name?cap_first}Proxy
        {
            <#list service.operations as operation>
                <#if operation.type == "REQUEST_RESPONSE">
            private readonly IRequestResponseClient<${operation.requestMessage.name?cap_first}, ${operation.responseMessage.name?cap_first}> _${operation.name}Client;
                <#elseif operation.type == "REQUEST_ONLY">
            private readonly IRequestOnlyClient<${operation.requestMessage.name?cap_first}> _${operation.name}Client;
                <#elseif operation.type == "STREAM">
            private readonly IStreamClient<${operation.updateMessage.name?cap_first}> _${operation.name}Client;
                <#elseif operation.type == "REQUEST_STREAM">
            private readonly IRequestStreamClient<${operation.requestMessage.name?cap_first}, ${operation.updateMessage.name?cap_first}> _${operation.name}Client;
                <#elseif operation.type == "PUBLISH_SUBSCRIBE">
                private readonly IPublishSubscribeClient<${operation.requestMessage.name?cap_first}, ${operation.updateMessage.name?cap_first}> _${operation.name}Client;
                
            

            public ${service.name?cap_first}Proxy(IClientOperationFactory operationFactory)
                : base(operationFactory)
            {
            <#list service.operations as operation>
                <#if operation.type == "REQUEST_RESPONSE">
                _${operation.name}Client = CreateRequestResponse(
                    new RequestResponseDefinition<${operation.requestMessage.name?cap_first}, ${operation.responseMessage.name?cap_first}>(
                        "${service.name}", "${operation.name}", ${service.isEnterprise()?c}, ${service.isSessionRequired()?c}));
                <#elseif operation.type == "REQUEST_ONLY">
                _${operation.name}Client = CreateRequestOnly(
                    new RequestOnlyDefinition<${operation.requestMessage.name?cap_first}>(
                        "${service.name}", "${operation.name}", ${service.isEnterprise()?c}, ${service.isSessionRequired()?c}));
                <#elseif operation.type == "STREAM">
                _${operation.name}Client = CreateStream(
                    new StreamDefinition<${operation.updateMessage.name?cap_first}>(
                        "${service.name}", "${operation.name}", ${service.isEnterprise()?c}, ${service.isSessionRequired()?c}));
                <#elseif operation.type == "REQUEST_STREAM">
                _${operation.name}Client = CreateRequestStream(
                    new RequestStreamDefinition<${operation.requestMessage.name?cap_first}, ${operation.updateMessage.name?cap_first}>(
                        "${service.name}", "${operation.name}", ${service.isEnterprise()?c}, ${service.isSessionRequired()?c}));
                <#elseif operation.type == "PUBLISH_SUBSCRIBE">
                _${operation.name}Client = CreatePublishSubscribe(
                    new PublishSubscribeDefinition<${operation.requestMessage.name?cap_first}, ${operation.updateMessage.name?cap_first}>(
                        "${service.name}", "${operation.name}", ${service.isEnterprise()?c}, ${service.isSessionRequired()?c}));
                
            
            }

            <#list service.operations as operation>
                <#if operation.type == "REQUEST_RESPONSE">
            public void ${operation.name?cap_first}(${operation.requestMessage.name?cap_first} request, IResponseHandler<${operation.responseMessage.name?cap_first}> responseHandler)
            {
                _${operation.name}Client.Request(request, responseHandler);
            }

                <#elseif operation.type == "REQUEST_ONLY">
            public void ${operation.name?cap_first}(${operation.requestMessage.name?cap_first} request)
            {
                _${operation.name}Client.Request(request);
            }

                <#elseif operation.type == "STREAM">
            public IDisposable ${operation.name?cap_first}(UpdateHandler<${operation.updateMessage.name?cap_first}> updateHandler)
            {
                return _${operation.name}Client.Subscribe(updateHandler);
            }

                <#elseif operation.type == "REQUEST_STREAM">
            public IDisposable ${operation.name?cap_first}(${operation.requestMessage.name?cap_first} request, IStreamHandler<${operation.updateMessage.name?cap_first}> streamHandler)
            {
                return _${operation.name}Client.Request(request, streamHandler);
            }
                <#elseif operation.type == "PUBLISH_SUBSCRIBE">
            public IDisposable ${operation.name?cap_first}(${operation.requestMessage.name?cap_first} request, IStreamHandler<${operation.updateMessage.name?cap_first}> streamHandler)
            {
                return _${operation.name}Client.Subscribe(request, streamHandler);
            }
                
            
        }
    }

Message

The template below is for the Messages for the Java platform. Hopefully it's fairly easy to read, but in essence, we create either a class that implements Serializable or and enum depending on whether it's a complex Message or not. All of the fields, the constructor and getters etc. are generated also.


    /* Code Generated - do not edit by hand */
    package ${namespace.javaNamespace};

    <#if message.type == "COMPLEX">
    public class ${message.name} implements Serializable {
        <#list message.fields as field>
        private ${getFieldType(field)}<#if field.optionality == "REPEATED">[] ${field.name};
        

        public ${message.name}(<#list message.fields as field>${getFieldType(field)}<#if field.optionality == "REPEATED">[] ${field.name}<#if field_has_next>, ) {
            <#list message.fields as field>
            this.${field.name} = ${field.name};
            
        }

        <#if message.fields?has_content>
        // Serialization constructor
        private ${message.name}() {
        }
        

        <#list message.fields as field>
        public ${getFieldType(field)}<#if field.optionality == "REPEATED">[] get${field.name?cap_first}() {
            return ${field.name};
        }
        
    }
    <#else>
    public enum ${message.name} {
        <#list message.values as value>
        ${value}<#if value_has_next>,
        
    }
    

    <#function getFieldType field>
        <#switch field.type>
            <#case "PRIMITIVE">
                <#switch field.primitive>
                    <#case "BOOL">
                        <#return "boolean">
                    <#case "CHAR">
                        <#return "char">
                    <#case "DOUBLE">
                        <#return "double">
                    <#case "FLOAT">
                        <#return "float">
                    <#case "INT">
                        <#return "int">
                    <#case "SHORT">
                        <#return "short">
                    <#case "STRING">
                        <#return "String">
                
            <#case "MESSAGE">
                <#return field.message.name>
        
    

Summary

We have shown here how Code Generation can be used to transform our Service Domain Model into idiomatic code for any number of different platforms. The generated code is efficient and idiomatic to each platform. By adopting this strategy, we are able to rapidly target new platforms by writing a relatively small set of components.

The next post can be found here.

adaptive
Written by an Adaptive Consultant.



×

Contact us

By pressing "Send" I agree that I am happy to be contacted by Adaptive Financial Consulting. I can unsubscribe at any time. You can read more about our privacy policy here.

×

I would like to read the full story

By completing the below form I confirm I am happy to be contacted by Adaptive Financial Consulting Ltd. I can unsubscribe at anytime.