Inheritance? In Apama?

One of the common questions we hear about Apama, and EPL, is: “How can I create derived classes?” The naive answer is “you can’t” – but let’s dig a little deeper.

First off, a little bit of background. Apama EPL doesn’t have classes – it has events. An event can have both data and methods (actions) but it is not a class. Among other things, this means that you can’t directly extend or inherit an event. You can do something like:

event BaseEvent {
   action() method1;
   // More actions here...
}

event ExtendedEvent {
   BaseEvent base;
   action() extendedMethod1;
   // More actions here...
}

However, this approach means that ExtendedEvents are not DataEvents – unlike inheritance. It also makes it harder to have configuration-based behaviour – I cannot create a different behaviour at run-time.

By using action variables, instead of actions, we can overcome this. The approach is:

Write the interface for your “class”.
Create implementations.
Write a factory class, that sets up the members on the interface and returns the populated interface.
Let’s take a look at a sample. This is a (slightly enhanced) version of “Hello World”. The first piece to look at is UsingTheSample.mon. This is how we actually use an interface like this.

UsingTheSample.mon

package com.apamax.sample;
 
event UserName {
    string name;
}
 
monitor UsingTheSample {
    
    action onload {
        optional <SampleInterface> sample := SampleFactory.create("English");
        // Guard against null return value - i.e. unsuccessful creation
        ifpresent sample {
            sample.introduceApama();
            on all UserName() as user {
                sample.enterName(user.name);
                sample.greetName();
                print "======\n\n======";
                sample.introduceApama();
            }
        } else {
            log "Could not create interface from factory!" at ERROR;
            die;
        }
    }
}

In the onload() action, we call the factory’s static create method, which gives us a completed interface. The optional return type is there just in case there’s a problem, for example. This sample has very naive error handling, but at least it does something.

OK, we’ve got an interface, with an implementation behind it. Note that we don’t need to know what the implementation is to use it: this is a classic example of implementation masking, just like in a truly object-oriented language. Let’s interact with it: we tell the code to introduce itself, waiting for a user name, and then greeting the user by name. Some sample output:

Hello, I am APAMA.
What is your name?
Nice to meet you, Henry Lockwood.
======

Now let’s take a look at how this works. The interface file first (SampleInterface.mon):

SampleInterface.mon

package com.apamax.sample;
 
// This event file defines the interface of the "class" we're
// building.  It uses action variables ONLY - no implementation
// details here, and no state.  This allows us to overcome the
// lack of inheritance in the language.
event SampleInterface {
    action<string> enterName;
    action<> introduceApama;
    action<> greetName;
}

This one’s very simple. All it does is define the actions signatures for the “object”: anything else is handled elsewhere.

Now we’ll consider the Factory:

SampleFactory.mon

package com.apamax.sample;
 
// This event object allows you to create "instances" of an interface,
// using whichever implementation you specify.  This can be used to 
// support significant behaviour changes based on configuration: for example,
// which data store TYPE do we use - CSV, SQL Server, Oracle, NoSQL...
event SampleFactory
{
    // Static create action: this takes a parameter (which language do you want?)
    // and creates a interface to the sample, using that language.  Note 
    // that it relies on that implementation existing - some situations
    // might call for a default implementation that supports overrides, 
    // whereas other use-cases need explicit implementation and should fail
    // if no implementation exists.
    static action create(string language) returns optional<SampleInterface>
    {
        optional<SampleInterface> sample := new optional<SampleInterface>;
        
        string upperCaseLanguage := language.toUpper();
        if upperCaseLanguage = "ENGLISH" {
            SampleImplementationEnglish implEN := new SampleImplementationEnglish;
            sample := SampleInterface(implEN.enterName, implEN.introduceApama, implEN.greetName);
        }
        if upperCaseLanguage = "FRENCH" or upperCaseLanguage = "FRANCAIS" {
            SampleImplementationFrench implFR := new SampleImplementationFrench;
            sample := SampleInterface(implFR.enterName, implFR.introduceApama, implFR.greetName);
        }
        
        // You can support more implementations here.
        
        // Using an optional type here gives the ability to return null, if the requested 
        // implementation is not valid.
        return sample;
    }
}
````

This wraps up the various implementations – in this case, English and French – and makes them available for creation. We can create an instance by specifying a language – and if there’s no implementation for the desired language, we can return an empty optional – this is an approximate equivalent of NULL in other languages. Note that, if you wanted to, you could implement a default behaviour here – for example, we could write the interface to return the English-language version unless a recognized language was requested.

This creates method constructs a SampleInterface object and defines the implementation of each of the interfaces. Note that, if an interface is unset, you can try calling it – but it will cause a stack dump, so be careful here.

Finally, there’s the implementation itself. All this does is take the action variables in the Interface, and create actions matching those signatures. You can think of this as providing an implementation for a (pure) virtual class.

SampleImplementationEnglish.mon
`````
package com.apamax.sample;
 
event SampleImplementationEnglish {
    string userName;
    action enterName(string name) {
        userName := name;
    }
    
    action introduceApama() {
        print "Hello, I am APAMA.";
        print "What is your name?";
    }
    
    action greetName() {
        print "Nice to meet you, " + userName + ".";
    }
}
`````

SampleImplementationFrench.mon
````
package com.apamax.sample;
 
event SampleImplementationFrench {
    string userName;
    action enterName(string name) {
        userName := name;
    }
    
    action introduceApama() {
        print "Bonjour, je m'appelle APAMA.";
        print "Comment vous appelez-vous?";
    }
    
    action greetName() {
        print "Heureux de faire votre connaissance, " + userName + ".";
    }
}

So there you have it – an interface, allowing you to write code without caring about the implementation; and a factory, to create instances of that interface with whichever implementation you choose. There are some other things you can do with that pattern – such as standard functionality that’s the same across all implementations or using optional types to support variable overriding, but those are more detailed than we’re going to get into here.

You can download the project here.

Disclaimer:
Utilities and samples shown here are not official parts of the Software AG products. These utilities and samples are not eligible for technical support through Software AG Customer Care. Software AG makes no guarantees pertaining to the functionality, scalability, robustness, or degree of testing of these utilities and samples. Customers are strongly advised to consider these utilities and samples as “working examples” from which they should build and test their own solutions