Dans quelle mesure le modèle de responsabilité unique devrait-il être spécifique aux classes?

14

Par exemple, supposons que vous ayez un programme de jeu sur console, qui a toutes sortes de méthodes d'entrée / sortie vers et depuis la console. Serait - il intelligent de les garder tous en une seule inputOutputclasse ou les décomposer à des classes plus spécifiques comme de startMenuIO, inGameIO, playerIO, gameBoardIO, etc. de telle sorte que chaque classe a environ 1-5 méthodes?

Et sur la même note, s'il est préférable de les décomposer, serait-il intelligent de les placer dans un IOespace de noms rendant ainsi les appeler un peu plus verbeux, par exemple: IO.inGameetc.?

shinzou
la source
Connexe: Je n'ai jamais vu de bonne raison de combiner entrée et sortie. Et quand je les sépare délibérément, mon code s'avère beaucoup plus propre.
Mooing Duck
1
Dans quelle langue nommez-vous les classes dans lowerCamelCase, de toute façon?
user253751

Réponses:

7

Mise à jour (récapitulation)

Depuis que j'ai écrit une réponse assez verbeuse, voici ce que cela se résume à:

  • Les espaces de noms sont bons, utilisez-les chaque fois que cela a du sens
  • L'utilisation inGameIOet les playerIOclasses constitueraient probablement une violation du SRP. Cela signifie probablement que vous associez la façon dont vous gérez les E / S avec la logique d'application.
  • Avoir quelques classes d'E / S génériques, qui sont utilisées (ou parfois partagées) par les classes de gestionnaire. Ces classes de gestionnaire traduiraient alors l'entrée brute dans un format que votre logique d'application peut comprendre.
  • Il en va de même pour la sortie: cela peut être fait par des classes assez génériques, mais passez l'état du jeu via un objet gestionnaire / mappeur qui traduit l'état de jeu interne en quelque chose que les classes génériques d'E / S peuvent gérer.

Je pense que vous regardez cela dans le mauvais sens. Vous séparez l'IO en fonction des composants de l'application, alors que - pour moi - il est plus logique d'avoir des classes d'E / S distinctes en fonction de la source et du "type" d'E / S.

Avoir des KeyboardIOclasses de base / génériques MouseIOpour commencer, puis en fonction du moment et de l'endroit où vous en avez besoin, ont des sous-classes qui gèrent différemment les E / S.
Par exemple, la saisie de texte est quelque chose que vous voudrez probablement gérer différemment des contrôles en jeu. Vous vous retrouverez à vouloir mapper certaines clés différemment selon chaque cas d'utilisation, mais ce mappage ne fait pas partie de l'IO lui-même, c'est la façon dont vous gérez l'IO.

S'en tenir au SRP, j'aurais quelques classes que je peux utiliser pour les entrées-sorties clavier. Selon la situation, je souhaiterai probablement interagir différemment avec ces classes, mais leur seul travail est de me dire ce que fait l'utilisateur.

J'injecterais ensuite ces objets dans un objet gestionnaire qui mapperait le IO brut sur quelque chose avec lequel ma logique d'application peut fonctionner (par exemple: l'utilisateur appuie sur "w" , le gestionnaire le mappe MOVE_FORWARD).

Ces gestionnaires, à leur tour, sont utilisés pour faire bouger les personnages et dessiner l'écran en conséquence. Une simplification grossière, mais l'essentiel est ce genre de structure:

[ IO.Keyboard.InGame ] // generic, if SoC and SRP are strongly adhered to, changing this component should be fairly easy to do
   ||
   ==> [ Controls.Keyboard.InGameMapper ]

[ Game.Engine ] <- Controls.Keyboard.InGameMapper
                <- IO.Screen
                <- ... all sorts of stuff here
    InGameMapper.move() //returns MOVE_FORWARD or something
      ||
      ==> 1. Game.updateStuff();//do all the things you need to do to move the character in the given direction
          2. Game.Screen.SetState(GameState); //translate the game state (inverse handler)
          3. IO.Screen.draw();//generate actual output

Ce que nous avons maintenant, c'est une classe qui est responsable de l'IO du clavier dans sa forme brute. Une autre classe qui traduit ces données en quelque chose que le moteur de jeu peut réellement donner un sens, ces données sont ensuite utilisées pour mettre à jour l'état de tous les composants impliqués, et enfin, une classe distincte se chargera de la sortie à l'écran.

Chaque classe a un seul travail: la gestion des entrées au clavier est effectuée par une classe qui ne sait pas / ne prend pas soin / doit savoir ce que signifie l'entrée qu'elle traite. Tout ce qu'il fait, c'est savoir comment obtenir l'entrée (tamponnée, non tamponnée, ...).

Le gestionnaire traduit cela en une représentation interne pour le reste de l'application pour donner un sens à ces informations.

Le moteur de jeu prend les données qui ont été traduites et les utilise pour informer tous les composants pertinents que quelque chose se passe. Chacun de ces composants ne fait qu'une chose, qu'il s'agisse de contrôles de collision ou de changements d'animation de personnage, peu importe, c'est à chaque objet individuel.

Ces objets retransmettent ensuite leur état et ces données sont transmises à Game.Screen, qui est essentiellement un gestionnaire d'E / S inversé. Il mappe la représentation interne sur quelque chose que le IO.Screencomposant peut utiliser pour générer la sortie réelle.

Elias Van Ootegem
la source
Puisqu'il s'agit d'une application console, il n'y a pas de souris et l'impression des messages ou du tableau est étroitement liée à l'entrée. Dans votre exemple, les espaces de noms IOet gameles classes sont-ils avec des sous-classes?
shinzou
@kuhaku: Ce sont des espaces de noms. L'essentiel de ce que je dis est que, si vous choisissez de créer des sous-classes en fonction de la partie de l'application dans laquelle vous vous trouvez, vous couplez efficacement la fonctionnalité d'E / S de base avec votre logique d'application. Vous vous retrouverez avec des classes responsables des IO en fonction de l'application . Pour moi, cela ressemble à une violation du PÉR. Quant aux classes de noms et d'espaces de noms: j'ai tendance à préférer les espaces de noms dans 95% des cas
Elias Van Ootegem
J'ai mis à jour ma réponse, pour résumer ma réponse
Elias Van Ootegem
Oui, c'est en fait un autre problème que j'ai (couplage des E / S avec la logique), donc vous recommandez réellement de séparer l'entrée de la sortie?
shinzou
@kuhaku: Cela dépend vraiment de ce que vous faites et de la complexité des éléments d'entrée / sortie. Si les gestionnaires (traduction de l'état du jeu par rapport aux entrées / sorties brutes) diffèrent trop, alors vous voudrez probablement séparer les classes d'entrée et de sortie, sinon, une seule classe IO est très bien
Elias Van Ootegem
23

