Forums Développement Multimédia

Les formations Mediabox
Les formations Mediabox

POO en ActionScript 3 - 2eme partie - Héritage, polymorphisme et interface

Compatible ActionScript 3. Cliquer pour en savoir plus sur les compatibilités.Par tannoy (Antony Chauviré), le 10 février 2010

Cette page est une adaptation de la documentation Adobe, et contient des extraits de la page Programmation avec ActionScript 3.0 > Programmation orientée objet en ActionScript > Héritage et de la page Programmation avec ActionScript 3.0 > Programmation orientée objet en ActionScript > Interfaces.

Adobe a donné son accord concernant l'utilisation de citations de la documentation. Lilive 04/03/2010

L’héritage

L’héritage est une forme de réutilisation du code qui permet aux programmeurs de développer de nouvelles classes à partir de classes existantes.

La classe existante est appelée classe de base ou classe mère, la nouvelle classe est appelée classe dérivée ou classe fille.

L’un des principaux avantages de l’héritage est qu’il permet de réutiliser le code d’une classe de base sans modifier le code existant. De plus, l’héritage ne nécessite pas de modifier les modes d’interaction des autres classes avec la classe de base. Plutôt que de modifier une classe existante, qui est peut-être soigneusement testée et déjà utilisée, l’héritage permet de traiter cette classe comme un module intégré qui peut être étendu à l’aide de propriétés et de méthodes supplémentaires.

Une classe ne peut pas hériter de plusieurs classes.

Création d’une sous-classe

On utilise le mot clé extends pour indiquer que notre classe est une sous-classe d’une autre.

package fr.tannoy.garage
{
	public class Voiture extends Vehicule
	{
		public function Voiture()
		{
			super();
		}
 
	}
}

L’instruction super(); permet d’appeler le constructeur de la classe mère et donc dans certains cas, d’initialiser les paramètres.

Dans la mesure où chaque classe définit un type de données, l'utilisation de l'héritage crée une relation spéciale entre une classe de base et une classe qui l'étend. Une sous-classe possède obligatoirement toutes les propriétés de sa classe de base, ce qui signifie qu'il est toujours possible de substituer une instance d'une sous-classe à une instance de la classe de base.

Par exemple, si une méthode définit un paramètre du type Vehicule, il est parfaitement autorisé de lui passer un argument du type Voiture, car Voiture étend Vehicule, comme on le voit ci-dessous :

package fr.tannoy.garage
{
	public class Garage
	{
		public function Garage()
		{
		}
 
		public function faireLaVidange(vehicule:Vehicule):void
		{
			trace(vehicule)
		}
 
	}
}
package fr.tannoy
{
	import fr.tannoy.garage.Garage;
	import fr.tannoy.garage.Vehicule;
	import fr.tannoy.garage.Voiture;
 
 
	public class Main
	{
		public function Main()
		{
			var garage:Garage = new Garage();
			garage.faireLaVidange(new Voiture());
			garage.faireLaVidange(new Vehicule());
		}
 
	}
}

Les caractéristiques de la sous-classe

Grâce au concept d'héritage, cet objet Vehicule va pouvoir donner naissance à un ou des descendants. Ces descendants vont tous bénéficier des caractéristiques propres de leur ancêtre, à savoir ses propriétés et méthodes.

package fr.tannoy.garage
{
	public class Vehicule
	{
		public function Vehicule()
		{
		}
 
		private var _marque:String;
		public function get marque():String
		{
			return _marque;
		}
		public function set marque(value:String):void
		{
			_marque = value;
		}
	}
}
 
package fr.tannoy.garage
{
	public class Voiture extends Vehicule
	{
		public function Voiture()
		{
			super();
		}
 
	}
}
 
package fr.tannoy.garage
{
	public class Moto extends Vehicule
	{
		public function Moto()
		{
			super();
		}
 
	}
}
 
package fr.tannoy
{
	import fr.tannoy.garage.Moto;
	import fr.tannoy.garage.Voiture;
 
 
	public class Main
	{
		public function Main()
		{
			var voiture:Voiture = new Voiture();
			voiture.marque = "Renault";
			trace(voiture.marque);
			var moto:Moto = new Moto();
			moto.marque = "Honda";
			trace(moto.marque);
		}
 
	}
}

Cependant, les descendants conservent la possibilité de posséder leurs propres propriétés et méthodes. Tout comme un enfant hérite des caractéristiques de ses parents et développe les siennes, un objet peut hériter des caractéristiques de son ancêtre, mais aussi en développer de nouvelles, ou bien encore se spécialiser.

