Fermer

novembre 27, 2019

Un guide pratique sur les classes d'héritage, d'interfaces et d'abstraits


À propos de l'auteur

Ryan M. Kay est un développeur autodidacte de développeurs mobiles Java, Kotlin et Android qui se passionne pour l'architecture logicielle appliquée, le développement piloté par les tests et l'écriture…
Plus d'informations sur
Ryan
Kay

On peut soutenir que le pire moyen d'enseigner les bases de la programmation est de décrire ce qu'est une chose, sans préciser comment et quand l'utiliser. Dans cet article, Ryan M. Kay discute de trois concepts fondamentaux de la programmation orientée objet dans les termes les moins ambigus, afin que vous ne puissiez plus jamais vous demander quand utiliser l'héritage, les interfaces ou les classes abstraites. Les exemples de code fournis sont en Java avec quelques références à Android, mais seules des connaissances de base de Java sont nécessaires.

Pour autant que je sache, il est rare de trouver du contenu éducatif dans le domaine du développement logiciel qui fournit un mélange approprié d'informations théoriques et pratiques. Si je devais comprendre pourquoi, je suppose que c'est parce que les personnes qui se concentrent sur la théorie ont tendance à se lancer dans l'enseignement et que celles qui se concentrent sur des informations pratiques ont tendance à être payées pour résoudre des problèmes spécifiques, à l'aide de langages et d'outils spécifiques.

, bien sûr, une large généralisation, mais si nous l’acceptons brièvement comme argument, il s’ensuit que beaucoup de personnes (qui ne font pas tout le monde) assument le rôle d’enseignant, ont tendance à être soit pauvres, soit tout à fait incapables d’expliquer les connaissances pratiques pertinentes pour un concept particulier.

Dans cet article, je ferai de mon mieux pour discuter des trois mécanismes principaux que vous trouverez dans la plupart des langages de programmation orientée objet (POO): Héritage interfaces (alias protocoles ) et classes abstraites . Plutôt que de vous donner des explications verbales techniques et complexes sur chaque mécanisme je ferai de mon mieux pour me concentrer sur ce qu'ils font et lorsque les utilise

Cependant, avant de les aborder individuellement, je voudrais aborder brièvement ce que signifie donner une explication théoriquement valable, mais pratiquement inutile. J'espère que vous pourrez utiliser ces informations pour vous aider à parcourir différentes ressources pédagogiques et à éviter de vous culpabiliser lorsque les choses n'ont pas de sens.

Different Degrees Of Knowing

Connaître les noms