Le principe de la responsabilité unique peut être difficile à comprendre. Ce que j'ai trouvé utile, c'est de penser à la façon dont vous écrivez des phrases. Vous n'essayez pas de rassembler beaucoup d'idées en une seule phrase. Chaque phrase doit énoncer clairement une idée et différer les détails. Par exemple, si vous vouliez définir une voiture, vous diriez:

Un véhicule routier, généralement à quatre roues, propulsé par un moteur à combustion interne.

Ensuite, vous définiriez séparément des éléments comme "véhicule", "route", "roues", etc. Vous n'essayez pas de dire:

Un véhicule pour transporter des personnes sur une voie, un itinéraire ou un chemin sur terre entre deux endroits qui a été pavé ou autrement amélioré pour permettre un voyage qui a quatre objets circulaires qui tournent sur un essieu fixé en dessous du véhicule et est propulsé par un moteur qui génère la force motrice par la combustion d'essence, d'huile ou d'autres combustibles avec de l'air.

De même, vous devriez essayer de faire en sorte que vos classes, méthodes, etc. énoncent le concept central aussi simplement que possible et reportent les détails à d'autres méthodes et classes. Tout comme pour l'écriture de phrases, il n'y a pas de règle stricte quant à leur taille.

Vaughn Cato
la source
Donc, comme définir la voiture avec quelques mots au lieu de plusieurs, définir de petites classes spécifiques mais similaires, c'est bien?
shinzou
2
@kuhaku: Petit c'est bien. Spécifique est bon. Il en va de même tant que vous le gardez au SEC. Vous essayez de faciliter la modification de votre conception. Garder les choses petites et spécifiques vous aide à savoir où apporter des modifications. Le garder au sec permet d'éviter d'avoir à changer beaucoup d'endroits.
Vaughn Cato
6
Votre deuxième phrase ressemble à "Merde, le professeur a dit que cela devait faire 4 pages ... 'génère de la force motrice' c'est !!"
corsiKa
2

Je dirais que la meilleure façon de procéder est de les garder dans des classes séparées. Les petites classes ne sont pas mauvaises, en fait la plupart du temps c'est une bonne idée.

En ce qui concerne votre cas spécifique, je pense qu'avoir les éléments séparés peut vous aider à changer la logique de l'un de ces gestionnaires spécifiques sans affecter les autres et, si nécessaire, il serait plus facile pour vous d'ajouter une nouvelle méthode d'entrée / sortie si elle venait à se produire.

Zalomon
la source
0

Le directeur à responsabilité unique déclare qu'une classe ne devrait avoir qu'une seule raison de changer. Si votre classe a plusieurs raisons de changer, vous pouvez la diviser en d'autres classes et utiliser la composition pour éliminer ce problème.

Pour répondre à votre question, je dois vous poser une question: votre classe a-t-elle une seule raison de changer? Sinon, n'ayez pas peur de continuer à ajouter des classes plus spécialisées jusqu'à ce que chacune n'ait qu'une seule raison de changer.

Greg Burghardt
la source