Aller au contenu

Concepts de base de la programmation orientée objet

Précédemment, nous avons couvert le parcours historique des langages de programmation primitifs ou bas niveau vers des langages avancés, notamment Java.
Nous avons brièvement vu la motivation pour la dissimulation de l’information, et pourquoi l’encapsulation et la sécurité de type sont des concepts puissants pour rendre nos implémentations plus robustes.
Dans cette conférence, nous plongeons dans les concepts de base de la programmation orientée objet, en détaillant plus particulièrement la philosophie de l’encapsulation et l’interaction de l’héritage avec la sécurité de type.

Conclusion de la conférence

L’encapsulation et l’héritage forment tous deux le tissu de la séparation des préoccupations. En cachant les détails d’implémentation au sein d’une classe, nous pouvons contenir la complexité. L’héritage permet de restructurer notre code pour réutiliser les propriétés existantes des classes (champs et fonctions) sans duplication de code.

Encapsulation et dissimulation de l’information

Lors de la dernière séance, nous avons brièvement vu l’importance de garder une classe secrète.

Pour jouer l’avocat du diable, pourquoi ne pas simplement rendre tous les champs publics ?

  • Beaucoup moins de casse-tête à penser public / private.
  • Tout fonctionne encore, nous pouvons accéder directement à nos données.
  • Notre code est encore plus court, on pourrait même dire que c’est plus élégant que des rangées de getters et setters standard.
Quel est le problème ?

Vous ne pouvez pas compter sur les utilisateurs de vos fonctions pour qu’ils manipulent correctement les internes de l’objet. Très probablement, ils n’ont pas de mauvaises intentions mais ne connaissent simplement pas les détails internes de la classe. Codez de manière défensive, sécurisez les internes de vos objets.

Exemple

Un exemple simple est celui des classes avec des restrictions de base et des plages de valeurs.

Si nous devions implémenter un dé classique, nous nous attendrions à ce que sa valeur soit toujours dans la plage 1-6.

Cependant, si nous avons uniquement des champs publics, il n’y a absolument aucune garantie que cette contrainte soit respectée.

public class Dice {

  public int value = 1;

  public Dice() {
  }
}

Cependant, si nous rendons le champ privé, nous pouvons implémenter une vérification de cohérence de base dans notre setter :

public class Dice {

  private int value = 1;

  public Dice() {
  }

  public int getValue() {
    return value;
  }

  public void setValue(int value) {
    if (value > 6 || value < 1) {
      throw new RuntimeException("Value not allowed!");
    }
    this.value = value;
  }
}

Masquer la complexité

Une autre motivation pour restreindre l’accès à l’information privée est de cacher les informations privées de la classe, c’est-à-dire les détails d’implémentation.

  • Si vous lisez l’heure sur votre réveil, vous voulez juste l’heure – vous ne vous intéressez pas à la façon dont l’horloge fonctionne en interne.
  • Il en va de même pour les classes Java.

Protéger les références profondes

Implémenter correctement les getters est parfois moins trivial qu’on pourrait le penser.

  • Les getters pour les valeurs primitives sont généralement sûrs, personne ne peut modifier la valeur stockée dans notre classe en utilisant le getter.
  • Les getters pour les objets, en revanche, sont une cause fréquente de fuite de secrets de classe. Voici un exemple :
public class University {

  // The list is private and only accessible via a getter so we might think this is all safe
  private List<Student> students;

  // However we can abuse the getter, because it returns a reference, not an object!
  public List<Student> getStudents() {
    return students;
  }
}

Illustration des objects dans le mémoire versus variables:

Il existe plusieurs façons de surmonter ce problème :

  • Si nous sommes l’auteur de la classe retournée, nous pouvons nous assurer qu’il existe une variante en lecture seule, en utilisant une interface ou une classe de base commune (un peu plus là-dessus plus tard aujourd’hui).
  • Si c’est une collection, nous pouvons utiliser Collections.immutable... pour créer une variante immuable.
  • Et enfin, nous pouvons simplement créer une copie profonde du résultat avant de retourner une référence.

Il doit s’agir d’une copie profonde

Les copies seules ne suffisent pas, il doit s’agir d’une copie profonde, c’est-à-dire que chaque objet référencé à l’intérieur doit également être copié. Attention avec Arrays.clone(), cela ne crée que des copies superficielles.
Créer des copies profondes peut être un peu fastidieux, mais il existe quelques astuces pour contourner ce problème (mentionnées en classe).

