Problématique de dépendance entre les types
Imaginons qu’on ait une voiture à modéliser. Nous allons définir une structure Car. La voiture a un moteur (Engine), quatre roues, un volant, quatre portes, etc. Vous allez définir, à l’intérieur de la structure Voiture d’autres structures pour les différentes parties. Intéressons-nous au moteur de la voiture. Comment exprimons la dépendance entre un moteur et une voiture ?
Imaginons qu’on ait une voiture à modéliser. Nous allons définir une structure Car. La voiture a un moteur (Engine), quatre roues, un volant, quatre portes, etc. Vous allez définir, à l’intérieur de la structure Voiture d’autres structures pour les différentes parties. Intéressons-nous au moteur de la voiture. Comment exprimons la dépendance entre un moteur et une voiture ?
Solution
1 : Héritage (IS-A)
Par définition, une classe A hérite d’une classe B si A est un type de B (en anglais IS-A, A is a B). La sous-classe A peut redéfinir des méthodes de sa super-classe B afin de les spécialiser. L’héritage représente la relation : EST-UN.
Par définition, une classe A hérite d’une classe B si A est un type de B (en anglais IS-A, A is a B). La sous-classe A peut redéfinir des méthodes de sa super-classe B afin de les spécialiser. L’héritage représente la relation : EST-UN.
object FuelType extends Enumeration{
type FuelType = Value
val Diesel, Essence, Electric = Value
}
trait Engine {
private var running = false
def start: Unit = {
if (!running) println("Engine started")
running = true
}
def stop: Unit = {
if (running) println("Engine stopped")
running = false
}
def isRunning: Boolean = running
def fuelType: FuelType.FuelType
}
trait CarByInheritance extends Engine{
def drive {
start
println("Vroom vroom")
}
def park {
if (isRunning ) println("Break!")
stop
}
}
trait DieselEngine extends Engine{
override def fuelType = FuelType.Diesel
}
//Main
def main(args: Array[String]) {
//par héritage
val myCar = new CarByInheritance with DieselEngine
myCar.drive;
}
Au niveau conception, c'est étrange de faire hériter un Car à un Engine. C'est l’inconvénient majeur de cette solution : un Car n’est pas un Engine !
Solution
2 : Composition (HAS-A)
La relation de composition est caractérisée par une relation du type : Has a, A un, Possède un, Est composé de, Contient …
La relation de composition est caractérisée par une relation du type : Has a, A un, Possède un, Est composé de, Contient …
trait CarByComposition {
def engine : Engine
def drive {
engine.start
println("Vroom vroom")
}
def park {
if (engine.isRunning ) println("Break!")
engine.stop
}
}
//Main
def main(args: Array[String]) {
//par composition
val myEngine = new Engine with DieselEngine
val myCar = new CarByComposition {
override val engine = myEngine
}
myCar.drive;
}
Nous remarquons pour cette solution le manque de
garantie de l’unicité d’un moteur par voiture. Un même moteur peut être utilisé
par plusieurs voitures.
Solution
3 : self-type (REQUIRES-A)
La notion de self-type de scala permet d’exprimer qu’un type voiture (Car) a besoin d’un type moteur (Engine) pour être instancié. Les membres visibles du moteur sont accessibles depuis la voiture. Une voiture n'est pas pour autant un moteur (Engine) (comme dans le cas d’un héritage) mais a juste besoin d'être mixé avec un moteur (Engine). Il est donc impossible d’instancier une voiture sans moteur. Le nouveau design en utilisant self-type :
La notion de self-type de scala permet d’exprimer qu’un type voiture (Car) a besoin d’un type moteur (Engine) pour être instancié. Les membres visibles du moteur sont accessibles depuis la voiture. Une voiture n'est pas pour autant un moteur (Engine) (comme dans le cas d’un héritage) mais a juste besoin d'être mixé avec un moteur (Engine). Il est donc impossible d’instancier une voiture sans moteur. Le nouveau design en utilisant self-type :
trait CarBySelfType {
this: Engine => // self-type
def drive {
start
println("Vroom vroom")
}
def park {
println("Break!")
stop
}
}
//Main
def main(args: Array[String]) {
//par self type
val myCar = new CarBySelfType with DieselEngine
myCar.drive;
}
En plus de pouvoir exprimer une dépendance entre les types (Car dépend Engine), scala nous offre également la possibilité d’exprimer la
dépendance d’un type à un bloc de code. Dans l’exemple suivant, nous exprimons
que le type Car nécessite d’être mixé avec deux méthode start
et stop (tout type qui définit ces deux méthodes peut être mixé) :
trait CarBySelfType {
this:
{ def start: Unit
def stop: Unit
} => // self-type
def drive {
start
println("Vroom vroom")
}
def park {
println("Break!")
stop
}
}
//Main
def main(args: Array[String]) {
//par self type
val myCar = new CarBySelfType with DieselEngine
myCar.drive;
}
Pour finir, un type peut dépendre de plusieurs autres types (les types
sont séparés par le mot clé with).
Par exemple une voiture a besoin d’un moteur (Engine) et une boite de vitesse (GearBox) :
trait GearBox {
def move = println("move")
def gearBoxType : String
}
trait CarBySelfType {
this: Engine with GearBox=> // self-type
def drive {
start
move
println("Vroom vroom")
}
def park {
println("Break!")
stop
}
}
trait AutomaticGearBox extends GearBox{
override def gearBoxType = "Automatic"
}
//Main
def main(args: Array[String]) {
//par self type bloc
val myCar = new CarBySelfType with DieselEngine with AutomaticGearBox
myCar.drive;
}