Constructeur d'accès restreint en ActionScript 3

Alors que de nombreux design patterns reposent sur un constructeur d'accès restreint (protected, internal, private ou un namespace), le constructeur en actionscript 3 ne supporte à ce jour qu'une visibilité publique. Cette restriction rend difficile l'implémentation intègre de certains patterns pourtant très simple comme le singleton, la fabrique statique ou la classe abstraite (1). Je propose dans cet article un pattern qui permet de faire reposer la visibilité du constructeur sur un autre membre de classe, qui, lui, bénéficie de tous les modificateurs de visibilité existants en as3.

Parmi les constructeurs à accès restreint, le plus commun est le constructeur privé. Une implémentation communément rencontrée en as3 consiste à lever une exception dans le constructeur lui même, afin d'en verrouiller l'appel à l'exécution :

package com.zanshine.blog.restrictedConstructor
{

  public class NaivePrivateConstructor
  {
    public function NaivePrivateConstructor()
    {
      //Ici, le constructeur ne peut jamais être appelé.
      throw new Error("Private Constructor");
    }
  }
}

En dehors d'une classe statique ou abstraite, un tel constructeur n'est guère utilisable en l'état. Cette solution présente deux inconvénient majeurs : la restriction d'accès ne s'effectue qu'à l'exécution et l'héritage par appel explicite à super() n'est possible qu'au prix d'un appel à l'api de réflexion. Une implémentation de classe abstraite à l'aide de cette méthode :

package com.zanshine.blog.restrictedConstructor
{
  import flash.utils.getQualifiedClassName;

  public class NaiveAbstractClass
  {
    public function NaiveAbstractClass()
    {
      //on ne peut pas instancier directement cette classe
      if(getQualifiedClassName(this) ==
        "com.zanshine.blog.restrictedConstructor::NaiveAbstractClass")
        throw new Error("Abstract class instanciation");
    //reste de l'implémentation
    }
  }
}
package com.zanshine.blog.restrictedConstructor
{

  public class NaiveConcreteClass extends NaiveAbstractClass
  {
    public function NaiveConcreteClass()
    {
      //ici l'appel à super ne lève pas d'exception, puisque l'on sous-classe
      super();
      //reste de l'implémentation
    }
  }
}

Une alternative consiste à utiliser une clé, que l'on fournit au constructeur afin de valider la légitimité de l'appel. Si cette clé est nulle ou invalide, l'instanciation lève une exception. Deux solutions sont couramment utilisées, l'une étant une variante de l'autre : utiliser une fonction ou un objet comme clé. La validité de cette clé va être vérifiée dans le constructeur par test d'égalité de références et une exception sera levée le cas échéant.

La première variante, avec une fonction comme clé :

package com.zanshine.blog.restrictedConstructor
{

  public class RestrictedWithAFunction
  {
    public function RestrictedWithAFunction(key : Function)
    {
      if (key != constructorKey)
    throw new Error("Illegal instanciation");
    }

    private function constructorKey() : void;
  }
}

La seconde variante, avec un objet comme clé :

package com.zanshine.blog.restrictedConstructor
{

  public class RestrictedWithAnObject
  {
    public function RestrictedWithAnObject(key : *)//Il peut s'agir de n'importe quel type,
              //y compris une fonction, ce qui nous amènera d'ailleurs à la solution finale
    {
      if (key != constructorKey)
    throw new Error("Illegal instanciation");
    }

    private static var constructorKey : * = {};
    //on pourrait tout autant instancier [], function():void{},
    //new Function ou new MyCustomType, le mécanisme fonctionnerait.
  }
}

Voici un exemple d'implémentation de classes abstraite et concrète avec cette solution (je prends ici la seconde alternative, puisque la première, comme vous l'avez vu, n'est qu'une variante de la seconde, une fonction étant un objet) :

package com.zanshine.blog.restrictedConstructor
{

  public class WithAnObjectKeyAbstractClass
  {
    public function WithAnObjectKeyAbstractClass(key : *)
    {
      if (key != constructorKey)
    throw new Error("Illegal instanciation");
    }

    protected static var constructorKey : * = {};
  }
}
package com.zanshine.blog.restrictedConstructor
{

  public class WithAnObjectKeyConcreteClass extends WithAnObjectKeyAbstractClass
  {
    public function WithAnObjectKeyConcreteClass()
    {
      super(constructorKey);
    }
  }
}