Classes, héritage et polymorphisme

Commun à la plupart des langages de programmation orientée objet est le concept de hiérarchies.

  • Hiérarchies de classes
  • Hiérarchies d’interfaces
  • Classes implémentant des interfaces
  • etc.

Dans la suite, nous parcourrons les différents scénarios et leur utilité.

Héritage simple

Dans le cas le plus simple, une classe peut hériter des propriétés ou du comportement d’une seule autre classe.

On appelle également...

  • ... la classe fournissant les propriétés et le comportement originaux la super-classe.
  • ... la classe héritant des propriétés et du comportement la sous-classe.

L’intérêt de l’héritage simple est de définir un comportement commun dans la super-classe et un comportement spécialisé dans la sous-classe.

Exemple et visualisation

Toutes les Duck peuvent swim() et quack() :

classDiagram
    class Duck {
        <<Class>>
        +String swim()
        +String quack()
    }

Pour simplifier, supposons que toutes les méthodes retournent une chaîne vocalisant le son de chaque méthode :

  • L’implémentation swim() des Duck retourne "splash splash splash"
  • L’implémentation quack() des Duck retourne "quack quack quack"
public class Duck {

  public String swim() {
    return "splash splash splash";
  }

  public String quack() {
    return "quack quack quack";
  }
}

Nous nous intéressons maintenant à un nouveau type de Canards : les RedheadDuck.

  • Les RedheadDuck sont des Duck et se comportent presque exactement comme la définition existante de Duck.
  • Les RedheadDuck peuvent swim() et quack(). L’implémentation existante est parfaite.

Nous ne voulons pas copier-coller le code existant de Duck, mais puisque les RedheadDuck sont des Duck, nous pouvons simplement étendre l’implémentation existante :

classDiagram
    class Duck {
        <<Class>>
        +String swim()
        +String quack()
    }

    class RedheadDuck {
        <<Class>>
    }

    Duck <|-- RedheadDuck : extends
  • La flèche avec un triangle creux de RedheadDuck vers Duck indique que les deux méthodes sont héritées.
  • La classe RedheadDuck ne copie pas les définitions de méthodes :
class RedheadDuck extends Duck {

  // no methods implemented here
}
  • Néanmoins, ces méthodes existent – elles sont héritées de la super-classe. Ainsi, pour chaque objet RedheadDuck créé, nous pouvons appeler ces méthodes :
public static void main() {
  RedheadDuck redheadDuck = new RedheadDuck();

  // Can be called, and implicitly applies implementation of Duck superclass:
  System.out.println(redheadDuck.swim());
  System.out.println(redheadDuck.quack());
}

Exemple tiré de "Freeman & Freeman, Head First Design Patterns"

Sous-classes multiples

La relation "est-un", bien que non commutative, peut avoir des alternatives parallèles. Autrement dit, plusieurs sous -classes peuvent partager la même super-classe, sans interférer les unes avec les autres.

Si c’est le cas, toutes les sous-classes héritent des mêmes méthodes de la super-classe.

Exemple et visualisation

Nous pouvons ajouter un type supplémentaire de Duck à notre exemple précédent : les Mallard.

  • Les Mallard, comme les RedheadDuck, sont des Duck et doivent hériter de toutes les méthodes communes.
  • Encore une fois, nous ne voulons pas copier-coller le code de la classe Duck, nous utilisons donc simplement une deuxième relation extend :
  class Mallard extends Duck {

}
  • Avec maintenant deux types de Duck, nous pouvons visualiser le diagramme de classes résultant comme suit :
classDiagram
    class Duck {
        <<Class>>
        +String swim()
        +String quack()
    }

    class RedheadDuck {
        <<Class>>
    }

    class Mallard {
        <<Class>>
    }

  Duck <|-- RedheadDuck : extends
  Duck <|-- Mallard : extends

Surcharge de méthode

  • Jusqu’à présent, nos deux sous-classes Duck héritent du comportement exact de la super-classe Duck.
  • Mais il y a un problème : les Mallard sont en fait une espèce avec un cri très fort... plutôt un "QUACK !!!".

Je suis un canard colvert bruyant - QUACK !!!

  • Mais puisque notre classe Mallard extends Duck, elle possède toujours le quack par défaut.
  • Heureusement, nous pouvons modifier le comportement par défaut en surchargeant certaines méthodes.
  class Mallard extends Duck {

  // placing a method with identical signature overrides the inherited super-behaviour.
  // All other methods, e.g. `swim` remain unaffected.
  @Override
  public String quack() {
    return "QUACK !!!";
  }
}

