friends_using_interfaces

Implementing "friends" relationships using interfaces/typedefs

Just as many other OOP languages, Haxe includes and lets one write classes which can have private and public fields. What if, however, your class must be accessed with different privileges from different parts of your program? This is what interfaces can be used for. As private class fields are inaccessible from anywhere but within their own class definition, and public fields sometimes restrict access to read-only, field specification syntax alone is insufficient when a more fine-grained, context-aware field access control is needed. In our case we want to implement a "friends" relationship, where a class provides a "friendly" interface to certain parts of the program that need elevated privilege access to it. Another interface is left for accessing the class functionality as "general public", i.e. all other cases that do not require these privileges. There are several ways to accomplish this, each with its own merits and drawbacks, here we will cover one using the class interface itself for full access, and another interface for "public" access.

Note: classes are interfaces too

I have and will again mention "class interfaces" or "interface of a class". What does that really mean? There are classes and there are interfaces, right? Classes implement interfaces. They can't like, be or have interfaces, right? Wrong. A class is an interface too (the opposite is not the case). Any class, all classes. This class interface is composed of all the fields and methods of the class (sans their implementation of course - interfaces don't concern themselves with that.) You can actually have a class implement another class interface in Haxe:

class Bar
{
    function f()
    {
        trace("Hello from Bar.f");
    }
}

class FooBar implements Bar
{
    function f()
    {
        trace("Hello from FooBar.f");
    }
}

When FooBar implements Bar, bodies of whatever methods Bar has, are absolutely irrelevant for the "implements" relationship. Also, since class fields are private by default, the class interface fields are private too.

Now, back to implementing "friends" relationships, this is one way to do it:

/// Module "Main"

interface IPublicFoo
{
    var i(default, null): Int;
}

class Foo implements IPublicFoo
{
    public var i(default, set_i): Int;
    function set_i(v) return i = v

    public function new()
    {
    }
}

class Main
{
    static function main()
    {
        new Main();
    }

    function new()
    {
        var foo: IPublicFoo = new Foo();

        foo.i = 555; /// Error - IPublicFoo interface denies write access to 'i'

        var friendly_foo = new Foo();

        friendly_foo.i = 555; //Ok, because object is exposed through a more relaxed Foo interface
    }
}

This illustrates how a class with inherently relaxed access restrictions can present itself through another separate, more stricter interface to certain "public" parts of the program.

A curious property of this implementation and a fundamental principle of class-interface relationships in general is the fact that classes that implement interfaces can only "relax" these interfaces, not "narrow" them. This is why our class interface itself (Foo) functions as the "friendly class" interface, while the IPublicFoo interface that it implements is used for more restricted "public" access to Foo objects. This is also why we cannot reverse the roles easily - having Foo function as the restricted "public" access interface and some IFriendlyFoo interface providing "unrestricted access" - Foo objects in themselves would provide "friend" access anyway since the class now has to implement the IFriendlyFoo interface, and neither would work for "public" access. A semantical explanation would be "Foo implements IFriendlyFoo, will have to be friendly, always, everywhere". Here is what it would look like:

/// Module "Main"

interface IFriendlyFoo
{
    var i(default, set_i): Int;
}

class Foo implements IFriendlyFoo
{
    public var i(default, set_i): Int;
    function set_i(v) return i = v

    public function new()
    {
    }
}

class Main
{
    static function main()
    {
        new Main();
    }

    function new()
    {
        var friendly_foo: IFriendlyFoo = new Foo();

        friendly_foo.i = 555; /// Ok, IFriendlyFoo interface allows write access

        var foo = new Foo();

        foo.i = 555; /// Ok too - Foo class allows write access as well!

        /// How to provide "public" access?
    }
}

As we can see, with the snippet above, you always get full access no matter whether you expose a Foo object though its own type or IFriendlyFoo. The "public" access interface is simply not defined at all. You cannot restrict the specification of the "Foo.i" field either, because that would violate the "implements IFriendlyFoo" constraint.

Instead, because of the class-interface relationship principle, an interface fits best the role where it provides restricted "public" access and a class that implements this interface fits best the role where it provides more "friendly" (potentially unrestricted) access by itself (as interface).

Still, if for one reason or another, instead of class interface, you want to have an explicit separate interface for "friend" access, this is how you would do it:

/// Module "Main"

interface IFriendlyFoo
{
    var i(default, set_i): Int;
}

interface IPublicFoo
{
    var i(default, null): Int;
}

class Foo implements IFriendlyFoo, implements IPublicFoo
{
    public var i(default, set_i): Int;
    function set_i(v) return i = v

    public function new()
    {
    }
}

class Main
{
    static function main()
    {
        new Main();
    }

    function new()
    {
        var friendly_foo: IFriendlyFoo = new Foo();

        friendly_foo.i = 555; /// Ok, IFriendlyFoo interface allows write access of course

        var foo = new Foo();

        foo.i = 555; /// Ok too - Foo class allows write access as well!

        var some_foo: IPublicFoo = new Foo();

        some_foo.i = 555; /// Error - write access denied
    }
}

This version also gives you an opportunity to add fields to the Foo class that are unavailable when Foo objects are exposed as either IPublicFoo or IFriendlyFoo. Essentially, you end up defining three potential interfaces to Foo objects - a "public access" interface, a "friendly access" interface, and most relaxed of them - the "at least friends" access interface defined by the class itself. In most cases however, a class needs only the first two of these, since the third is often identical to the second, so the above scenario is a bit of a rarity case.

Conclusions

The first example was arguably the "best", in terms of code size and complexity. Essentially, the class itself provides "friend" access, and another interface is used for "public" access.

Using typedefs instead of interfaces

Interfaces and typedefs are different conceptually. They are not always and automatically compatible. In few situations when solving a programming problem however, they can be used instead of one another. Implementing "friends" relationships is one such situation, where we can use typedefs instead of interfaces. There are a few factors that matter though. Interfaces are runtime constructs for at least one of the platforms Haxe currently supports. They may thus be a factor in runtime optimization. Among their other differences however (for extensive documentation, see Typedefs), typedefs do not provide the kind of runtime type safety that interfaces do, they only exist as distinct types at compile-time and are only type-checked when compiling the program, albeit most often with good enough results compared to runtime type checking.

Unlike interfaces, typedefs are not and cannot be bound to class definitions. A crucial factor for some would be the fact that since you cannot change existing classes, implementing interfaces for them is impossible, and so these classes cannot expose themselves as "friends" using an interface they do not implement, and attempting to forcefully cast such class objects to interfaces they do not implement is an unsafe runtime operation. With typedefs you can safely have new and existing classes "become friends" with other new or existing classes, this is one of the applications of their advantages.

Finally, using typedefs instead of interfaces in our case arguably presents a simple and functional case for review:

/// Module "Main"

typedef TPublicFoo
{
    var i(default, null): Int;
}

class Foo
{
    public var i(default, set_i): Int;
    function set_i(v) return i = v

    public function new()
    {
    }
}

class Main
{
    static function main()
    {
        new Main();
    }

    function new()
    {
        var foo: TPublicFoo = new Foo();

        foo.i = 555; /// Error - TPublicFoo typedef interface denies write access to "i"

        var friendly_foo = new Foo();

        friendly_foo.i = 555; //Ok
    }
}

version #13756, modified 2012-04-15 08:46:57 by elyon