package fr.tannoy.garage
{
	public class Voiture extends Vehicule
	{
		public function Voiture()
		{
			super();
		}
 
		private var _numPortes:int = 5;
		public function get numPortes():int
		{
			return _numPortes;
		}
		public function set numPortes(value:int):void
		{
			_numPortes = value;
		}
 
		public function ouvrirVitres():void
		{
			trace("Les vitres sont ouvertes");
		}
	}
}
 
package fr.tannoy
{
	import fr.tannoy.garage.Moto;
	import fr.tannoy.garage.Voiture;
 
 
	public class Main
	{
		public function Main()
		{
			var voiture:Voiture = new Voiture();
			voiture.marque = "Renault";
			trace(voiture.marque);
			var moto:Moto = new Moto();
			moto.marque = "Honda";
			trace(moto.marque);
 
			voiture.numPortes = 3;
			voiture.ouvrirVitres();
		}
 
	}
}

Notre classe Voiture nous offre donc des services plus évolués. Elle vient compléter la classe Vehicule que l’on pourrait qualifier de squelette. Ce processus d’héritage ne s’arrête pas à un seul niveau de descendance. Il est tout à fait possible de créer un descendant de la classe Voiture comme par exemple une classe Monospace développant sa spécialisation.

package fr.tannoy.garage
{
	public class Monospace extends Voiture
	{
		public function Monospace()
		{
			super();
		}
 
		private var _numPlaces:int = 5;
		public function get numPlaces():int
		{
			return _numPlaces;
		}
		public function set numPlaces(value:int):void
		{
			_numPlaces = value;
		}
 
		public function passerEnSeptPlaces():void
		{
 
		}
	}
}
 
package fr.tannoy
{
	import fr.tannoy.garage.Monospace;
	import fr.tannoy.garage.Moto;
	import fr.tannoy.garage.Voiture;
 
 
	public class Main
	{
		public function Main()
		{
			var voiture:Voiture = new Voiture();
			voiture.marque = "Renault";
			trace(voiture.marque);
			var moto:Moto = new Moto();
			moto.marque = "Honda";
			trace(moto.marque);
 
			voiture.numPortes = 3;
			voiture.ouvrirVitres();
 
			var monospace:Monospace = new Monospace();
			monospace.marque = "Opel";
			monospace.numPlaces = 7;
			monospace.passerEnSeptPlaces();
		}
 
	}
}

Empêcher l’héritage

Dans certains cas, il peut-être utile qu’une classe ne puisse être étendue. On utilisera alors l’attribut d’accès final.

package fr.tannoy.garage
{
	public final class Monospace extends Voiture
	{
		public function Monospace()
		{
			super();
		}
 
		private var _numPlaces:int = 5;
		public function get numPlaces():int
		{
			return _numPlaces;
		}
		public function set numPlaces(value:int):void
		{
			_numPlaces = value;
		}
 
		public function passerEnSeptPlaces():void
		{
 
		}
	}
}

Le polymorphisme

Le nom de polymorphisme vient du grec et signifie qui peut prendre plusieurs formes. Cette caractéristique est un des concepts essentiels de la programmation orientée objet. Dans un arbre d’héritage, une méthode polymorphe est une méthode qui a plusieurs formes en fonction de la classe dans laquelle elle se situe.

Redéfinition des méthodes

Dans notre exemple, notre classe de base Vehicule possède une méthode vidange(). Tous nos véhicules pourront donc être vidangés.

package fr.tannoy.garage
{
	public class Vehicule
	{
		public function Vehicule()
		{
		}
 
		private var _marque:String;
		public function get marque():String
		{
			return _marque;
		}
		public function set marque(value:String):void
		{
			_marque = value;
		}
 
		public function vidange():void
		{
			trace("Vidange d'un véhicule de marque : " + marque);
		}
	}
}
 
package fr.tannoy
{
	import fr.tannoy.garage.Garage;
	import fr.tannoy.garage.Monospace;
	import fr.tannoy.garage.Moto;
	import fr.tannoy.garage.Voiture;
 
 
	public class Main
	{
		public function Main()
		{
			var voiture:Voiture = new Voiture();
			voiture.marque = "Renault";
			var moto:Moto = new Moto();
			moto.marque = "Honda";
			voiture.vidange();
			moto.vidange();
		}		
	}
}