Nous pouvons également visualiser le fait qu’une méthode spécifique a été surchargée :

classDiagram
  class Duck {
      <<Class>>
      +String swim()
      +String quack()
  }

  class RedheadDuck {
      <<Class>>
  }

  class Mallard {
      <<Class>>
      >>
      +String quack()
  }

  Duck <|-- RedheadDuck
  Duck <|-- Mallard

En UML, montrer explicitement les méthodes héritées suggère une implémentation de type Override.

Polymorphisme

Supposons que nous ayons un petit parc avec un tableau de 5 Duck.
Le parc est également censé avoir une méthode makeDuckConcert() qui permet à chaque canard de crier une fois.

public class Park {

  private Duck[] ducks;

  public String makeDuckConcert();
}
  • Malheureusement, nous ne savons pas quel type de canards se trouve dans le tableau.
  • Il pourrait s’agir uniquement de Ducks classiques. Il pourrait s’agir de Mallards. Il pourrait s’agir de RedheadDucks... ou d’un mélange de tous.
  • Alors, comment implémenter la méthode makeDuckConcert pour s’assurer que les bons cris sont entendus ?

Mauvais : vérifications de type

Nous pourrions itérer sur chaque élément du tableau, puis vérifier le type de canard, et ensuite afficher le cri approprié.

// primitive version, using type checks
String makeDuckConcert() {

  // iterate over all ducks (whatever they exact subtype)
  for (int i = 0; i < ducks.length; i++) {

    // Check if it is a very loud duck
    if (ducks[i].getClass() == Mallard.class) {
      System.out.println("QUACK !!!");
    }

    // if it's not a Mallard, print the normal quack
    else {
      System.out.println("Quack");
    }
  }
}

Bien que cette solution fonctionne, pouvez-vous repérer 2 problèmes ?

Quel est le problème avec le code ci-dessus ?

Nous avons réimplémenté le comportement de cri, nous avons besoin de connaissances concrètes sur tous les types de canards pouvant exister.

Bon : Délégation

Nous pouvons faire bien mieux !

  • Chaque canard sait comment crier lui-même – nous n’avons pas besoin de vérifier son type
    • Plutôt que de crier en fonction du type de canard, demandons simplement à chaque canard de crier.
    • Le canard peut appliquer lui-même le comportement approprié.
// improved version, using polymorphism
String makeDuckConcert() {
  for (int i = 0; i < ducks.length; i++) {
    // Just delegate - let the duck itself decide on how to quack.
    ducks[i].quack();
  }
}

Pourquoi cela fonctionne-t-il ?

  • Tous les canards ont garanti une implémentation de quack().
  • Lorsque nous appelons quack(), cela invoque l’implémentation du type d’objet concret – nous ignorons exactement lequel c’est, mais l’objet le sait !

La technique s’appelle polymorphisme. Nous tirons parti du fait que chaque implémentation apporte son propre comportement et délègue l’exécution réelle aux implémentations individuelles.

Quels sont les ingrédients pour une soupe polymorphique ?

1) Une méthode commune. 2) Des implémentations surchargées. 3) Appeler la méthode commune sur le super-type.

Polymorphisme dans d’autres langages

Java possède un système de types strict, c’est-à-dire que le compilateur ne considère les types comme compatibles que s’ils sont explicitement déclarés.

  • Dans les exemples précédents, nous avons pu assigner une instance de RedheadDuck à une variable Duck.
  • Cela n’était possible que parce que nous avons explicitement déclaré que RedheadDuck extends Duck.

Tous les langages n’appliquent pas un système de types strict, par exemple JavaScript ne le fait pas.

  • Le polymorphisme est toujours possible, c’est-à-dire que nous pouvons toujours déléguer le comportement concret d’une méthode à l’instance de l’objet.
  • Comment est-ce possible ? JavaScript (et d’autres langages faiblement typés) appliquent un système de Duck-Typing.

Duck Typing

L’explication informelle du Duck typing est : "Si ça nage comme un canard et ça cancane comme un canard, c’est probablement un canard".
Plus formellement : la compatibilité des objets est déterminée en fonction des méthodes disponibles, plutôt que d’une hiérarchie explicitement déclarée.
En JavaScript : vous créez un tableau d’objets (qui peuvent être compatibles ou non), puis appelez la méthode désirée (qui peut être disponible ou non). Vous ne saurez qu’à l’exécution si cela fonctionne comme prévu.