Connaître le nom de quelque chose est sans doute la forme la plus superficielle de savoir. En fait, un nom n'est généralement utile que dans la mesure où il est couramment utilisé par de nombreuses personnes pour désigner la même chose et / ou qu'il est utile de décrire la chose. Malheureusement, comme tous ceux qui ont passé du temps dans ce domaine ont découvert que de nombreuses personnes utilisent des noms différents pour la même chose (par exemple, interfaces et protocoles ), les mêmes noms pour différentes choses (par exemple: modules et composants ), ou des noms qui sont ésotériques au point d’être absurde (par exemple Soit Monad . En fin de compte, les noms ne sont que des pointeurs (ou des références) vers des modèles mentaux, et ils peuvent être plus ou moins utiles.

Pour rendre ce domaine encore plus difficile à étudier, je devinerais que pour la plupart des individus, l'écriture de code est: (ou du moins était) une expérience très unique. Il est encore plus compliqué de comprendre comment ce code est finalement compilé en langage machine et représenté dans la réalité physique sous la forme d’une série d’impulsions électriques évoluant dans le temps. Même si on peut se rappeler les noms des processus, concepts et mécanismes utilisés dans un programme, rien ne garantit que les modèles mentaux que l'on crée pour de telles choses sont compatibles avec les modèles d'un autre individu; encore moins si elles sont objectivement exactes.

C'est pour ces raisons, à côté du fait que je n'ai pas naturellement une bonne mémoire pour le jargon, que je considère les noms comme l'aspect le moins important de la connaissance. Cela ne veut pas dire que les noms sont inutiles, mais dans le passé, j’ai appris et utilisé de nombreux modèles de conception dans le seul but de connaître les noms couramment utilisés des mois, voire des années plus tard.

Connaître les définitions et les analogies verbales [19659010] Les définitions verbales sont le point de départ naturel pour décrire un nouveau concept. Cependant, comme pour les noms, ils peuvent être plus ou moins utiles et pertinents. cela dépend en grande partie des objectifs finaux de l'apprenant. Le problème le plus courant que je vois dans les définitions verbales est la connaissance supposée généralement sous la forme d'un jargon.

Supposons, par exemple, que je devais expliquer qu'un fil de ressemble beaucoup à un processus sauf que les fils occupent le même espace adresse d'un processus donné . Pour quelqu'un qui connaît déjà les processus et les espaces d'adressage j'ai essentiellement déclaré que les fils peuvent être associés à leur compréhension du processus ] (c’est-à-dire qu’elles possèdent bon nombre des mêmes caractéristiques), mais elles peuvent être différenciées sur la base d’une caractéristique distincte.

Pour quelqu'un qui ne possède pas cette connaissance, au mieux je n’ai aucun sens, et au pire je cause l’apprenant. se sentir insuffisant en quelque sorte pour ne pas savoir les choses je suppose qu'ils devraient savoir. En toute honnêteté, cela est acceptable si vos apprenants doivent réellement posséder de telles connaissances (telles qu'enseigner à des étudiants diplômés ou à des développeurs expérimentés), mais j'estime que c'est un échec monumental de le faire dans les documents de niveau d'introduction.

C'est souvent le cas. Il est très difficile de donner une bonne définition verbale d’un concept qui ne ressemble à rien de ce que l’apprenant a vu auparavant. Dans ce cas, il est très important que l'enseignant choisisse une analogie qui est probablement familière à la personne moyenne et qui est également pertinente dans la mesure où elle traduit bon nombre des mêmes qualités du concept.

Par exemple, il est Il est extrêmement important pour un développeur de logiciel de comprendre ce que cela signifie lorsque des entités logicielles (différentes parties d'un programme) sont étroitement couplées ou faiblement couplées . Lors de la construction d'un abri de jardin, un charpentier junior peut penser qu'il est plus rapide et plus facile de l'assembler à l'aide de clous au lieu de vis. Cela est vrai jusqu'au moment où une erreur est commise ou qu'un changement de conception de l'abri de jardin nécessite de reconstruire une partie de l'abri.

À ce stade, la décision d'utiliser des clous pour rapprocher le couple Les parties de l'abri de jardin réunies ont rendu le processus de construction dans son ensemble plus difficile, voire plus lent, et l'extraction des clous avec un marteau risquerait d'endommager la structure. À l'inverse, l'assemblage des vis peut prendre un peu plus de temps, mais elles sont faciles à retirer et présentent peu de risque d'endommager les parties voisines de la remise. C'est ce que je veux dire par faiblement couplé . Naturellement, il existe des cas où vous avez vraiment besoin d'un clou, mais cette décision doit être guidée par la pensée critique et l'expérience.

Comme je le détaillerai plus loin, il existe différents mécanismes permettant de relier les différentes parties d'un programme. fournir différents degrés de couplage ; comme les clous et les vis . Bien que mon analogie puisse vous avoir aidé à comprendre ce que signifie ce terme d’une importance capitale, je ne vous ai pas donné la moindre idée de la façon de l’appliquer en dehors du contexte de la construction d’un abri de jardin. Cela m'amène au type de connaissance le plus important et à la clé pour comprendre en profondeur des concepts vagues et difficiles dans n'importe quel domaine de recherche; Bien que nous nous en tenions à écrire du code dans cet article.

Connaître le code

À mon avis, en ce qui concerne le développement de logiciels, la forme la plus importante de la connaissance d’un concept provient de sa possibilité de l’utiliser dans le code de l’application de travail. Cette forme de connaissance peut être obtenue simplement en écrivant beaucoup de code et en résolvant de nombreux problèmes différents; les noms de jargon et les définitions verbales n’ont pas besoin d’être inclus.

D’après ma propre expérience, je me souviens avoir résolu le problème de la communication avec une base de données distante et une base de données locale via une interface unique (vous saurez ce que cela signifie). signifie bientôt si vous ne l'avez pas déjà); plutôt que le client (quelle que soit la classe qui communique avec l'interface ) ait besoin d'appeler explicitement le serveur distant et local (ou même une base de données de test). En fait, le client n'avait aucune idée de ce qu'il y avait derrière l'interface. Je n'avais donc pas besoin de la changer, qu'elle soit exécutée dans une application de production ou dans un environnement de test. Environ un an après avoir résolu ce problème, je suis tombé sur le terme «motif de façade», et peu de temps après le terme «motif de référentiel», qui sont les deux noms que les gens utilisent pour la solution décrite précédemment.

Tout ce préambule J'espère que nous pourrons éclairer certains des défauts les plus fréquents dans l'explication de sujets tels que héritage interfaces et classes abstraites . l'héritage est probablement le plus simple à utiliser et à comprendre. D'après mon expérience à la fois en tant qu'étudiant en programmation et en tant qu'enseignant, les deux autres sont presque toujours un problème pour les apprenants, sauf si une attention toute particulière est portée à éviter les erreurs discutées plus tôt. À partir de ce moment, je ferai de mon mieux pour rendre ces sujets aussi simples qu'ils devraient l'être, mais pas plus simples.

Note sur les exemples

Étant moi-même très au courant du développement d'applications mobiles Android, je vais utiliser des exemples. pris de cette plate-forme afin que je puisse vous apprendre à construire des applications GUI en même temps que l'introduction des fonctionnalités de langage de Java. Cependant, je n’entrerai pas dans les détails, les exemples devraient être incompréhensibles pour une personne ayant une connaissance superficielle de Java EE, Swing ou JavaFX. Mon but ultime en discutant de ces sujets est de vous aider à comprendre ce qu’ils signifient dans le contexte de la résolution d’un problème dans n'importe quel type d’application.

Je voudrais également vous avertir, cher lecteur, que cela peut parfois Il me semble que je suis inutilement philosophique et pédant sur des mots spécifiques et leurs définitions. La raison en est qu’il faut véritablement une base philosophique profonde pour comprendre la différence entre quelque chose qui est concret (réel) et quelque chose qui est abstrait (moins détaillé qu’un réel chose). Cette compréhension s'applique à beaucoup de choses en dehors du domaine de l'informatique, mais il est particulièrement important que tout développeur de logiciel connaisse la nature des abstractions de . Dans tous les cas, si mes mots vous manquent, les exemples de code ne le seront pas.

Héritage et implémentation

Lorsqu'il s'agit de créer des applications avec une interface utilisateur graphique, héritage est sans doute le mécanisme le plus important permettant de construire rapidement une application.

Bien que l’utilisation de héritage soit moins bien comprise, elle sera examinée ultérieurement, mais le principal avantage est de partager la mise en oeuvre entre classes . Ce mot «mise en œuvre», du moins aux fins de cet article, a un sens distinct. Pour donner une définition générale du mot en anglais, je pourrais dire que mettre en œuvre quelque chose revient à le rendre réel .

Donner une définition technique propre au développement de logiciels, Je pourrais dire que mettre en œuvre un logiciel, c’est écrire du béton en lignes de code qui répondent aux exigences dudit logiciel. Par exemple, supposons que j'écris une méthode de la somme :
double somme privée (double premier, double seconde) {

 double somme privée (double premier, double seconde) {
        // TODO: implémenter
}

L'extrait ci-dessus, même si je l'ai écrit jusqu'à écrire un type de retour ( double ) et une déclaration de méthode qui spécifie les arguments ( premier, deuxième ) et le nom qui peut être utilisé pour appeler ladite méthode ( sum ), il n'a pas été mis en œuvre . Pour mettre en œuvre nous devons compléter le méthode method comme suit:

 privé double somme (double premier, double seconde) {
        retourne en premier + deuxième;
}

Naturellement, le premier exemple ne compilerait pas, mais nous verrons un instant que les interfaces sont un moyen d'écrire ces sortes de fonctions non implémentées sans erreur. Héritage en Java

Vraisemblablement, si vous lisez cet article, vous avez déjà utilisé le mot clé au moins une fois. La mécanique de ce mot clé est simple et le plus souvent décrite à l'aide d'exemples relatifs à différents types d'animaux ou de formes géométriques; Dog et Cat s’étendent Animal et ainsi de suite. Je suppose que je n'ai pas besoin de vous expliquer la théorie des types rudimentaire. Passons donc au principal avantage de héritage à Java, via le mot clé extended .

Construire une application «Hello World» basée sur la console en Java est très simple. En supposant que vous possédiez un compilateur Java ( javac ) et un environnement d’exécution ( jre ), vous pouvez écrire une classe contenant une fonction principale de la manière suivante:

 Classe publique JavaApp {
     public static void main (String [] args) {
        System.out.println ("Hello World");
     }
}

Construire une application graphique en Java sur presque toutes les plateformes principales (Android, Entreprise / Web, Bureau), avec l'aide d'un IDE pour générer le code squelette / passe-partout d'une nouvelle application, est également relativement facile grâce au mot-clé étend .

Supposons que nous ayons une structure XML appelée activity_main.xml (nous construisons généralement des interfaces utilisateur de manière déclarative dans Android, via des fichiers Layout) contenant un ] TextView (comme une étiquette de texte) appelé tvDisplay :

 


    



Supposons également que tvDisplay dise «Bonjour tout le monde!». Pour ce faire, nous devons simplement écrire une classe utilisant le mot clé extended pour hériter du Activité Classe:

 import android.app.Activity;
importer android.os.Bundle;
importer android.widget.TextView;

Classe publique MainActivity étend Activity {
    @Passer outre
    Void protégé onCreate (Bundle savedInstanceState) {
        super.onCreate (savedInstanceState);
        setContentView (R.layout.activity_main);


        ((TextView) findViewById (R.id.tvDisplay)). SetText ("Hello World");
    }

L'effet de l'héritage de la mise en œuvre de la classe Activité peut être mieux apprécié en jetant un coup d'œil à son code source . Je doute fort qu'Android soit devenu la plate-forme mobile dominante s'il fallait mettre en œuvre même une petite partie des plus de 8 000 lignes nécessaires pour interagir avec le système, afin de générer une simple fenêtre avec du texte. Héritage est ce qui nous permet de ne pas avoir à reconstruire le cadre Android ou toute plate-forme avec laquelle vous travaillez, à partir de zéro.

L'héritage peut être utilisé pour l'abstraction

] Dans la mesure où il peut être utilisé pour partager une implémentation entre plusieurs classes, héritage est relativement simple à comprendre. Cependant, il existe un autre moyen important d'utiliser l'héritage conceptuellement en relation avec les classes d'interface et abstraites dont nous discuterons bientôt. 19659005] Si vous voulez bien, supposons un instant que l'abstraction, utilisée dans son sens le plus général, soit une représentation moins détaillée d'une chose . Au lieu de qualifier cela par une longue définition philosophique, je vais essayer de montrer comment les abstractions fonctionnent dans la vie quotidienne, et de les discuter rapidement par la suite en termes de développement logiciel.

Supposons que vous voyagiez en Australie , et vous savez que la région que vous visitez est l’hôte d’une densité particulièrement élevée de serpents taipans de l’intérieur (apparemment très toxiques). Vous décidez de consulter Wikipedia pour en savoir plus à leur sujet en consultant des images et d’autres informations. Ce faisant, vous êtes maintenant parfaitement conscient d'un type particulier de serpent que vous n'avez jamais vu auparavant.

Les abstractions, idées, modèles ou tout autre nom que vous souhaitez appeler, sont des représentations moins détaillées d'une chose. Il est important qu’ils soient moins détaillés que la réalité car un vrai serpent peut vous mordre; les images sur les pages Wikipedia ne le font généralement pas. Les abstractions sont également importantes car les ordinateurs et le cerveau humain ont une capacité limitée de stockage, de communication et de traitement des informations. Disposer d'assez de détails pour utiliser ces informations de manière pratique, sans occuper trop de place dans la mémoire, permet aux ordinateurs et aux cerveaux humains de résoudre les problèmes de la même manière.

Rapprocher ces informations d'un héritage les trois sujets principaux dont je discute ici peuvent être utilisés comme abstractions ou mécanismes de l’abstraction . Supposons que dans le fichier de présentation de notre application «Hello World», nous décidions d'ajouter un ImageView Button et un ImageButton :



    

Supposons également que notre activité a mis en œuvre View.OnClickListener pour gérer les clics:

 La classe publique MainActivity étend Activity implémente View.OnClickListener {
    bouton privé b;
    imageButton ib privée;
    imageView privé iv;


    @Passer outre
    Void protégé onCreate (Bundle savedInstanceState) {
        super.onCreate (savedInstanceState);
        setContentView (R.layout.activity_main);

        // ...
        b = findViewById (R.id.imvDisplay) .setOnClickListener (this);
        ib = findViewById (R.id.btnDisplay) .setOnClickListener (this);
        iv = findViewById (R.id.imbDisplay) .setOnClickListener (this);
    }

    @Passer outre
    public void onClick (Afficher la vue) {
        ID final final = view.getId ();
        // gérer le clic en fonction de l'id ...
    }
}

Le principe clé ici est que Button ImageButton et ImageView héritent de la classe View . Le résultat est que cette fonction onClick peut recevoir des événements de clic provenant d'éléments d'interface utilisateur disparates (bien que hiérarchiquement liés) en les référençant comme leur classe parent moins détaillée. C'est beaucoup plus pratique que de devoir écrire une méthode distincte pour gérer tous les types de widgets sur la plateforme Android (sans parler des widgets personnalisés).

Interfaces And Abstraction [19659002] Vous avez peut-être trouvé l’exemple de code précédent un peu sans intérêt, même si vous compreniez pourquoi je l’avais choisi. Pouvoir partager la mise en œuvre à travers une hiérarchie de classes est extrêmement utile, et je dirais que c'est la principale utilité de l'héritage . En ce qui concerne l’autorisation de traiter un ensemble de classes ayant une classe parent commune égale sur un type (c’est-à-dire comme la classe parent de classe ), cette caractéristique de l'héritage a un usage limité.

Par limitation, je parle de l'exigence selon laquelle les classes enfant doivent appartenir à la même hiérarchie de classes pour pouvoir être référencées via, ou appelée classe parente. En d'autres termes, l'héritage est un mécanisme très restrictif pour l'abstraction . En fait, si je suppose que l'abstraction est un spectre qui varie entre différents niveaux de détail (ou d'information), je pourrais dire que l'héritage est le moins abstrait mécanisme pour l'abstraction de dans Java.

Avant de discuter des interfaces de je voudrais mentionner qu'à partir de Java 8 deux fonctionnalités appelées ] Les méthodes par défaut et Les méthodes statiques ont été ajoutées aux interfaces . J'en discuterai éventuellement, mais j'aimerais pour le moment prétendre qu'elles n'existent pas. C’est pour me permettre de mieux expliquer le but premier de l’utilisation d’une interface qui était à l’origine et qui reste sans doute le mécanisme le plus abstrait d’abstraction sous Java .

Moins de détails, plus de liberté

Dans la section sur l'héritage j'ai donné une définition du mot implémentation qui devait contraster avec un autre terme que nous allons maintenant décrire. dans. Pour être clair, je ne me soucie pas des mots eux-mêmes, ou si vous êtes d'accord avec leur usage;

Alors que l’héritage est avant tout un outil pour partager la mise en œuvre sur un ensemble de classes, on peut dire que est une interface ] sont principalement un mécanisme permettant de partager le comportement entre plusieurs classes. Le comportement utilisé dans ce sens n'est vraiment qu'un mot non technique désignant les méthodes abstraites . Une méthode abstraite est une méthode qui ne peut pas, en fait, contenir un corps de méthode :

 interface publique OnClickListener {
        annuler onClick (Affichage v);
}