Cette méthode pour restreindre l'accès au constructeur commence à gagner en élégance, mais ne permet toujours pas une vérification à la compilation. Il faut donc compter sur les tests unitaires pour soutenir la solution. Pour parfaire ce pattern de Constructeur d'Accès Restreint (Restricted Acces Constructor, RAC), il nous faut donc un typage fort et non générique de la clé. Ce typage permettra au compilateur de vérifier que la clé est bien du type attendu. Si ce type est connu de la seule classe à RAC, on garantit en grande partie la validité de la clé dès la compilation. L'actionscript 3 permettant la déclaration de classes interne, la solution se présente sous cette forme :

package com.zanshine.blog.restrictedConstructor
{

  public class RestrictedAccesConstructor
  {

    public function RestrictedAccesConstructor(key : Key)//(A)
    {
      if (!key)//(D)
    throw new Error("Illegal instanciation");
      //implémentation ...
    }

    private static var _constructorKey : Key = new Key; //(C)

    //(F)
    [public|protected|internal|private|namespace] static function get constructorKey() : Key {//(E)
      return _constructorKey;
    }
  }
}

internal final class Key {} //(B) Cette déclaration n'existe que pour RestrictedAccesConstructor

Le constructeur de RestrictedAccesConstructor prend en argument une référence à la classe interne Key (A) que seule RestrictedAccesConstructor est à même de fournir par export (B), puisque qu'aucune autre classe ne peut avoir une référence à Key par import. Une instance de cette classe interne sert donc de clé d'instanciation pour RestrictedAccesConstructor (C). A part si null est délibérément passé en argument au constructeur de RestrictedAccesConstructor, l'utilisation d'un type connu de cette seule classe pour la clé d'instanciation garantit la vérification dès la compilation. Il n'est donc plus nécessaire de tester l'égalité de références entre la clé connue et la clé donnée. Il ne reste qu'à vérifier la non nullité de la référence passée en argument afin d'assurer une vérification à l'exécution (D). Pour finir, il faut donner la possibilité à RestrictedAccesConstructor d'exporter sa clé vers les classes devant avoir le privilège de son instanciation (E). Ceci se fait via une méthode statique dont l'accès peut être maîtrisé par les modificateurs de visibilité natifs (public, protected, internal, private) ou les namespaces (F).

Une classe héritant de RestrictedAccessConstructor (qui illustre toujours l'exemple de classes abstraite/concrete) :

package com.zanshine.blog.restrictedConstructor
{
  import com.zanshine.blog.restrictedConstructor.RestrictedAccesConstructor;

  public class InheritFromRestrictedAccesConstructor extends RestrictedAccesConstructor
  {
    public function InheritFromRestrictedAccesConstructor()
    {
      super(constructorKey);
      //reste de l'implémentation
    }
  }
}

Voici un second exemple pour illustrer une autre visibilité de la clé (donc du constructeur) que protected. Ici, le constructeur est techniquement internal via le modificateur de visibilité de get constructorKey():Key :

package com.zanshine.blog.restrictedConstructor
{

  public class Car
  {
    public function Car(key : Key)
    {
      if(!key)
        throw new Error('The key reference passed to the
        constructor is null');
    }

    private static var _constructorKey : Key;

    //seules les classes du même package pourront accéder à la clé
    internal static function get constructorKey() : Key {
      return _constructorKey = _constructorKey ? _constructorKey : new Key;
    }
  }
}

internal final class Key {}
package com.zanshine.blog.restrictedConstructor
{

  public class CarFactory
  {
    public static function createCar() : Car
    {
      return new Car(Car.constructorKey);
    }
  }
}

Grâce à cette solution, de nombreuses possibilités s'ouvrent à nous pour implémenter les design patterns reposant sur un constructeur d'accès restreint. L'actionscript 3, au delà de ses lacunes, offre aussi des possibilités intéressantes. Dans le cas présent, je pense à l'usage des namespaces comme modificateurs de visibilité pour l'export de la clé d'instanciation. Les namespaces en as3 et leur usage débordent largement du cadre de cet article, mais je me ferais un plaisir d'en explorer les myriades de possibilités dans un autre post !

note :

1). Je range la classe abstraite dans la catégorie des des design patterns car, n'étant pas prise en charge nativement par l'actionscript 3, il nous faut passer par le design pour mettre le concept en pratique.