mardi 28 avril 2015

Scala self-type : Expression de dépendances entre les types

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 ?

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.

  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

 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 :

 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;
  }

Aucun commentaire:

Enregistrer un commentaire