Types, interfaces et classes abstraites

Systèmes de types stricts

  • Les langages orientés objet stricts appliquent un système de types strict, c’est-à-dire que le compilateur vérifie à la compilation si les assignations de variables sont possibles.
    • Les systèmes de types stricts sont un mécanisme de sécurité pour prévenir les erreurs de programmation. Si le programmeur tente de stocker quelque chose dans une variable incompatible, il est très probable qu’il ait fait une erreur. Le système de types garantit que l’erreur peut être corrigée avant l’exécution du programme, au lieu de provoquer un crash ou pire (comportement imprévisible).
    • Certains langages permettent les déclarations de type, mais uniquement à des fins décoratives, par exemple les indications de type en Python ne préviennent pas des attributions incompatibles.
  • Par exemple, en Java, il n’est pas possible d’assigner une valeur de type String à un champ int :
// Not allowed, only int values can be assigned.
int myMagicNumber = "42";

Sous-types et compatibilité

  • Le mot-clé extends, décrivant l’héritage simple, est l’équivalent verbal d’une relation "est-un".
  • Ce mot-clé permet d’élargir la compatibilité des types :
    • Si chaque RedheadDuck est-un Duck, nous pouvons le stocker dans une variable Duck :
    • Exemple de code :
        // This is ok, because every RedheadDuck is a Duck
        Duck duck = new RedheadDuck();
      

extends n’est pas commutatif

L’héritage de classe n’est pas commutatif, ce qui signifie que bien que chaque objet de la sous-classe puisse être assigné à une variable de la super-classe, cela ne fonctionne pas dans l’autre sens !

Exemple non commutatif :

  • Chaque RedheadDuck est un Duck
  • Mais pas chaque Duck est un RedheadDuck !
// This is not ok, because not every Duck is a RedheadDuck
RedheadDuck redheadDuck = new Duck();

??? question "Que se passe-t-il exactement si nous essayons de stocker une instance du supertype dans une variable du sous-type ?"

Nous recevrons une erreur du **compilateur**, car la relation `est-un` ne fonctionne que dans l’autre sens :  
`error: incompatible types: Duck cannot be converted to RedheadDuck`

Classes abstraites

  • Auparavant, notre super-classe (Duck) était une classe Java standard.
  • Il était tout à fait possible d’instancier de nouveaux objets et d’appeler les méthodes fournies avec :
void initializeSuperClass() {
  Duck duck = new Duck();
  System.out.println(duck.swim());
  System.out.println(duck.quack());
}
  • Cependant, pour être totalement honnête, aucun animal n’est jamais juste un Duck. Dans la vie réelle, chaque animal est une certaine espèce (hélas sous-classe) de Duck. Nous avons vu :
    • Mallard
    • RedheadDuck
  • Comment pouvons-nous empêcher l’instanciation de Duck pur et restreindre les objets aux espèces concrètes ( Mallard, RedheadDuck) ?

Classes abstraites

Le mot-clé abstract indique qu’une classe ne peut pas être instanciée. Elle ne peut servir que de super-classe pour d’autres classes, mais il ne peut y avoir aucune instance de la classe abstraite.

Exemple de classe abstraite

Pour notre hiérarchie précédente, abstract est un bon choix :

  • Une super-classe abstract Duck empêche l’instanciation d’objets Duck (les Mallard et RedheadDuck ne sont pas affectés).
  • Les sous-classes héritent toujours de toutes les méthodes implémentées, c’est-à-dire que Mallard et RedheadDuck ont un comportement par défaut pour swim() et quack().

En termes de code, cela se traduit par :

public abstract class Duck {

  public String swim() {
    return "splash splash splash";
  }

  public String quack() {
    return "quack quack quack";
  }
}

Le langage de modélisation unifié (UML) marque également les classes abstraites avec un stéréotype correspondant :

  classDiagram
    class Duck {
        <<Abstract>>
        +String swim()
        +String quack()
    }

    class RedheadDuck {
        <<Class>>
    }

    class Mallard {
        <<Class>>
    }

    Duck <|-- RedheadDuck : extends
    Duck <|-- Mallard : extends

Quelle est la conséquence, en termes de code ?

  • La classe Duck ne peut plus être instanciée.
  • Les RedheadDuck et Mallard peuvent toujours être instanciés (et assignés à des variables Duck) comme auparavant.