La réaction naturelle pour moi, et un certain nombre de personnes que j'ai enseignées, après avoir examiné pour la première fois une interface était de se demander quelle était l'utilité de ne partager qu'un type de retour , nom de la méthode et liste de paramètres pourrait l'être. En apparence, cela semble être un excellent moyen de créer du travail supplémentaire pour vous-même ou pour tous ceux qui écrivent le cours qui implémente l'interface . La réponse est que les interfaces sont idéales pour les situations dans lesquelles vous voulez qu'un ensemble de classes se comporte de la même manière (c'est-à-dire qu'elles possèdent les mêmes méthodes abstraites publiques ), mais vous vous attendez à ce qu'ils appliquent ce comportement de différentes manières.

Pour prendre un exemple simple mais pertinent, la plate-forme Android possède deux classes qui appartiennent principalement au secteur de création et gestion d'une partie de l'interface utilisateur: Activité et Fragment . Il s'ensuit que ces classes auront très souvent l'obligation d'écouter les événements qui apparaissent lorsqu'un clic est fait sur un widget (ou avec lequel un utilisateur interagit avec un autre moyen). Aux fins de discussion, prenons un moment pour comprendre pourquoi l’héritage ne permet presque jamais de résoudre un tel problème:

 public class OnClickManager {.
    public void onClick (Afficher la vue) {
        // Attends une minute ... Les activités et les fragments presque jamais
        // gère les événements de clic exactement de la même manière ...
    }
}

