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.
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 :
Transposé en langage objet :
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.
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.
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.