La composition à la rescousse de l'héritage

Publié le 18 février 2016 par Nicolas Cavigneaux | architecture

Cet article est publié sous licence CC BY-NC-SA

La notion d’héritage est un concept qu’on se doit de maîtriser lorsqu’on utilise un langage objet. Si vous souhaitez organiser votre code autour d’objets, il y a de fortes chances que vous soyez confronté au besoin d’utiliser l’héritage.

Bien que l’héritage apporte tout un tas d’avantages indéniables, ce n’est pas la réponse à toutes les problématiques. Si vous tombez dans l’utilisation aveugle de l’héritage, il y a de fortes chances que vous vous en mordiez les doigts quelques mois plus tard quand vous devrez faire évoluer le périmètre fonctionnel de votre application.

Les limites de l’héritage

Bien sûr il est tentant de se dire “Si j’hérite de Foo alors j’aurai toutes ses fonctionnalités sans efforts !”. Mais par la même occasion, vous liez très fortement vos deux classes et si elles viennent à diverger dans le futur vous ne pourrez plus que vous contenter de dire “Ok, elles partagent quand même quelques fonctionnalités…”.

Vous commencerez alors à vous rendre compte qu’il est maintenant difficile de tester chacune de ces classes et qu’il est loin d’être évident de les scinder. Les problèmes commencent. Le souci pour les développeurs non expérimentés est qu’on ne se rend compte de cet état de fait qu’en plein milieu d’un projet ou quand le client souhaite apporter une modification au fonctionnement d’un élément existant. Vous pouvez me croire sur parole, ça finit toujours par arriver.

Il faut savoir détecter les différents types de relations qui peuvent exister entre les classes. Une classe peut être liée à une autre par trois types de relations :

  • une voiture est un véhicule
  • une voiture a un GPS
  • une voiture agit comme un engin qui roule

Transposé en langage objet :

  • est un correspond à de l’héritage
  • a un correspond à de la composition
  • agit comme correspond à un mixin

La composition sera toujours plus flexible qu’un mixin et ne sera pas liée directement à la classe qui l’accueille contrairement à l’héritage.

Un exemple concret

Disons donc qu’on veuille modéliser des véhicules, on pourrait avoir quelque chose comme :

class Vehicle
  attr_accessor :speed

  def initialize(speed)
    @speed = speed
  end
end

class Car < Vehicle
  def drive
    puts "driving at #{speed}"
  end
end

class Helicopter < Vehicle
  def fly
    puts "flying at #{speed}"
  end
end

On a donc maintenant des véhicules qui ont une vitesse, les voitures peuvent rouler et les hélicoptères peuvent voler.

Disons maintenant qu’on souhaite créer une classe pour les avions qui techniquement peuvent rouler et voler. Comment faire ? Notre avion est à mi-chemin entre la voiture et l’hélicoptère.

Bien sûr, on pourrait utiliser les mixins mais ce n’est ni plus ni moins qu’une forme d’héritage multiple. Ça serait beaucoup mieux que notre solution actuelle et permettrait de résoudre notre problème.

L’autre solution serait d’utiliser la composition qui permet d’isoler des comportements dans des classes spécialisées. On va ensuite utiliser des instances de ces classes dans d’autres classes.

Ça permet donc d’avoir des classes propres, concises, sans méthodes superflues et très facilement testables. Utiliser la composition, c’est avoir accès à toute la puissance d’une classe dédiée pour manipuler un objet. Mettons donc ça en place :

class Wheels
  def initialize(vehicle)
    @vehicle = vehicle
  end

  def drive
    puts "driving at #{@vehicle.speed}"
  end
end

class Wings
  # ...
end

class Car < Vehicle
  def drive
    Wheels.new(self).drive
  end
end

class Plane < Vehicle
  def drive
    Wheels.new(self).drive
  end

  def fly
    Wings.new(self).fly
  end
end

Je vous concède que cette solution est plus verbeuse que les mixins mais elle est aussi beaucoup plus flexible et puissante et sera en pratique certainement plus simple à tester.

Pour la démonstration j’ai initialisé les objets Wheels et Wings à la volée mais en pratique on aurait plutôt tendance à faire ce travail d’initialisation dans la méthode initialize ce qui permettrait d’avoir des objets persistants et d’éviter les problèmes de concurrence.

Variez vos outils

En pratique, aucune raison de suivre une méthode précise, pourquoi utiliser la composition, les mixins ou l’héritage de manière exclusive quand on peut mixer les trois ?

Il faut savoir s’adapter et utiliser la solution qui sera la plus flexible. On utilisera donc l’héritage quand c’est nécessaire, rappelez vous “un développeur est une personne”. On passera aux mixins quand on est dans la situation “un développeur agit comme un salarié”. On se tournera probablement vers la composition si cette relation s’avère être quelque chose de complexe, un objet nécessitant une classe dédiée.

Pour résumer la teneur de cet article, pensez vos classes pour qu’elles soient le plus modulaire possible, ne vous enfermez pas dans une boîte de laquelle vous ne pourrez plus sortir par la suite, pensez à bien délimiter les responsabilités de chacun. Si vos tests deviennent difficiles à mettre en place, c’est souvent le signe d’un problème d’architecture qui devrait vous mettre la puce à l’oreille.


L’équipe Synbioz.

Libres d’être ensemble.