Cependant, nous souhaitons redéfinir la façon dont on réalise la vidange d’une moto. Nous allons donc donner une nouvelle forme à la méthode vidange() dans la classe Moto.

Les méthodes statiques ne sont pas héritées et ne peuvent donc pas être redéfinies. Toutefois, les sous-classes héritent des méthodes d'instance et il est donc possible de redéfinir celles-ci sous réserve de deux conditions :

  • la méthode d'instance ne doit pas être déclarée avec le mot-clé final dans la classe de base. Lorsqu'il est utilisé avec une méthode d'instance, le mot-clé final indique que l’on souhaite empêcher les sous-classes de redéfinir cette méthode.
  • la méthode d'instance ne doit pas être déclarée avec le spécificateur de contrôle d'accès private dans la classe de base. Si une méthode est marquée comme private dans la classe de base, il n'est pas nécessaire d'utiliser le mot-clé override lors de la définition d'une méthode portant le même nom dans la sous-classe, puisque la méthode de la classe de base n'est pas visible à partir de la sous-classe.

Pour redéfinir une méthode d'instance correspondant à ces critères, la définition de la méthode dans la sous-classe doit utiliser le mot-clé override et correspondre comme défini ci-dessous à la version de cette méthode dans la super-classe :

  • la méthode de redéfinition doit avoir le même niveau de contrôle d'accès que celle de la classe de base. Les méthodes définies comme internes doivent avoir le même niveau de contrôle d'accès que celles qui n'ont pas de spécificateur de contrôle d'accès.
  • la méthode de redéfinition doit avoir le même nombre de paramètres que celle de la classe de base.
  • les paramètres de la méthode de redéfinition doivent avoir les mêmes annotations de type de données que ceux de la méthode de la classe de base.
  • la méthode de redéfinition doit avoir le même type de renvoi que celle de la classe de base.

Toutefois, il n'est pas nécessaire que les noms des paramètres de la méthode de redéfinition correspondent à ceux des paramètres de la classe de base, tant que le nombre de paramètres et leurs types de données correspondent.

package fr.tannoy.garage
{
	public class Moto extends Vehicule
	{
		public function Moto()
		{
			super();
		}
 
		override public function vidange():void
		{
			trace("Vidange de la moto : " + marque);
			trace("Vérification de la transmission");
		}
	}
}

L'instruction super

Il arrive fréquemment que nous souhaitions compléter le comportement de la méthode de super-classe que nous redéfinissons, plutôt que remplacer ce comportement. Il est donc nécessaire de disposer d'un mécanisme permettant à une méthode d'une sous-classe d'appeler sa « méthode mère » dans la super-classe. Ce mécanisme est assuré par l'instruction super, qui contient une référence à la super-classe immédiate.

package fr.tannoy.garage
{
	public class Voiture extends Vehicule
	{
		public function Voiture()
		{
			super();
		}
 
		private var _numPortes:int = 5;
		public function get numPortes():int
		{
			return _numPortes;
		}
		public function set numPortes(value:int):void
		{
			_numPortes = value;
		}
 
		public function ouvrirVitres():void
		{
			trace("Les vitres sont ouvertes");
		}
 
		override public function vidange():void
		{
			super.vidange();
			trace("Changement du filtre à gasoil pour une voiture diesel");
		}
	}
}

Redéfinition des getters et setters

Bien qu'il soit impossible de redéfinir les variables définies dans une super-classe, il est possible de redéfinir les méthodes de lecture et de définition. Le code suivant redéfinit la méthode de lecture nommée marque:

package fr.tannoy.garage
{
	public final class Monospace extends Voiture
	{
		public function Monospace()
		{
			super();
		}
 
		private var _numPlaces:int = 5;
		public function get numPlaces():int
		{
			return _numPlaces;
		}
		public function set numPlaces(value:int):void
		{
			_numPlaces = value;
		}
 
		public function passerEnSeptPlaces():void
		{
 
		}
 
		override public function get marque():String
		{
			return super.marque.toUpperCase();
		}
	}
}

Interface

Une interface est une définition fonctionnelle d’un type de donnée.

Les interfaces sont basées sur la distinction entre l'interface d'une méthode et l'implémentation de celle-ci. L'interface d'une méthode comprend toutes les informations nécessaires pour appeler cette méthode (le nom de la méthode, l'ensemble des paramètres qu'elle reçoit et le type de données qu'elle renvoie). L'implémentation d'une méthode comprend non seulement les informations de l'interface, mais aussi les instructions exécutables qui caractérisent le comportement de la méthode. La définition d'une interface ne contient que les interfaces de la méthode, et toute classe qui implémente l'interface doit donc définir les implémentations de la méthode.