Non seulement, nos fragments Activités et hérités de hériteraient de OnClickManager il serait impossible de gérer les événements différemment, mais le coup d'envoi est que nous pouvions pas même le faire si on le voulait. Les activités et le fragment étendent déjà une classe parent et Java n'autorise pas les classes parentales multiples . Notre problème est donc que nous voulons un ensemble de classes pour se comporter de la même manière mais nous devons avoir une certaine flexibilité quant à la manière dont la classe implémente ce comportement . Cela nous ramène à l'exemple précédent de l'interface publique View.OnClickListener :

 OnClickListener {
        annuler onClick (Affichage v);
}

Ceci est le code source actuel (qui est imbriqué dans la classe View ), et ces quelques lignes nous permettent de garantir un comportement cohérent avec différents widgets ( Views ) et les contrôleurs d'interface utilisateur ( Activités, fragments, etc. ).

L'abstraction favorise le couplage lâche

J'ai répondu avec espoir à la question de savoir pourquoi les interfaces existent en Java; parmi beaucoup d'autres langues. D'un certain point de vue, ils ne sont qu'un moyen de partager le code entre les classes, mais ils sont délibérément moins détaillés afin de permettre différentes implémentations . Mais, de la même manière que héritage peut être utilisé à la fois comme mécanisme de partage de code et de abstraction (malgré des restrictions de la hiérarchie de classes), il en découle que les interfaces fournissent une mécanisme flexible pour l'abstraction .

