Ludovic ROLAND

Blog technique sur mes expériences de développeur.

Implémenter le pattern Abstract Factory en Kotlin

3 février 2023

Dans cet article, nous allons voir comment implémenter le patron de conception Abstract Factory (ou pattern Abstract Factory en anglais) à l’aide du langage de programmation Kotlin.

Comme évoqué dans un précédent article, le patron de conception Abstract Factory est un patron de conception qui appartient à la famille des patrons de construction.

Qu’est-ce que le pattern Abstract Factory ?

Au moment où j’écris cet article, l’intelligence artificielle ChatGPT affole le web. Il s’agit d’un agent conversationnel qui a réponse à presque tout. Je lui ai donc demandé de m’expliquer en quelques lignes le but du patron de conception Abstract Factory. Voici sa réponse :

L’objectif du patron de conception Abstract Factory est de fournir une interface pour la création de familles d’objets liés sans spécifier leurs classes concrètes. Cela permet de changer la famille d’objets créée sans affecter le code qui utilise ces objets. Il soutient également l’encapsulation des créations d’objets.

Le but du pattern Abstract Factory est donc simple : permettre la création d’objets regroupés en famille tout en s’affranchissant des classes concrètes permettant leur création.

Illustration avec un exemple

Même si la définition proposée par notre intelligence artificielle ChatGPT est plutôt claire, un exemple concret est toujours plus parlant.

Entre 2018 et 2022, j’ai travaillé pour l’entreprise Hager Group. Il s’agit d’un industriel qui propose des solutions et des services pour les installations électriques dans les bâtiments résidentiels, tertiaires et industriels. L’entreprise propose dans son catalogue des milliers de références dont des références en lien avec la protection de l’habitat. On y retrouve notamment des produits non connectés et des produits connectés destinés à être utilisé dans le cadre de la domotique.

L’ensemble de ces produits sont donc exposés grâce à un catalogue qui est alors en charge de créer les différents objets.

Pour chaque produit lié à la protection de l’habitat, nous exploiterons deux éléments en Kotlin :

  • une classe abstraite ;
  • une sous-classe concrète permettant de décrire les propriétés du produit dans sa version non connectée ;
  • une sous-classe concrète permettant de décrire les propriétés du produit dans sa version connectée.

Dans les faits, rien n’empêche notre catalogue d’utiliser ces sous-classes concrètes pour instancier les produits. Cependant, la maintenance du programme peut s’avérer complexe dès lors que l’on souhaite ajouter de nouveaux produits comme par exemple les produits connectés en Bluetooth, les produits connectés via MATTER, les produits connectés via ZigBee, etc. En effet, les modifications à apporter au programme peuvent s’avérer fastidieuse.

C’est justement pour répondre à cette problématique que le pattern Abstract Factory a été créé. Pour résoudre ce problème de maintenance, il convient d’introduire une interface, `FabriqueProduitProtection” dans le cadre de notre exemple, qui contient la signature des méthodes définissant chacun des produits que l’on peut fabriquer. Pour chacune de ces méthodes, Le type de retour est une classe abstraite et non une des sous-classes concrètes. L’objectif est ici de faire en sorte que le catalogue n’est pas nécessairement connaissance des sous-classes concrètes et qu’il puisse être decorrélé des familles de produit.

Revenons le temps d’un paragraphe sur les sous-classes dont on parle depuis quelques lignes maintenant dans cet article. Le but est ici d’introduire une sous-classe pour chaque famille de produits. Pour rappel, dans le cadre de notre exemple, nous avons deux familles de produits :

  • Les produits non connectés ;
  • Les produits connectés ;

Ainsi, on obtient les sous-classes :

  • FabriqueProduitProtectionNonConnecte
  • FabriqueProduitProtectionConnecte

Ces sous-classes ont pour objectif d’implanter les opérations de création du produit approprié pour la famille, connecté ou non, à laquelle elle est associée.

Pour ce faire, le catalogue doit alors prendre en paramètre une instance de classe implémentant l’interface FabriqueProduitProtection, ce qui dans notre cas correspond soit à une instance de la classe FabriqueProduitProtectionNonConnecte, soit une instance de la classe FabriqueProduitProtectionConnecte.

Résumé des classes

Pour résumé, voici l’ensemble des classes et des interfaces nécessaires à la mise en place du patron de conception Abstract Factory :

  • Une interface FabriqueAbstraite, qui dans notre exemple s’appelle FabriqueProduitProtection, qui permet de spécifier les signatures des méthodes créant les différents produits ;
  • Les classes concrètes FabriqueConcrète1 et FabriqueConcrète2, qui dans notre exemple s’appellent FabriqueProduitProtectionConnecte et FabriqueProduitProtectionNonConnecte, qui implantent les méthodes créant les produits pour chaque famille de produits. Puisque ces classes connaissent la famille et le produit, elles peuvent sans problèmle créer une instance du produit pour la famille en question ;
  • Les classes abstraites ProduitAbstraitA et `ProduitAbstraitB, qui dans notre exemple seront un disjoncteur et un interrupteur différentiel, qui sont les classes abstraites des produits. Ici la notion de famille (connecté ou non) n’apparaît pas. Cette notion est réservée aux sous-classes concrètes..

