Conception de classe orientée objet

12

Je me posais des questions sur une bonne conception de classe orientée objet. En particulier, j'ai du mal à choisir entre ces options:

  1. méthode statique vs instance
  2. méthode sans paramètres ni valeur de retour vs méthode avec paramètres et valeur de retour
  3. chevauchement vs fonctionnalité de méthode distincte
  4. méthode privée vs méthode publique

Exemple 1:

Cette implémentation utilise des méthodes d'instance, sans valeur ni paramètre de retour, sans fonctionnalité qui se chevauchent et toutes les méthodes sont publiques

XmlReader reader = new XmlReader(url);
reader.openUrl();
reader.readXml();
Document result = reader.getDocument();

Exemple 2:

Cette implémentation utilise des méthodes statiques, avec des valeurs et des paramètres de retour, avec des fonctionnalités qui se chevauchent et des méthodes privées

Document result = XmlReader.readXml(url); 

Dans l'exemple un, toutes les méthodes sont publiques, ce qui facilite leur test unitaire. Bien que toutes les méthodes soient distinctes, readXml () dépend de openUrl () dans la mesure où openUrl () doit être appelé en premier. Toutes les données sont déclarées dans les champs d'instance, il n'y a donc pas de valeurs de retour ou de paramètres dans aucune méthode, sauf dans le constructeur et les accesseurs.

Dans l'exemple deux, une seule méthode est publique, les autres sont statiques privées, ce qui les rend difficiles à tester. Les méthodes se chevauchent en ce que readXml () appelle openUrl (). Il n'y a pas de champs, toutes les données sont passées comme paramètres dans les méthodes et le résultat est retourné immédiatement.

Quels principes dois-je suivre pour faire une programmation orientée objet appropriée?

siamii
la source
3
Les choses statiques sont mauvaises lorsque vous faites du multi-threading. L'autre jour, j'avais un XMLWriter statique, comme XMLWriter.write (data, fileurl). Cependant, étant donné qu'il avait un FileStream statique privé, en utilisant cette classe à partir de plusieurs threads en même temps, le deuxième thread a écrasé les premiers threads FileStream, provoquant une erreur qui serait très difficile à trouver. Les classes statiques avec des membres statiques + multi-threading sont une recette pour un désastre.
Per Alexandersson
1
@Paxinum. Le problème que vous décrivez est un problème d'état, pas un problème "statique". Si vous utilisiez un singleton avec des membres non statiques, vous auriez le même problème avec le multithread.
mike30
2
@Per Alexandersson Les méthodes statiques ne sont pas mauvaises par rapport à la concurrence. L'état statique est mauvais. C'est pourquoi la programmation fonctionnelle, dans laquelle toutes les méthodes sont statiques, fonctionne très bien dans des situations concurrentes.
Yuli Bonner

Réponses:

11

L'exemple 2 est assez mauvais pour les tests ... et je ne veux pas dire que vous ne pouvez pas tester les internes. Vous ne pouvez pas non plus remplacer votre XmlReaderobjet par un objet factice car vous n'avez aucun objet du tout.

L'exemple 1 est inutilement difficile à utiliser. Qu'en est-il de

XmlReader reader = new XmlReader(url);
Document result = reader.getDocument();

qui n'est pas plus difficile à utiliser que votre méthode statique.

Des choses comme l'ouverture de l'URL, la lecture de XML, la conversion d'octets en chaînes, l'analyse, la fermeture de sockets, etc., sont sans intérêt. Créer un objet et l'utiliser est important.

Donc, à mon humble avis, la conception OO appropriée est de rendre uniquement les deux choses publiques (sauf si vous avez vraiment besoin des étapes intermédiaires pour une raison quelconque). La statique est mauvaise.

maaartinus
la source
-1. VOUS POUVEZ en fait remplacer XmlReader par un objet factice. Pas avec un framework moick open source cerveau mort, mais avec un bon niveau industriel, vous pouvez;) Coûte quelques centaines de dollars par développeur mais fait des merveilles pour tester les fonctionnalités scellées dans les API que vous publiez.
TomTom
2
+1 pour ne pas avoir écouté l'argumentaire de vente de TomTom. Quand je veux une doublure, je préfère écrire quelque chose comme Document result = new XmlReader(url).getDocument();Pourquoi? Pour que je puisse le mettre à jour Document result = whateverReader.getDocument();et me le whateverReaderremettre par autre chose.
candied_orange
6