Dans une section précédente de cet article, j'ai présenté le sujet de le couplage lâche / étroit par analogie avec la différence entre l'utilisation de ongles et a vissé pour construire une sorte de structure. Pour récapituler, l’idée de base est que vous voudrez utiliser des vis dans les cas où il est probable que la structure existante sera modifiée (ce qui peut résulter de la correction d’erreurs, de modifications de la conception, etc.). Les clous sont parfaits pour utiliser lorsque vous devez simplement attacher des parties de la structure et ne vous inquiétez pas particulièrement pour les démonter dans un avenir proche.

Nails et vis sont supposées être analogues aux béton et références abstraites (le terme dépendances s'applique également) entre classes. Pour éviter toute confusion, l’exemple suivant montre ce que je veux dire:

 class Client {
    Validateur privé validateur;
    private INetworkAdapter networkAdapter;

    void sendNetworkRequest (Entrée de chaîne) {
        if (validator.validateInput (input)) {
            essayer {
                networkAdapter.sendRequest (entrée);
            } catch (IOException e) {
                // gérer une exception
            }
        }
    }
}

Validateur de classe {
    //... logique de validation
    boolean validateInput (Entrée de chaîne) {
        boolean isValid = true;
        //...change isValid à false en fonction de la logique de validation
        return isValid;
    }
}

interface INetworkAdapter {
    // ...
    void sendRequest (entrée de chaîne) lève IOException;
}

Nous avons ici une classe appelée Client qui possède deux types de références . Notez que, en supposant que Client n’a rien à voir avec la création de ses références (ce n’est vraiment pas le cas), il est découplé des détails de mise en œuvre de tout adaptateur réseau particulier.

Il y a quelques implications importantes de ce couplage lâche . Pour commencer, je peux construire le client de manière totalement isolée de toute mise en oeuvre de INetworkAdapter . Imaginez un instant que vous travailliez dans une équipe de deux développeurs; un pour construire le front-end, un pour construire le back-end. Tant que les deux développeurs sont au courant des interfaces qui associent leurs classes respectives, ils peuvent poursuivre le travail pratiquement indépendamment l'un de l'autre.

Deuxièmement, si je vous disais que les deux développeurs pourraient-ils vérifier que leurs implémentations respectives fonctionnaient correctement, également indépendamment des progrès réalisés? C'est très facile avec les interfaces. venez de construire un Test Double qui implémente l'interface appropriée :

 la classe FakeNetworkAdapter implémente INetworkAdapter {
    public boolean throwError = false;


    @Passer outre
    void public sendRequest (entrée de chaîne) lève IOException {
        if (throwError) lance un nouvel IOException ("Test Exception");
    }
}

En principe, on peut constater que l'utilisation de références abstraites ouvre la porte à une modularité et une testabilité accrues, ainsi qu'à des modèles de conception très puissants tels que le Facade Pattern Motif d'observateur et plus. Ils peuvent également permettre aux développeurs de trouver un juste équilibre entre la conception de différentes parties d’un système basé sur le comportement ( Programme To An Interface ), sans s’embourber dans la mise en oeuvre de détails.

Un dernier point sur les abstractions

Les abstractions n'existent pas de la même manière qu'une chose en béton . Ceci est reflété dans le langage de programmation Java par le fait que les interfaces abstraites et ne peuvent pas être instanciées.

Par exemple, cela ne compilerait certainement pas:

 public class Main s'étend Application {
    public static void main (String [] args) {
        lancement (args);
    }

    @Passer outre
    public void start (Stage primaryStage) {
      // ERREUR x2:
        Foo f = nouveau Foo ();
        Barre b = nouvelle barre ()

    }


    classe abstraite privée Foo {}
    interface privée Bar {}


}