void initializationExample() {

  // This does not work ! Duck is abstract and cannot be instantiated
  Duck duck = new Duck();

  // But this does work: The sub-classes are not abstract and can be instantiated. The still inherit a method implementation from the super-class:
  Duck mallard = new Mallard();
  Duck redheadDuck = new redheadDuck();
  mallard.quack();
  mallard.swim();
  redheadDuck.quack();
  redheadDuck.swim();
}

Héritage multiple

  • L’héritage n’implique souvent pas seulement une paire super-classe/sous-classe, mais une chaîne plus longue.
  • Par exemple, nous pouvons imaginer que les Duck, puisqu’ils sont des Bird, devraient également avoir une méthode fly().
    • La méthode fly() ne devrait pas être implémentée dans Duck, car d’autres oiseaux non-canards peuvent également voler.
classDiagram
    class Bird {
        <<Abstract>>
        +String fly()
    }

    class Duck {
        <<Abstract>>
        +String swim()
        +String quack()
    }

    class RedheadDuck {
        <<Class>>
    }

    class Mallard {
        <<Class>>
        >>
    }

    Bird <|-- Duck : extends
    Duck <|-- RedheadDuck : extends
    Duck <|-- Mallard : extends

L’héritage est transitif

Avec Duck étendant Bird et RedheadDuck + Mallard étendant Duck, la méthode fly() est héritée par toutes les instances de RedheadDuck et Mallard. L’héritage est transitif.

Un effet secondaire pratique est que nous pouvons maintenant ajouter autant de nouveaux sous-types de Duck que nous voulons, ils pourront toujours fly(), swim() et quack().

Que doit-on ajouter à Duck pour hériter de Bird comme super-classe ?

extends Bird ! Donc la déclaration complète de la classe est maintenant public abstract class Duck extends Bird

Le problème du diamant

  • Jusqu’à présent, nous avons seulement considéré les cas où une classe hérite (extends) d’une seule super-classe.
  • Mais nous pourrions envisager ce qui devrait se passer lorsqu’une classe hérite de plusieurs super-classes :
classDiagram
    class Vehicle {
        <<Abstract>>
        +String accelerate()
        +String slowDown()
    }

    class Electric {
        <<Abstract>>
        +String accelerate()
        +String chargeBattery()
    }

    class Combustion {
        <<Abstract>>
        +String accelerate()
        +String fuelUp()
    }

    class Hybrid {
        <<Class>>
    }

    Vehicle <|-- Electric : extends
    Vehicle <|-- Combustion : extends
    Electric <|-- Hybrid : extends
    Combustion <|-- Hybrid : extends

Ici, nous pouvons dire que les classes Electric et Combustion fournissent chacune leur propre méthode accelerate(), car elles utilisent en interne des sources d’énergie différentes pour mettre le véhicule en mouvement.

Pourquoi cela s’appelle-t-il le problème du diamant ?

La hiérarchie de classes résultante, telle qu’affichée dans le diagramme UML, montre deux chemins d’extension alternatifs. Le problème est que Hybrid hérite maintenant de deux implémentations conflictuelles de accelerate(). Laquelle doit l’emporter ? Il n’y a pas de bonne réponse, donc certains langages, comme Java, interdisent complètement la double (ou plus) héritage. En Java, il n’est pas possible d’étendre plus d’une super-classe directe.

Interfaces

  • Avec le "problème du diamant", nous avons vu comment l’héritage multiple, c’est-à-dire une classe ayant plusieurs super-classes directes, conduit facilement à des conflits.
  • Par conséquent, de nombreux langages OO, notamment Java, n’autorisent pas l’héritage multiple.
  • Cependant, il existe de bonnes raisons d’exiger que les classes fournissent certaines méthodes spécifiques.
  • Cela est possible avec les interfaces en Java :
    • Comme pour l’héritage de classe, une classe Java peut implements une interface donnée.
    • Avec le mot-clé implements, la classe doit fournir toutes les méthodes "mentionnées" dans l’interface.
    • L’interface ne fournit aucune implémentation réelle, seulement des signatures de méthodes.

Exemples d’interfaces dans la vie réelle :

  • Prises électriques : quel que soit l’appareil électrique que vous concevez, il doit respecter les spécifications. (Et nous connaissons tous, en voyage, les frustrations liées aux appareils qui ne correspondent pas à la spécification de l’interface.)
  • Pédales de véhicule : embrayage à gauche, frein au milieu, accélérateur à droite – mieux vaut ne pas commercialiser une voiture avec d’autres dispositions. Cela causerait des problèmes.