La catalogue, qui se traduira, dans la suite de cet exemple, par le programme principal, utilise alors une instance de l’une des fabriques concrètes pour créer des produits au travers de l’interface de la fabrique abstraite.

Exemple en Kotlin

Maintenant que les bases de notre example sont posées, il est temps de coder un peu. Pour ce faire, nous allons illustrer tout ce qui a été dit plus haut dans ce billet de blog en implémentant notre exemple du patron de conception Abstract Factory, à l’aide du langage de programmation Kotlin.

Commençons par décrire les propriétés d’un premier produit à travers une classe abstraite. Débutons par le disjoncteur :

abstract class Disjoncteur(
  protected val positionNeutre: String,
  protected val nombrePolesProteges: Int,
  protected val nombrePoles: Int,
  protected val typePole: String,
  protected val courbe: Char)
{

  abstract fun afficheCaracteristiques()

}

class DisjoncteurNonConnecte(
  positionNeutre: String,
  nombrePolesProteges: Int,
  nombrePoles: Int,
  typePole: String,
  courbe: Char) 
  : Disjoncteur(positionNeutre, nombrePolesProteges, nombrePoles, typePole, courbe)
{

  override fun afficheCaracteristiques()
  {
    println("DisjoncteurNonConnecte(positionNeutre='$positionNeutre', nombrePolesProteges=$nombrePolesProteges, nombrePoles=$nombrePoles, typePole='$typePole', courbe=$courbe)")
  }

}

class DisjoncteurConnecte(
  positionNeutre: String,
  nombrePolesProteges: Int,
  nombrePoles: Int,
  typePole: String,
  courbe: Char) 
  : Disjoncteur(positionNeutre, nombrePolesProteges, nombrePoles, typePole, courbe)
{

  override fun afficheCaracteristiques()
  {
    println("DisjoncteurConnecte(positionNeutre='$positionNeutre', nombrePolesProteges=$nombrePolesProteges, nombrePoles=$nombrePoles, typePole='$typePole', courbe=$courbe)")
  }

}

Maintenant que le code permettant de décrire des disjoncteurs, qu’ils soient connectés ou pas, est écrit, il convient de faire le même exercice pour le second produit : l’interrupteur différentiel.

abstract class InterrupteurDifferentiel(
  protected val positionNeutre: String,
  protected val nombrePoles: Int,
  protected val modeFixation: String,
  protected val declenchementTemporise: Boolean
)
{

 abstract fun afficheCaracteristiques()

}

class InterrupteurDifferentielNonConnecte(
  positionNeutre: String,
  nombrePoles: Int,
  modeFixation: String,
  declenchementTemporise: Boolean
) : InterrupteurDifferentiel(positionNeutre, nombrePoles, modeFixation)
{

 override fun afficheCaracteristiques()
 {
   println("InterrupteurDifferentielNonConnecte(positionNeutre='$positionNeutre', nombrePoles=$nombrePoles, modeFixation='$modeFixation', declenchementTemporise=$declenchementTemporise)")
 }

}

class InterrupteurDifferentielConnecte(
  positionNeutre: String,
  nombrePoles: Int,
  modeFixation: String,
  declenchementTemporise: Boolean
) : InterrupteurDifferentiel(positionNeutre, nombrePoles, modeFixation)
{

 override fun afficheCaracteristiques()
 {
   println("InterrupteurDifferentielConnecte(positionNeutre='$positionNeutre', nombrePoles=$nombrePoles, modeFixation='$modeFixation', declenchementTemporise=$declenchementTemporise)")
 }

}

Maintenant que la partie liée aux objets métiers est écrites, nous pouvons passer à la suite et nous concentrer l’interface FabriqueProduitProtection ainsi que ses deux implémentations. Pour rappel, le but de chacune des implémentations est d’adresser une famille de produits. Dans notre cas, il s’agit de ma famille des produits connectés et la famille des produits non connectés.

interface FabriqueProduitProtection
{

