5
\$\begingroup\$

The goal is to be able to call an event_<name> method on an object if it implements it, or otherwise do nothing, without needing to explicitly define and implement an interface for every event. The object may also forward the event to any sub-objects it has.

Here is my current code:

module eventobject;

import std.typetuple;
import std.traits;
import std.conv;

/**
 * Iterface template that specifies that the class implements
 * event_<name>(<args>).
 */
interface Event(string Name, Args...) {
    mixin("public void event_"~Name~"(Args);");
}

/**
 * Generates an event dispatcher based on which event methods the
 * class implements.
 * 
 * This should be used inside of the class whose event dispatcher
 * is being generated, and the class should be passed in.
 */
mixin template CreateEventDispatcher(alias cls) {
    mixin(EventDispatcherImplGen!cls);

    static if(!__traits(hasMember, cls, "_eventDispatcher")) {
        protected EventDispatcherBase _eventDispatcher;
    }

    protected override EventDispatcherBase eventDispatcher() @property {
        if(!_eventDispatcher)
            _eventDispatcher = this.new EventDispatcher();
        return _eventDispatcher;
    }
}

/**
 * Base object for objects with events.
 * Events are functions that return void and are of the form "event_<name>". Event
 * methods should not be overloaded.
 */
abstract class EventObject {
    /**
     * Returns an array of objects to forward events to, or null to not
     * do forwarding.
     * 
     * (Overriding `event` to forward events would be better design, but template functions
     * are non-virtual and can't be overwritten)
     */
    protected EventObject[] getObjectsToForward() { return null; }

    /**
     * Triggers an event. If the object implements the event, it will call the event method.
     * Otherwise, the event is ignored.
     * 
     * If the object returns non-null from getObjectsToForward, also calls the event method
     * on all of the returned objects.
     */
    public final void event(string name, Args...)(Args a) {
        this.eventDispatcher.callEvent!(name, Args)(a);

        auto forward = this.getObjectsToForward();
        if(forward)
            foreach(obj; forward)
                obj.event!(name, Args)(a);
    }

    /**
     * Do not use directly; use `event` instead. Returns the event dispatcher,
     * which implements the event interfaces and calls the EventObject's methods.
     */
    protected abstract EventDispatcherBase eventDispatcher() @property;
}

// //////////////////////////////////////////////////////////////////////////////
// Private stuff

/**
 * Do not use directly. Base class for event dispatchers.
 */
abstract class EventDispatcherBase {
    public void callEvent(string Name, Args...)(Args args) {
        if(cast(Event!(Name, Args)) this)
            mixin("(cast(Event!(Name, Args)) this).event_"~Name~"(args);");
    }
}

/**
 * Returns comma-separated string list of types.
 * Ex. "string,int,bool"
 */
private template TypeStrings(bool first, T...) {
    static if(T.length == 0) {
        enum TypeStrings = "";
    } else static if(first) {
        enum TypeStrings = T[0].stringof ~ TypeStrings!(false, T[1..$]);
    } else {
        enum TypeStrings = "," ~ T[0].stringof ~ TypeStrings!(false, T[1..$]);
    }
}

/**
 * Returns arguments for types.
 * Ex. "string arg0, int arg1, bool arg2"
 */
private template TypeStringsWithParams(int n, T...) {
    static if(T.length == 0) {
        enum TypeStringsWithParams = "";
    } else static if(n == 0) {
        enum TypeStringsWithParams = T[0].stringof ~ " arg" ~ to!string(n) ~
            TypeStringsWithParams!(n+1, T[1..$]);
    } else {
        enum TypeStringsWithParams = "," ~ T[0].stringof ~ " arg" ~ to!string(n) ~
            TypeStringsWithParams!(n+1, T[1..$]);
    }
}

/**
 * Wrapper around T.stringof for use with CommaSeparatedConcat
 */
private template TypeString(int n, T) {
    enum TypeString = T.stringof;
}

/**
 * Outputs typed arguments, for use with CommaSeparatedConcat.
 * ex. "string arg1", "bool arg5"
 */
private template TypedArg(int n, T) {
    enum TypedArg = T.stringof ~ " arg" ~ to!string(n);
}

/**
 * Outputs argument references, for use with CommaSeparatedConcat.
 * ex. "arg1", "arg5"
 */
private template UntypedArg(int n, T) {
    enum UntypedArg = "arg" ~ to!string(n);
}

/**
 * Reduces F!(n, T) by concatenating the results with a comma separating each result.
 */
private template CommaSeparatedConcat(int n, alias F, T...) {
    static if(T.length == 0) {
        enum CommaSeparatedConcat = "";
    } else static if(n == 0) {
        enum CommaSeparatedConcat = F!(n, T[0]) ~ CommaSeparatedConcat!(n+1, F, T[1..$]);
    } else {
        enum CommaSeparatedConcat = "," ~ F!(n, T[0]) ~ CommaSeparatedConcat!(n+1, F, T[1..$]);
    }
}

/**
 * Given an event name and parameters, constructs a method for EventDispatcher.
 */