En fait, l'idée de s'attendre à une interface ou abstraite abstraite et non implémentée pour fonctionner au moment de l'exécution a autant de sens que de s'attendre à ce qu'un uniforme UPS flotte autour de la livraison de colis. Quelque chose de concret doit être derrière l'abstraction pour qu'il soit utile; même si la classe appelante n'a pas besoin de savoir ce qui se cache derrière les références abstraites .

Les classes abstraites: tout mettre en ordre

Si vous en êtes arrivé là, je suis heureux de le dire vous que je n'ai plus de phrases philosophiques ou de jargons à traduire. Autrement dit, les classes abstraites constituent un mécanisme permettant de partager les comportements de mise en œuvre et entre plusieurs classes. Maintenant, je vais admettre tout de suite que je ne me retrouve pas souvent à utiliser des classes abstraites . Néanmoins, j'espère qu'à la fin de cette section, vous saurez exactement quand ils seront appelés.

Étude de cas Workout Log

Un an environ dans la création d'applications Android en Java, je reconstruisais ma première application Android. à partir de rien. La première version était une sorte de masse de code épouvantable à laquelle on pourrait s'attendre d'un développeur autodidacte avec peu de conseils. By the time I wanted to add new functionality, it became clear that the tightly coupled structure I had built exclusively with nailswas so impossible to maintain that I must rebuild it entirely.