 fun creeDisjoncteur(positionNeutre: String, nombrePolesProteges: Int, nombrePoles: Int, typePole: String, courbe: Char): Disjoncteur

 fun creeInterrupteurDifferentiel(positionNeutre: String, nombrePoles: Int, modeFixation: String, declenchementTemporise: Boolean): InterrupteurDifferentiel

}

class FabriqueProduitProtectionNonConnecte
 : FabriqueProduitProtection
{

  override fun creeDisjoncteur(positionNeutre: String, nombrePolesProteges: Int, nombrePoles: Int, typePole: String, courbe: Char): Disjoncteur =
    DisjoncteurNonConnecte(positionNeutre, nombrePolesProteges, nombrePoles, typePole, courbe)

  override fun creeInterrupteurDifferentiel(positionNeutre: String, nombrePoles: Int, modeFixation: String, declenchementTemporise: Boolean): InterrupteurDifferentiel =
    InterrupteurDifferentielNonConnecte(positionNeutre, nombrePoles, modeFixation)

}

class FabriqueProduitProtectionConnecte
 : FabriqueProduitProtection
{

  override fun creeDisjoncteur(positionNeutre: String, nombrePolesProteges: Int, nombrePoles: Int, typePole: String, courbe: Char): Disjoncteur =
    DisjoncteurConnecte(positionNeutre, nombrePolesProteges, nombrePoles, typePole, courbe)

  override fun creeInterrupteurDifferentiel(positionNeutre: String, nombrePoles: Int, modeFixation: String, declenchementTemporise: Boolean): InterrupteurDifferentiel =
    InterrupteurDifferentielConnecte(positionNeutre, nombrePoles, modeFixation)

}

Vous noterez que seules les classes d’implémentation utilisent les classes concrètes.

Nous arrivons enfin au bout de notre implémentation du patron de conception et nous allons pouvoir passer à l’écriture d’un programme principal afin d’illustrer son utilisation. A noter que pour des raisons de simplification et de pédagogie, le programme principal va demander à l’utilisateur quelle fabrique il souhaite utiliser, le début de celui-ci consiste à demander la fabrique à utiliser :

const val NB_DISJONCTEURS = 3

const val NB_INTERRUPTEURS_DIFFERENTIELS = 2

fun main()
{
 val disjoncteurs = Array<Disjoncteur?>(NB_DISJONCTEURS) { null }
 val interrupteursDifferentiels = Array<InterrupteurDifferentiel?>(NB_INTERRUPTEURS_DIFFERENTIELS { null }

 print("Voulez-vous utiliser des produits de protection connectés (1) ou non connectés (2) : ")
 val choix = readLine()

 val fabrique = if (choix == "1")
 {
   FabriqueProduitProtectionConnecte()
 }
 else
 {
   FabriqueProduitProtectionNonConnecte()
 }

 for (index in 0 until NB_DISJONCTEURS)
 {
   disjoncteurs[index] = fabrique.creeDisjoncteur("gauche", 1, index, "1P+N", 'C')
 }

 for (index in 0 until NB_INTERRUPTEURS_DIFFERENTIELS)
 {
   interrupteursDifferentiels[index] = fabrique.creeInterrupteurDifferentiel("gauche", index, "rail DIN symétrique", true)
 }

 disjoncteurs.forEach { it?.afficheCaracteristiques() }
 interrupteursDifferentiels.forEach { it?.afficheCaracteristiques() }
}

Libre à vous de jouer alors avec le programme pour tester les différents scénarios offerts par la condition dans le code.

Voici tout de même un exemple d’exécution pour des produits connectés :

Voulez-vous utiliser des produits de protection connectés (1) ou non connectés (2) :
1
DisjoncteurConnecte(positionNeutre='gauche', nombrePolesProteges=1, nombrePoles=0, typePole='1P+N', courbe=C)
DisjoncteurConnecte(positionNeutre='gauche', nombrePolesProteges=1, nombrePoles=1, typePole='1P+N', courbe=C)
DisjoncteurConnecte(positionNeutre='gauche', nombrePolesProteges=1, nombrePoles=2, typePole='1P+N', courbe=C)
InterrupteurDifferentielConnecte(positionNeutre='gauche', nombrePoles=0, modeFixation='rail DIN symétrique', declenchementTemporise=true)
InterrupteurDifferentielConnecte(positionNeutre='gauche', nombrePoles=1, modeFixation='rail DIN symétrique', declenchementTemporise=true)

Dans cet exemple d’exécution, c’est une fabrique de produits connectés qui a été créée. Aussi, le programme contruit des disjoncteurs connectés et des interrupteurs différentiels connectés.

Commentaires