Voici la chose - il n'y a pas une seule bonne réponse, et il n'y a pas de définition absolue de «conception orientée objet appropriée» (certaines personnes vous en proposeront une, mais elles sont naïves ... donnez-leur du temps).

Tout se résume à vos OBJECTIFS.

Vous êtes artiste et le papier est vierge. Vous pouvez dessiner un portrait latéral noir et blanc délicat et finement crayonné, ou une peinture abstraite avec d'énormes entailles de néons mélangés. Ou quoi que ce soit entre les deux.

Alors, QUI SE SENT BIEN pour le problème que vous résolvez? Quelles sont les plaintes des personnes qui ont besoin d'utiliser vos cours pour travailler avec xml? Qu'est-ce qui est difficile dans leur travail? Quel type de code essaient-ils d'écrire qui entoure les appels vers votre bibliothèque, et comment pouvez-vous aider à mieux l'écouler pour eux?

Souhaiteraient-ils plus de concision? Souhaiteraient-ils qu'il soit très intelligent pour déterminer les valeurs par défaut des paramètres, afin qu'ils n'aient pas à spécifier beaucoup (ou quoi que ce soit) et que cela devine correctement? Pouvez-vous automatiser les tâches de configuration et de nettoyage nécessaires à votre bibliothèque pour qu'il leur soit impossible d'oublier ces étapes? Que pouvez-vous faire d'autre pour eux?

Enfer, ce que vous devez probablement faire est de le coder de 4 ou 5 façons différentes, puis mettez votre chapeau de consommateur et écrivez le code qui utilise les 5, et voyez ce qui se sent mieux. Si vous ne pouvez pas le faire pour l'ensemble de votre bibliothèque, faites-le pour un sous-ensemble. Et vous devez également ajouter des alternatives supplémentaires à votre liste - qu'en est-il d'une interface fluide, ou d'une approche plus fonctionnelle, ou de paramètres nommés, ou de quelque chose basé sur DynamicObject afin que vous puissiez créer des "pseudo-méthodes" significatives qui les aident en dehors?

Pourquoi jQuery est-il le roi en ce moment? Parce que Resig et l' équipe ont suivi ce processus, jusqu'à ce qu'ils sont tombés sur un principe syntaxique incroyablement réduit la quantité de code JS qu'il faut pour travailler avec le Royaume et les événements. Ce principe syntaxique n'était pas clair pour eux ni pour personne d'autre lorsqu'ils ont commencé. Ils l'ont TROUVÉ.

En tant que programmeur, c'est ce que vous appelez le plus. Vous tâtonnez dans le noir en essayant des choses jusqu'à ce que vous le trouviez. Quand vous le ferez, vous saurez. Et vous donnerez à vos utilisateurs un énorme bond en productivité. Et c'est à cela que sert la conception (dans le domaine du logiciel).