The app was a workout log that was designed to allow easy recording of your workouts, and the ability to output the data of a past workout as a text or image file. Without getting into too much detail, I structured the data models of the app such that there was a Workoutobject, which comprised of a collection of Exerciseobjects (among other fields which are irrelevant to this discussion).

As I was implementing the feature for outputting workout data to some kind of visual medium, I realized that I had to deal with a problem: Different kinds of exercises would require different kinds of text outputs.

To give you a rough idea, I wanted to change the outputs depending on the type of exercise like so:

  • Barbell: 10 REPS @ 100 LBS
  • Dumbbell: 10 REPS @ 50 LBS x2
  • Bodyweight: 10 REPS @ Bodyweight
  • Bodyweight +: 10 REPS @ Bodyweight + 45 LBS
  • Timed: 60 SEC @ 100 LBS

Before I proceed, note that there were other types (working out can become complicated) and that the code I will be showing has been trimmed down and changed to fit nicely into an article .

In keeping with my definition from before, the goal of writing an abstract classis to implement everything (even state such as variables and constants) which is shared across all child classes in the abstract class. Then, for anything which changes across said child classescreate an abstract method:

abstract class Exercise {
    private final String type;
    protected final String name;
    protected final int[] repetitionsOrTime;
    protected final double[] weight;

    protected static final String POUNDS = "LBS";
    protected static final String SECONDS = "SEC ";
    protected static final String REPETITIONS = "REPS ";

    public Exercise(String type, String name, int[] repetitionsOrTime, double[] weight) {
        this.type = type;
        this.name = name;
        this.repetitionsOrTime = repetitionsOrTime;
        this.weight = weight;
    }

    public String getFormattedOutput(){
        StringBuilder sb = new StringBuilder();
        sb.append(name);
        sb.append("n");
        getSetData(sb);
        sb.append("n");
        return sb.toString();
    }

    /**
     * Append data appropriately based on Exercise type
     * @param sb - StringBuilder to Append data to
     */
    protected abstract void getSetData(StringBuilder sb);
    
    //...Getters
}

I may be stating the obvious, but if you have any questions about what should or should not be implemented in the abstract classthe key is to look at any part of the implementation which has been repeated in all child classes.

Now that we have established what is common amongst all exercises, we can begin to create child classes with specializations for each kind of String output:

Barbell Exercise:
class BarbellExercise extends Exercise {
    public BarbellExercise(String type, String name, int[] repetitionsOrTime, double[] weight) {
        super(type, name, repetitionsOrTime, weight);
    }