Les interfaces sont des contrats

Les interfaces ne fournissent aucune implémentation, mais constituent un contrat technique. Quel que soit l’appareil (ou la classe) qui prétend implement une interface, le développeur doit s’assurer que les spécifications de l’interface sont respectées.

Exemple et illustration

En UML, les interfaces sont décorées avec le stéréotype <<Interface>>, et les classes qui les implémentent sont reliées par une flèche en pointillé :

classDiagram
    class Vehicle {
        <<Interface>>
        +String accelerate()
        +String slowDown()
    }

    class Electric {
        <<Interface>>
        +String chargeBattery()
    }

    class Combustion {
        <<Interface>>
        +String fuelUp()
    }

    class Hybrid {
        <<Class>>
        +String accelerate()
        +String slowDown()
        +String chargeBattery()
        +String fuelUp()
    }

    Vehicle <|-- Electric : extends
    Vehicle <|-- Combustion : extends
    Electric <|.. Hybrid : implements
    Combustion <|.. Hybrid : implements

Pour atteindre une implémentation double d’interface, les parents directs peuvent simplement être listés dans la définition de la classe :

public class Hybrid implements Electric, Combustion {

  @Override
  public String accelerate() {
    ...
  }

  @Override
  public String slowDown() {
    ...
  }

  @Override
  public String chargeBattery() {
    ...
  }

  @Override
  public String fuelUp() {
    ...
  }

Notez comment la classe "Audi" liste les deux méthodes, bien qu’elles soient déjà définies dans l’interface. C’est parce que l’interface ne fournit pas d’implémentation, c’est-à-dire que ces méthodes doivent être implémentées dans la classe Audi. Il n’y a pas de comportement par défaut à hériter comme c’était le cas avec l’extension d’une super-classe.

Extends vs implements

Dans l’exemple ci-dessus, nous avons également une hiérarchie implicite d’interfaces, c’est-à-dire que Electric et Combustion extends tous deux l’interface de base Vehicle.
Au départ, il peut y avoir une petite confusion sur le moment d’utiliser implements et celui d’utiliser extends, mais il existe une règle simple à mémoriser : "Extends fonctionne uniquement pour des concepts identiques".

Super concept Sous concept Mot-clé
Class Class extends
Interface Interface extends
Interface Class implements
Class Interface (*)
Que faut-il utiliser pour (*) ?

Rien ! Une interface définit contractuellement les méthodes à implémenter par une classe, elle ne peut pas hériter d’une classe. Éliminez cette combinaison, elle n’a aucun sens.

Combinaisons de mots-clés restreintes

L’héritage de classes et les extensions d’interfaces sont un tissu puissant pour de nombreux concepts orientés objet. Cependant, certaines combinaisons de mots-clés sont restreintes, car elles entraîneraient des conflits conceptuels :

Pas de méthodes privées dans les interfaces

  • Parfois, nous aimerions forcer les implémentations à contenir une méthode privée, utile comme aide interne pour l’implémentation.
  • Malheureusement, cela va à l’encontre de la motivation des interfaces : les méthodes private sont des détails d’implémentation, et les interfaces sont un contrat pour des interactions en boîte noire.
  • En fait, il n’est même pas nécessaire de placer le mot-clé public devant les méthodes d’une interface Java : toutes les méthodes d’une interface sont par défaut publiques.

Petite précision : les interfaces peuvent en fait contenir des méthodes privées, mais elles ne peuvent être appelées que par des méthodes default / static et ne sont jamais héritées.

Pas de méthodes statiques dans les classes abstraites

  • Lors de l’implémentation d’une classe abstraite commune, nous aimerions parfois fournir du code statique, disponible pour toutes les instances, par exemple pour un singleton intégré.
  • Malheureusement, cela constitue une contradiction inhérente :
    • abstract pour une classe signifie : une sous-classe est requise pour que cette fonctionnalité existe
    • static pour une méthode signifie : cette fonctionnalité peut être accessible, même si aucune instance n’existe.
  • Ainsi, en combinaison, on dit simultanément qu’un élément ne doit pas exister et que la fonctionnalité est attribuée à ce qui ne doit pas exister.

Bibliographie

Inspiration et lectures complémentaires pour les esprits curieux :

Voici le lien pour accéder à l’unité de labo : Lab 03