Il est aussi possible de décrire une interface en disant qu'elle définit un type de données, au même titre qu'une classe. En conséquence, une interface peut être utilisée comme annotation de type, tout comme une classe. En tant que type de données, une interface peut également être utilisée avec des opérateurs, par exemple les opérateurs is et as, qui nécessitent un type de données. Toutefois, à l'inverse d'une classe, il n'est pas possible d'instancier une interface. C'est en raison de cette distinction que l’on voit les interfaces comme des types de données abstraites et les classes comme des types de données concrètes.

La définition d'une interface

La structure de la définition d'une interface est similaire à celle de la définition d'une classe, à ceci près qu'une interface ne peut pas contenir les corps des méthodes. Les interfaces ne peuvent pas comporter des variables ou des constantes, mais elles peuvent contenir des méthodes de lecture et de définition. Pour définir une interface, on utilise le mot-clé interface.

package fr.tannoy.garage
{
	public interface IVidangable
	{
 
	}
}

Prenons le cas de notre garage, il doit vidanger des voitures, des motos mais aussi des tondeuses. Les voitures et les motos sont bien des véhicules et possède bien une méthode vidange() mais pas les tondeuses.

Notre garage doit pourtant appeler la méthode vidange sur chaque élément qu’il doit vidanger. Nous allons donc créer une interface IVidangable contenant la description de la méthode vidange(). Nos classes Vehicule et Tondeuse implémenterons cette interface nous garantissant la présence de la méthode vidange().

package fr.tannoy.garage
{
	public interface IVidangable
	{
		function vidange();
	}
}

Une interface définissant un type de données, nous allons donc modifier la méthode faireLaVidange() de notre classe Garage afin qu’elle prenne en paramètre non plus un objet Vehicule mais un objet de type IVidangable. A partir de cet objet, nous pourrons appeler sa méthode vidange().

package fr.tannoy.garage
{
	public class Garage
	{
		public function Garage()
		{
		}
 
		public function faireLaVidange(engin:IVidangable):void
		{
			engin.vidange();
		}
 
	}
}

Implémentation d'une interface dans une classe

La classe est le seul élément du langage ActionScript 3.0 qui puisse implémenter une interface. Pour implémenter une ou plusieurs interfaces, on utilise le mot-clé implements dans une déclaration de classe. Dans une classe qui implémente une interface, les méthodes implémentées doivent :

  • utiliser l'identificateur de contrôle d'accès public,
  • utiliser le même nom que la méthode de l'interface,
  • avoir le même nombre de paramètres, chacun étant du type de données correspondant au type de données du paramètre équivalent dans la méthode de l'interface,
  • utiliser le même type de retour.

Nous allons donc implémenter l’interface IVidangable dans la classe Vehicule et dans la classe Tondeuse en définissant le comportement de la méthode.

package fr.tannoy.garage
{
	public class Vehicule implements IVidangable
	{
		public function Vehicule()
		{
		}
 
		private var _marque:String;
		public function get marque():String
		{
			return _marque;
		}
		public function set marque(value:String):void
		{
			_marque = value;
		}
 
		public function vidange():void
		{
			trace("Vidange d'un véhicule de marque : " + marque);
		}
	}
}
 
package fr.tannoy.garage
{
	public class Tondeuse implements IVidangable
	{
		public function Tondeuse()
		{
		}
 
		public function vidange():void
		{
			trace("Vidange d'une tondeuse");
		}
 
	}
}

Notre garage pourra donc faire la vidange de ces trois engins en utilisant la même méthode à chaque fois.

package fr.tannoy
{
	import fr.tannoy.garage.Garage;
	import fr.tannoy.garage.Moto;
	import fr.tannoy.garage.Tondeuse;
	import fr.tannoy.garage.Voiture;
 
 
	public class Main
	{
		public function Main()
		{
			var voiture:Voiture = new Voiture();
			voiture.marque = "Renault";
 
			var moto:Moto = new Moto();
			moto.marque = "Honda";
 
			var tondeuse:Tondeuse = new Tondeuse();
 
			var garage:Garage = new Garage();
			garage.faireLaVidange(voiture);
			garage.faireLaVidange(moto);
			garage.faireLaVidange(tondeuse);
		}		
	}
}
Cet article développe l'intérêt qu'il y a à utiliser les interfaces.
Lilive - 01/03/2010