Maison  >  Article  >  Java  >  Exemple d'analyse d'héritage en Java

Exemple d'analyse d'héritage en Java

WBOY
WBOYavant
2023-04-20 17:19:081210parcourir

 Interface et classe ?
 Une fois, j'ai assisté à une réunion d'un groupe d'utilisateurs Java. Lors de la conférence, James Gosling (le père de Java) a prononcé le discours d'initiative. Dans ce segment de questions-réponses mémorable, on lui a demandé : « Si vous refactorisiez Java, que changeriez-vous ? » "Je veux me débarrasser des cours", a-t-il répondu. Une fois les rires calmés, il a été expliqué que le véritable problème n’était pas la classe elle-même, mais la mise en œuvre de la relation d’héritage. L'héritage d'interface (implémente la relation) est meilleur. Vous devez éviter autant que possible d’implémenter l’héritage.
 Perte de flexibilité
 Pourquoi éviter l'héritage ? Le premier problème est que l’utilisation explicite de noms de classes concrets vous enferme dans une implémentation spécifique, ce qui rend les modifications sous-jacentes inutilement difficiles.
 Dans la méthode de programmation agile actuelle, le cœur est le concept de conception et de développement parallèles. Vous commencez à programmer avant de concevoir le programme en détail. Cette technique diffère de l'approche traditionnelle - où la conception doit être terminée avant le début du codage - mais de nombreux projets réussis ont prouvé que vous pouvez développer un code de haute qualité plus rapidement qu'avec la méthode traditionnelle étape par étape. Mais la flexibilité est au cœur du développement parallèle. Vous devez écrire votre code de manière à ce que les exigences nouvellement découvertes puissent être fusionnées dans le code existant aussi facilement que possible.
  Plutôt que d'implémenter les fonctionnalités dont vous pourriez avoir besoin, il vous suffit d'implémenter les fonctionnalités dont vous avez clairement besoin et d'être modérément tolérant au changement. Sans ce type de développement flexible et parallèle, c'est tout simplement impossible.

 La programmation pour Inteface est au cœur de l'architecture flexible. Pour illustrer pourquoi, regardons ce qui se passe lorsqu'ils sont utilisés. Considérons le code suivant :

 f()
 {
 LinkedList list = new LinkedList();
 //...
 g( list );
 }

 g( LinkedList list )
 {
 list.add( . .. );
 g2( list )
 }

  Supposons qu'un besoin d'interrogation rapide soit soulevé, de sorte que cette LinkedList ne puisse pas le résoudre. Vous devez plutôt utiliser HashSet. Dans le code existant, les modifications ne peuvent pas être localisées, car vous devez modifier non seulement f() mais également g() (qui prend un paramètre LinkedList) et tout code auquel g() transmet la liste. Réécrivez le code comme suit :

  f()
 {
 Collection list = new LinkedList();
  //...
 g( list );
 }

 g( Collection list )
 {
 list.add ( ... );
 g2( list )
 }

  Pour modifier la liste chaînée en hachage, cela peut être aussi simple que d'utiliser new HashSet() au lieu de new LinkedList(). c'est tout. Il n'y a rien d'autre à modifier.

 Comme autre exemple, comparez les deux morceaux de code suivants :

  f()
 {
 Collection c = new HashSet();
  //...
 g( c );
 }

 g( Collection c )
  {
 for( Iterator i = c.iterator(); i.hasNext() )
  do_something_with( i.next() );
  }

  et

 f2()
 {
  Collection c = new Hash Ensemble ( ) ;
   //...
 g2( c.iterator() );
 }

 g2( Itérateur i )
 {
 while( i.hasNext() )
 do_something_with( i.next() );
  }
La méthode g2() peut désormais parcourir la dérivation de collection, tout comme vous pouvez obtenir des paires clé-valeur à partir d'une carte. En fait, vous pouvez écrire des itérateurs qui génèrent des données au lieu de parcourir une collection. Vous pouvez écrire des itérateurs qui obtiennent des informations du framework ou du fichier de test. Cela permettrait une grande flexibilité.

 Couplage