Fleurs de Charlie
la source
1
Cela implique qu'il n'y a pas de différences entre les meilleures pratiques et d'autres pratiques autres que la façon dont cela «se sent». C'est ainsi que vous vous retrouvez avec des boules de boue incontrôlables - car pour de nombreux développeurs, tendre la main au-delà des limites de la classe, etc., "se sent" tout simplement flippant.
Amy Blankenship
@Amy Blankenship, je dirais sans équivoque qu'il n'y a pas de "meilleure façon" de faire les choix que le PO demande. Cela dépend d'un million de choses, et il y a un million de degrés de liberté. Cependant, je pense qu'il y a une place pour les "meilleures pratiques", et c'est dans un environnement d' équipe , où certains choix ont déjà été faits, et nous avons besoin du reste de l'équipe pour rester cohérent avec ces choix précédents. En d'autres termes, dans un contexte spécifique , il pourrait y avoir des raisons d'étiqueter certaines choses «meilleures pratiques». Mais le PO n'a donné aucun contexte. Il construit quelque chose et ...
Charlie Flowers
... il fait face à tous ces choix possibles. Il n'y a pas de «bonne réponse» à ces choix. Elle est motivée par les objectifs et les points faibles du système. Je vous garantis que les programmeurs Haskell ne pensent pas que toutes les méthodes devraient être des méthodes d'instance. Et les programmeurs du noyau Linux ne pensent pas du tout qu'il est très important de rendre les choses accessibles à TDD. Et les programmeurs de jeux C ++ préfèrent souvent regrouper leurs données dans une structure de données étanche en mémoire plutôt que de tout encapsuler dans des objets. Chaque "meilleure pratique" n'est qu'une "meilleure pratique" dans un contexte donné, et est un anti-modèle dans un autre contexte.
Charlie Flowers
@AmyBlankenship Une dernière chose: je ne suis pas d'accord pour dire que le fait de tendre la main au-delà des limites de la classe "est tout simplement fantastique." Cela conduit à des boules de boue incontrôlables, qui semblent horribles . Je pense que vous essayez de résoudre le problème que certains travailleurs sont bâclés / démotivés / très inexpérimentés. Dans ce cas, quelqu'un qui est prudent, motivé et expérimenté doit faire les choix clés et les appeler «Meilleures pratiques». Mais, la personne qui choisit ces «meilleures pratiques» fait toujours des choix en fonction de ce qui «se sent bien», et il n'y a pas de bonnes réponses. Vous contrôlez simplement qui fait les choix.
Charlie Flowers
J'ai travaillé avec plusieurs programmeurs qui se considéraient comme des cadres supérieurs et étaient considérés de cette façon par la direction qui croyait fermement que la statique et les singletons étaient absolument la bonne façon de gérer les problèmes de communication. La partie statique de cette question n'aurait même pas été posée si le fait de tendre la main au-delà des limites de la classe comme cela "se sent" mal aux développeurs, et la réponse qui préconise l'alternative statique ne recevrait aucun vote positif.
Amy Blankenship
3

La deuxième option est meilleure car elle est plus simple à utiliser (même si ce n'est que vous), beaucoup plus simple.

Pour les tests unitaires, je testerais simplement l'interface et non les internes si vous voulez vraiment déplacer les internes de privé à protégé.


la source
2
Vous pouvez également rendre votre package de méthodes privé (par défaut), si vos scénarios de test sont dans le même package, à des fins de test unitaire.
Bon point - c'est mieux.
3

1. Statique vs instance

Je pense qu'il existe des directives très claires sur ce qu'est une bonne conception OO et ce qui ne l'est pas. Le problème est que la blogosphère rend difficile de séparer le bon du mauvais et du laid. Vous pouvez trouver une sorte de référence soutenant même la pire pratique à laquelle vous pouvez penser.

Et la pire pratique à laquelle je peux penser est Global State, y compris les statistiques que vous avez mentionnées et le Singleton préféré de tout le monde. Quelques extraits de l' article classique de Misko Hevery sur le sujet .

Pour vraiment comprendre les dépendances, les développeurs doivent lire chaque ligne de code. Cela provoque une action effrayante à distance: lors de l'exécution de suites de tests, l'état global muté dans un test peut entraîner l'échec inattendu d'un test ultérieur ou parallèle. Brisez la dépendance statique à l'aide d'une injection de dépendance manuelle ou Guice.

L'action fantasmagorique à distance est lorsque nous exécutons une chose que nous croyons isolée (puisque nous n'avons transmis aucune référence), mais des interactions et des changements d'état inattendus se produisent dans des endroits éloignés du système dont nous n'avons pas parlé à l'objet. Cela ne peut se produire que via l'état global.

Vous ne l'avez peut-être pas pensé de cette façon auparavant, mais chaque fois que vous utilisez un état statique, vous créez des canaux de communication secrets et ne les rendez pas clairs dans l'API. Une action fantasmagorique à distance oblige les développeurs à lire chaque ligne de code pour comprendre les interactions potentielles, réduit la productivité des développeurs et confond les nouveaux membres de l'équipe.

Cela revient à dire que vous ne devez pas fournir de références statiques à tout ce qui a une sorte d'état stocké. Le seul endroit où j'utilise la statique est pour les constantes énumérées, et j'ai des doutes à ce sujet.

2. Méthodes avec paramètres d'entrée et valeurs de retour vs méthodes sans aucun

La chose que vous devez comprendre est que les méthodes qui n'ont ni paramètres d'entrée ni paramètres de sortie sont à peu près garanties de fonctionner sur une sorte d'état stocké en interne (sinon, que font-elles?). Il existe des langages entiers qui sont construits sur l'idée d'éviter l'état stocké.