    @Override
    protected void getSetData(StringBuilder sb) {
        for (int i = 0; i 
Dumbbell Exercise:
class DumbbellExercise extends Exercise {
    private static final String TIMES_TWO = "x2";

    public DumbbellExercise(String type, String name, int[] repetitionsOrTime, double[] weight) {
        super(type, name, repetitionsOrTime, weight);
    }

    @Override
    protected void getSetData(StringBuilder sb) {

        for (int i = 0; i 
Bodyweight Exercise:
class BodyweightExercise extends Exercise {
    private static final String BODYWEIGHT = "Bodyweight";

    public BodyweightExercise(String type, String name, int[] repetitionsOrTime, double[] weight) {
        super(type, name, repetitionsOrTime, weight);
    }

    @Override
    protected void getSetData(StringBuilder sb) {

        for (int i = 0; i 

I am certain that some astute readers will find things which could have been abstracted out in a more efficient manner, but the purpose of this example (which has been simplified from the original source) is to demonstrate the general approach. Of course, no programming article would be complete without something which can be executed. There are several online Java compilers which you may use to run this code if you want to test it out (unless you already have an IDE):

public class Main {
    public static void main(String[] args) {
        //Note: I actually used another nested class called a "Set" instead of an Array
        //to represent each Set of an Exercise.
        int[] reps = {10, 10, 8};
        double[] weight = {70.0, 70.0, 70.0};

        Exercise e1 = new BarbellExercise(
                "Barbell",
                "Barbell Bench Press",
                reps,
                weight
        )

        Exercise e2 = new DumbbellExercise(
                "Dumbbell",
                "Dumbbell Bench Press",
                reps,
                weight
        )

        Exercise e3 = new BodyweightExercise(
                "Bodyweight",
                "Push Up",
                reps,
                weight
        )

        System.out.println(
                e1.getFormattedOutput()
                + e2.getFormattedOutput()
                + e3.getFormattedOutput()
        )
    }
}

Executing this toy application yields the following output:
Barbell Bench Press

10 REPS  @ 70.0LBS
10 REPS  @ 70.0LBS
8 REPS  @ 70.0LBS

Dumbbell Bench Press
10 REPS  @ 70.0LBSx2
10 REPS  @ 70.0LBSx2
8 REPS  @ 70.0LBSx2

Push Up
10 REPS  @ Bodyweight
10 REPS  @ Bodyweight
8 REPS  @ Bodyweight

Further Considerations

Earlier, I mentioned that there are two features of Java interfaces (as of Java 8) which are decidedly geared towards sharing implementationas opposed to behavior. These features are known as Default Methods and Static Methods.

I have decided not to go into detail on these features for the reason that they are most typically used in mature and/or large code bases where a given interface has many inheritors. Despite the fact that this is meant to be an introductory article, and I still encourage you to take a look at these features eventually, even though I am confident that you will not need to worry about them just yet.

I would also like to mention that there are other ways to share implementation across a set of classes (or even static methods) in a Java application that does not require inheritance or abstraction at all. For example, suppose you have some implementation which you expect to use in a variety of different classes, but does not necessarily make sense to share via inheritance. A common pattern in Java is to write what is known as a Utility class, which is a simple classcontaining the requisite implementation in a static method:

public class TimeConverterUtil {

    /**
     * Accepts an hour (0-23) and minute (0-59), then attempts to format them into an appropriate
     * format such as 12, 30 -> 12:30 pm
     */    
    public static String convertTime (int hour, int minute){
        String unformattedTime = Integer.toString(hour) + ":" + Integer.toString(minute);
        DateFormat f1 = new SimpleDateFormat("HH:mm");

        Date d = null;
        try { d = f1.parse(unformattedTime); }
        catch (ParseException e) { e.printStackTrace(); }
        DateFormat f2 = new SimpleDateFormat("h:mm a");
        return f2.format(d).toLowerCase();
    }
}

Using this static method in an external class (or another static method) looks like this:


public class Main {
    public static void main(String[] args){
        //...
        String time = TimeConverterUtil.convertTime(12, 30);
        //...
    }
}

Cheat Sheet

We have covered a lot of ground in this article, so I would like to spend a moment summarizing the three main mechanisms based on what problems they solve. Since you should possess a sufficient understanding of the terms and ideas I have either introduced or redefined for the purposes of this article, I will keep the summaries brief.

I Want A Set Of Child Classes To Share Implementation

Classic inheritancewhich requires a child class to inherit from a parent classis a very simple mechanism for sharing implementation across a set of classes. An easy way to decide if some implementation should be pulled into a parent classis to see whether it is repeated in a number of different classes line for line. The acronym DRY (Don’t Repeat Yourself) is a good mnemonic device to watch out for this situation.

While coupling child classes together with a common parent class can present some limitations, a side benefit is that they can all be referenced as the parent classwhich provides a limited degree of abstraction.

I Want A Set Of Classes To Share Behavior

Sometimes, you want a set of classes to be capable of possessing certain abstract methods (referred to as behavior), but you do not expect the implementation of that behavior to be repeated across inheritors.

By definition, Java interfaces may not contain any implementation (except for Default and Static Methods), but any class which implements an interfacemust supply an implementation for all abstract methods, otherwise, the code will not compile. This provides a healthy measure of flexibility and restriction on what is actually shared and does not require the inheritors to be of the same class hierarchy.

I Want A Set Of Child Classes To Share Behavior And Implementation

Although I do not find myself using abstract classes all over the place, they are perfect for situations when you require a mechanism for sharing both behavior and implementation across a set of classes. Anything which will be repeated across inheritors may be implemented directly in the abstract classand anything which requires flexibility may be specified as an abstract method.

Smashing Editorial(dm, il)




Source link