Pour la mise en œuvre de l'héritage, un problème plus critique est le couplage --- dépendance ennuyeuse, c'est-à-dire la dépendance d'une partie du programme par rapport à une autre partie. Les variables globales fournissent un exemple classique de la raison pour laquelle un couplage fort peut causer des problèmes. Par exemple, si vous modifiez le type d'une variable globale, toutes les fonctions qui utilisent cette variable peuvent être affectées, donc tout ce code devra être inspecté, modifié et retesté. De plus, toutes les fonctions qui utilisent cette variable sont couplées entre elles via cette variable. Autrement dit, si la valeur d'une variable est modifiée à un moment où elle est difficile à utiliser, une fonction peut affecter de manière incorrecte le comportement d'une autre fonction. Ce problème est considérablement caché dans les programmes multithread.
En tant que concepteur, vous devez vous efforcer de minimiser les relations de couplage. Vous ne pouvez pas éliminer complètement le couplage, car les appels de méthode depuis des objets d'une classe vers des objets d'une autre classe sont une forme de couplage lâche. Vous ne pouvez pas avoir de programme sans aucun couplage. Cependant, vous pouvez minimiser certains couplages en suivant les règles OO (plus important encore, l'implémentation d'un objet doit être complètement cachée aux objets qui l'utilisent). Par exemple, les variables d'instance d'un objet (champs qui ne sont pas des constantes) doivent toujours être privées. Je veux dire pendant un certain temps, sans exception, constamment. (Vous pouvez parfois utiliser efficacement les méthodes protégées, mais les variables d'instance protégées sont une abomination.) Pour la même raison, vous ne devriez pas utiliser les fonctions get/set --- elles semblent simplement trop compliquées pour être publiques sur un domaine (malgré le modificateur de retour La raison pour laquelle on accède aux fonctions des objets plutôt qu'aux valeurs primitives est dans certains cas, et dans ce cas, la classe d'objets renvoyée est une abstraction clé dans la conception).