Chaque fois que vous avez stocké l'état, vous avez la possibilité d'effets secondaires, alors assurez-vous de toujours l'utiliser en pleine conscience. Cela implique que vous devriez préférer les fonctions avec des entrées et / ou sorties définies.

Et, en fait, les fonctions qui ont des entrées et des sorties définies sont beaucoup plus faciles à tester - vous n'avez pas besoin d'exécuter une fonction ici et d'aller là-bas pour voir ce qui s'est passé, et vous n'avez pas besoin de définir une propriété quelque part sinon avant d'exécuter la fonction en cours de test.

Vous pouvez également utiliser en toute sécurité ce type de fonction comme statique. Cependant, je ne le ferais pas, car si je voulais ensuite utiliser une implémentation légèrement différente de cette fonction quelque part, plutôt que de fournir une instance différente avec la nouvelle implémentation, je suis coincé sans aucun moyen de remplacer la fonctionnalité.

3. Chevauchement vs Distinction

Je ne comprends pas la question. Quel serait l'avantage de 2 méthodes qui se chevauchent?

4. Privé vs public

N'exposez rien que vous n'avez pas besoin d'exposer. Cependant, je ne suis pas non plus un grand fan du privé. Je ne suis pas un développeur C #, mais un développeur ActionScript. J'ai passé beaucoup de temps dans le code Flex Framework d'Adobe, qui a été écrit vers 2007. Et ils ont fait de très mauvais choix sur ce qui devait être privé, ce qui en fait une sorte de cauchemar à essayer d'étendre leurs classes.

Donc, à moins que vous ne pensiez être un meilleur architecte que les développeurs d'Adobe vers 2007 (d'après votre question, je dirais que vous avez encore quelques années avant d'avoir la possibilité de faire cette affirmation), vous voudrez probablement simplement utiliser par défaut la protection .


Il y a quelques problèmes avec vos exemples de code qui signifient qu'ils ne sont pas bien architecturés, il n'est donc pas possible de choisir A ou B.

D'une part, vous devriez probablement séparer la création de votre objet de son utilisation . Donc, vous n'auriez généralement pas votre new XMLReader()droit à côté de l'endroit où il est utilisé.