private template EventDispatcherMethodGen(string name, T...) {
    enum EventDispatcherMethodGen = "public void "~name~"("~CommaSeparatedConcat!(0,TypedArg,T) ~
        ") { this.outer."~name~"("~CommaSeparatedConcat!(0,UntypedArg,T)~"); }";
}

/**
 * Given an EventObject class and its members, construts each method for EventDispatcher.
 */
private template EventDispatcherAllMethodsGen(alias cls, string[] S) {
    static if(S.length == 0)
        enum EventDispatcherAllMethodsGen = "";
    else static if(S[0].length <= 6 || S[0][0..6] != "event_")
        enum EventDispatcherAllMethodsGen = EventDispatcherAllMethodsGen!(cls, S[1..$]);
    else
        enum EventDispatcherAllMethodsGen = EventDispatcherMethodGen!(S[0], ParameterTypeTuple!(__traits(getMember, cls, S[0]))) ~
            " " ~ EventDispatcherAllMethodsGen!(cls, S[1..$]);
}

/**
 * Given an event name and parameters, constructs an Event interface to inherit from.
 */
private template EventDispatcherInterfaceGen(string name, T...) {
    enum EventDispatcherInterfaceGen = "Event!(\""~name[6..$]~"\"," ~ CommaSeparatedConcat!(0,TypeString,T) ~")";
}

/**
 * Given an EventObject class and its members, constructs a list of Event interfaces for EventDispatcher to inherit.
 * The list includes an extra leading comma.
 */
private template EventDispatcherAllInterfacesGen(alias cls, string[] S) {
    static if(S.length == 0)
        enum EventDispatcherAllInterfacesGen = "";
    else static if(S[0].length <= 6 || S[0][0..6] != "event_")
        enum EventDispatcherAllInterfacesGen = EventDispatcherAllInterfacesGen!(cls, S[1..$]);
    else
        enum EventDispatcherAllInterfacesGen = "," ~
            EventDispatcherInterfaceGen!(S[0], ParameterTypeTuple!(__traits(getMember, cls, S[0]))) ~
            EventDispatcherAllInterfacesGen!(cls, S[1..$]);
}

/**
 * Do not use directly. Generates an EventDispatcher implementation
 * based on the event functions in the passed members.
 */
template EventDispatcherImplGen(alias cls) {
    enum EventDispatcherImplGen = "private final class EventDispatcher : EventDispatcherBase" ~
        EventDispatcherAllInterfacesGen!(cls, [__traits(allMembers, cls)]) ~
        "{" ~
        EventDispatcherAllMethodsGen!(cls, [__traits(allMembers, cls)]) ~
        "};";

}

The EventObject class contains an eventDelegate object. The eventDelegate's class is dynamically created for each EventObject's subclass, using the CreateEventDispatcher mixin. The mixin scans the class's members for event_<name> methods, and for each one, a) implements an interface Event!("<name>", Args...) and b) implements the event function, which simply calls the outer class's event function.

When EventObject.event is called, it gets the eventDelegate, and checks if it implements the corresponding Event interface. If it does, it casts the object to the interface and calls the eventDelegate's event method, which calls the EventObject subclass's event method. It also goes through the array of EventObjects to forward events to, and calls their event method.

An example class that uses it:

class Foo : EventObject {
    private EventObject[] subobjects;

    public this(EventObject[] subobjects) {
        this.subobjects = subobjects;
    }

    protected override EventObject[] getObjectsToForward() { return subobjects; }

    public void event_test() {
        writeln("test");
    }
    public void event_anotherTest(int i, string j) {
        writeln("anotherTest ", i, " ", j);
    }

    mixin CreateEventDispatcher!Foo;
}

void main() {
    EventObject obj = new Foo([]);
    obj.event!"test"(); // Prints "test"
    obj.event!"anotherTest"(123, "bar"); // Prints "anotherTest 123 bar"
    obj.event!"nonexistant"(); // Prints nothing
}

Is there anything I can improve? In particular:

  • Is there a way to ditch the eventDelegate object and add the interfaces to the EventObject subclass itself?
  • Is there a way to call CreateEventDispatcher automatically when subclassing?
\$\endgroup\$
  • \$\begingroup\$ Welcome to Code Review! This looks like example code, class Foo, obj.event!"test"();, etc. Could you post your actual code? \$\endgroup\$ – Phrancis Dec 4 '14 at 15:54
  • \$\begingroup\$ @Phrancis The first code segment is the code to be reviewed. The second segment is example code. \$\endgroup\$ – Colonel Thirty Two Dec 4 '14 at 15:54
  • \$\begingroup\$ OK I'll retract my vote to close then. Hope you get some good reviews! \$\endgroup\$ – Phrancis Dec 4 '14 at 15:58
1
\$\begingroup\$

So I've made some improvements to the code after learning a bit more about D and reviewing the code myself.

  • The TypeStrings and TypeStringsWithParams templates were superseded by CommaSeparatedConcat, and aren't used anywhere. Remove them.
  • Since I'm requiring objects with events to extend the EventObject class anyway, put the _eventDispatcher there instead of defining it in the mixin if it hasn't been defined already.
  • Change getObjectsToForward to return an InputArray!EventObject, to avoid needing to allocate an array every call if the subclass doesn't store objects to forward events to in an array (which in my case, it doesn't).
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.