Ici, je ne suis pas livresque. Dans mon propre travail, je trouve une corrélation directe entre la rigueur de mon approche OO, le développement rapide du code et la facilité de mise en œuvre du code. Chaque fois que je viole les principes centraux de l'OO, tels que le masquage de l'implémentation, je finis par réécrire ce code (généralement parce que le code n'est pas déboguable). Je n'ai pas le temps de réécrire le code, donc je suis ces règles. Suis-je préoccupé par des raisons purement pratiques ? Je ne m'intéresse pas aux raisons pures.

   Le problème fragile de la classe de base

  Maintenant, appliquons le concept de couplage à l'héritage. Dans un système d'implémentation qui utilise des extensions, la classe dérivée est très étroitement couplée à la classe de base, et ce couplage étroit n'est pas souhaitable. Les concepteurs ont appliqué le surnom de « problème de classe de base fragile » pour décrire ce comportement. Les classes de base sont considérées comme fragiles car vous modifiez la classe de base tout en étant apparemment sûre, mais lors de l'héritage d'une classe dérivée, le nouveau comportement peut entraîner un dysfonctionnement de la classe dérivée. Vous ne pouvez pas savoir si les modifications apportées à une classe de base sont sûres en inspectant simplement la classe de base de manière isolée ; vous devez également examiner (et tester) toutes les classes dérivées. De plus, vous devez vérifier tout le code également utilisé dans les objets de classe de base et dérivés, car ce code peut être interrompu par le nouveau comportement. Une simple modification d'une classe de base peut rendre l'ensemble du programme inutilisable.

  Examinons la question des classes de base fragiles et du couplage des classes de base. La classe suivante étend la classe ArrayList de Java pour qu'elle se comporte comme une pile :

 class Stack extends ArrayList
 {
 private int stack_pointer = 0;

 public void push( Object article )
 {
 add( stack_pointer++, article );
}

 public Object pop()
 {
 return Remove( --stack_pointer );
  }

 public void push_many( Object[] articles)
 {
 for( int i = 0; i < articles. length; + +i )
  push( articles[i] );
  }
  }

 Même une classe simple comme celle-ci a des problèmes. Considérez le cas où un utilisateur équilibre l'héritage et utilise la méthode clear() d'ArrayList pour faire apparaître la pile :

 Stack a_stack = new Stack();
 a_stack.push("1");
 a_stack.push("2");
a_stack .clear();

 Ce code se compile avec succès, mais comme la classe de base ne connaît pas la pile du pointeur de pile, l'objet de pile est actuellement dans un état indéfini. Le prochain appel à push() place le nouvel élément à l'index 2. (la valeur actuelle de stack_pointer), donc la pile comporte effectivement trois éléments - les deux derniers sont des déchets. (La classe stack de Java a exactement ce problème, ne l'utilisez pas).

La solution à ce problème de méthode hérité ennuyeux est de remplacer toutes les méthodes ArrayList pour Stack, ce qui peut modifier l'état du tableau, donc le remplacement est correct. Manipuler le pointeur de pile ou lancer une exception. (La méthode RemoveRange() est un bon candidat pour lever une exception).

 Cette méthode présente deux inconvénients. Premièrement, si vous couvrez tout, la classe de base devrait en réalité être une interface et non une classe. Si vous n'utilisez aucune méthode héritée, cela ne sert à rien d'implémenter l'héritage. Deuxièmement, et plus important encore, une pile ne peut pas prendre en charge toutes les méthodes ArrayList. Par exemple, l'ennuyeux removeRange() ne fait rien. La seule manière raisonnable d’implémenter une méthode inutile est de lui faire lever une exception, puisqu’elle ne doit jamais être appelée. Cette méthode transforme efficacement les erreurs de compilation en erreurs d’exécution. Le problème est que si la méthode n’est tout simplement pas définie, le compilateur affichera une erreur de méthode non trouvée. Si une méthode existe mais lève une exception, vous ne découvrirez l'erreur d'appel que lorsque le programme sera réellement en cours d'exécution.

Une meilleure solution à ce problème de classe de base consiste à encapsuler la structure des données au lieu d'utiliser l'héritage. Voici la nouvelle version améliorée de Stack :
class Stack
{
private int stack_pointer = 0;
private ArrayList the_data = new ArrayList(); , article );
 }

 public Object pop()
 {
 return the_data .remove( --stack_pointer );
 }

 public void push_many( Object[] articles )
 {
 for( int i = 0; i < o.length; ++i )
  push( articles[i] ) ;
  }
  }

  Jusqu'ici, tout va bien, mais compte tenu du problème de classe de base fragile, nous disons que vous souhaitez en créer une dans la pile. Variable utilisée pour suivre la taille maximale de la pile sur une période de temps. Une implémentation possible pourrait ressembler à ceci :

class Monitorable_stack extends Stack

  high_water_mark = current_size;
  super.push( article );
 }

 publish Object pop()
 {
 --current_size;
  ​​​​retour super. pop();
 }

 public int maximum_size_so_far()
 {
 return high_water_mark;
 }
 }
Cette nouvelle classe a très bien fonctionné, du moins pendant un certain temps. Malheureusement, ce code exploite le fait que push_many() fonctionne en appelant push(). Tout d’abord, ce détail ne semble pas être un mauvais choix. Cela simplifie le code et vous pouvez obtenir la version dérivée de push() même lorsque Monitorable_stack est accessible via une référence Stack, afin que high_water_mark soit mis à jour correctement.

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Déclaration:
Cet article est reproduit dans:. en cas de violation, veuillez contacter admin@php.cn Supprimer