En outre, comme le dit @djna, vous devez encapsuler les méthodes utilisées dans vos utilisations de XML Reader, afin que votre API (exemple d'instance) puisse être simplifiée pour:

_document Document = reader.read(info);

Je ne sais pas comment C # fonctionne, mais comme j'ai travaillé avec un certain nombre de technologies Web, je soupçonnerais que vous ne seriez pas toujours en mesure de renvoyer un document XML immédiatement (sauf peut-être comme promesse ou type futur) objet), mais je ne peux pas vous donner de conseils sur la façon de gérer une charge asynchrone en C #.

Notez qu'avec cette approche, vous pouvez créer plusieurs implémentations qui peuvent prendre un paramètre qui leur indique où / quoi lire et retourner un objet XML, et les échanger en fonction des besoins de votre projet. Par exemple, vous pouvez lire directement à partir d'une base de données, d'un magasin local ou, comme dans votre exemple d'origine, d'une URL. Vous ne pouvez pas faire cela si vous utilisez une méthode statique.

Amy Blankenship
la source
2

Concentrez-vous sur le point de vue du client.

IReader reader = new XmlReader.readXml(url);  // or injection, or factory or ...
Document document = reader.read();

Les méthodes statiques ont tendance à limiter l'évolution future, notre client travaille en termes d'interface fournie par de nombreuses implémentations différentes.

Le problème majeur avec votre idiome ouvert / lu est que le client a besoin de connaître l'ordre dans lequel appeler les méthodes, quand il veut seulement faire un travail simple. C'est évident ici, mais dans une classe plus grande, c'est loin d'être évident.

La principale méthode à tester est read (). Les méthodes internes peuvent être rendues visibles pour tester les programmes en les rendant ni publics ni privés et en plaçant les tests dans le même package - les tests peuvent toujours être séparés du code publié.

djna
la source
Les méthodes avec visibilité par défaut sont-elles toujours visibles si la suite de tests se trouve dans un autre projet?
siamii
Java ne connaît pas les projets. Les projets sont une construction IDE. Le compilateur et la JVM examinent les packages dans lesquels les classes testées et testeurs sont dans le même package, la visibilité par défaut est autorisée. Dans Eclipse, j'utilise un seul projet, avec deux répertoires sources différents. Je viens d'essayer avec deux projets, ça marche.
2

méthode statique vs instance

Dans la pratique, vous constaterez que les méthodes statiques sont généralement limitées aux classes utilitaires et ne devraient pas encombrer vos objets de domaine, gestionnaires, contrôleurs ou DAO. Les méthodes statiques sont plus utiles lorsque toutes les références nécessaires peuvent raisonnablement être transmises en tant que paramètres et offrent certaines fonctionnalités qui peuvent être réutilisées dans de nombreuses classes. Si vous vous retrouvez à utiliser une méthode statique comme solution de contournement pour avoir une référence à un objet d'instance, demandez-vous pourquoi vous n'avez pas simplement cette référence à la place.

méthode sans paramètres ni valeur de retour vs méthode avec paramètres et valeur de retour

Si vous n'avez pas besoin de paramètres sur la méthode, ne les ajoutez pas. Il en va de même avec la valeur de retour. Garder cela à l'esprit simplifiera votre code et vous assurera que vous ne codez pas pour une multitude de scénarios qui ne finissent jamais par se produire.

chevauchement vs fonctionnalité de méthode distincte

C'est une bonne idée d'essayer d'éviter les fonctionnalités qui se chevauchent. Parfois, cela peut être difficile, mais lorsqu'un changement de logique est nécessaire, il est beaucoup plus facile de changer une méthode qui est réutilisée, que de changer tout un tas de méthodes avec des fonctionnalités similaires

méthode privée vs méthode publique

Les getters, setters et constructeurs devraient être publics. Tout ce que vous voudrez essayer de garder privé, sauf s'il existe un cas où une autre classe doit l'exécuter. Garder les méthodes par défaut sur privé aidera à maintenir l' encapsulation . Il en va de même pour les champs, habituez-vous à privé par défaut

Erich
la source
1

Je ne répondrai pas à votre question, mais je pense qu'un problème est créé par les termes que vous avez utilisés. Par exemple

XmlReader.read => twice "read"

Je pense que vous avez besoin d'un XML donc je vais créer un objet XML qui peut être créé à partir d'un type textuel (je ne connais pas C # ... en Java ça s'appelle String). Par exemple

class XML {
    XML(String text) { [...] }
}

Vous pouvez le tester et c'est clair. Ensuite, si vous avez besoin d'une usine, vous pouvez ajouter une méthode d'usine (et elle peut être statique comme votre 2ème exemple). Par exemple

class XML {
    XML(String text) { [...] }

    static XML fromUrl(url) { [...] }

}
sixro
la source
0

Vous pouvez suivre quelques règles simples. Cela aide si vous comprenez les raisons des règles.

méthode statique vs instance

Pour les méthodes, vous n'avez pas à prendre cette décision consciemment. S'il apparaît que votre méthode n'utilise aucun membre de champ (votre analyseur préféré devrait vous le dire), vous devez ajouter le mot clé statique.

méthode sans paramètres ni valeur de retour vs méthode avec paramètres et valeur de retour

La deuxième option est meilleure en raison de la portée. Vous devez toujours garder votre portée étroite. Tendre la main pour les choses dont vous avez besoin est mauvais, vous devez avoir une entrée, y travailler localement et retourner un résultat. Votre première option est mauvaise pour la même raison que les variables globales sont généralement mauvaises: elles ne sont significatives que pour une partie de votre code mais elles sont visibles (et donc du bruit) partout ailleurs et peuvent être altérées de n'importe où. Cela rend difficile d'obtenir une image complète de votre logique.

chevauchement vs fonctionnalité de méthode distincte

Je ne pense pas que ce soit un problème. Les méthodes appelant d'autres méthodes sont très bien si cela découpe les tâches en plus petits morceaux distinctement fonctionnels.

méthode privée vs méthode publique

Rendez tout privé sauf si vous en avez besoin pour qu'il soit public. L'utilisateur de votre classe peut se passer du bruit, il ne veut voir que ce qui compte pour lui.

Martin Maat
la source