¿Qué tan específico debe ser el patrón de responsabilidad única para las clases?

14

Por ejemplo, suponga que tiene un programa de juego de consola, que tiene todo tipo de métodos de entrada / salida hacia y desde la consola. ¿Sería inteligente mantenerlos a todos en una sola clase inputOutput o dividirlos en clases más específicas como startMenuIO , inGameIO , playerIO , gameBoardIO , etc. de tal manera que cada clase tenga aproximadamente 1- 5 métodos?

Y en la misma nota, si es mejor descomponerlos, ¿sería inteligente colocarlos en un espacio de nombres IO , lo que los hace un poco más detallados, por ejemplo: IO.inGame etc. ?

    
pregunta shinzou 20.04.2016 - 11:46

4 respuestas

7

Actualización (resumen)

Desde que escribí una respuesta bastante detallada, esto es lo que se reduce a:

  • Los espacios de nombres son buenos, utilícelos siempre que tenga sentido
  • El uso de las clases inGameIO y playerIO probablemente constituiría una violación del SRP. Probablemente significa que está acoplando la forma en que maneja la E / S con la lógica de la aplicación.
  • Tenga un par de clases genéricas de E / S, que son usadas (o algunas veces compartidas) por clases de manejador. Estas clases de manejadores luego traducirían la entrada en bruto a un formato que la lógica de la aplicación pueda entender.
  • Lo mismo se aplica a la salida: esto se puede realizar mediante clases bastante genéricas, pero pase el estado del juego a través de un objeto de controlador / asignador que traduce el estado interno del juego en algo que las clases de IO genéricas pueden manejar.

Creo que estás viendo esto de manera incorrecta. Está separando el IO en función de los componentes de la aplicación, mientras que, para mí, tiene más sentido tener clases de IO separadas basadas en la fuente, y "type" de IO.

Teniendo algunas clases base / genéricas KeyboardIO classes MouseIO para comenzar, y luego, dependiendo de cuándo y dónde las necesite, tenga subclases que manejen dicho IO de manera diferente.
Por ejemplo, la entrada de texto es algo que probablemente quieras manejar de manera diferente a los controles del juego. Descubrirá que desea asignar ciertas claves de manera diferente según cada caso de uso, pero esa asignación no es parte de la IO en sí, es la forma en que maneja la IO.

Siguiendo con el SRP, tendría un par de clases que puedo usar para el IO del teclado. Dependiendo de la situación, probablemente querré interactuar con estas clases de manera diferente, pero su único trabajo es decirme qué está haciendo el usuario.

Luego inyectaría estos objetos en un objeto de controlador que asignaría el IO en bruto a algo con lo que la lógica de mi aplicación podría funcionar (por ejemplo: el usuario presiona "w" , el controlador se asigna a en MOVE_FORWARD ).

Estos controladores, a su vez, se utilizan para hacer que los personajes se muevan y dibujan la pantalla en consecuencia. Una gran simplificación, pero la esencia de esto es este tipo de estructura:

[ 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

Lo que tenemos ahora es una clase que es responsable de la E / S del teclado en su forma original. Otra clase que traduce estos datos en algo que el motor del juego puede realmente entender, estos datos luego se usan para actualizar el estado de todos los componentes involucrados, y finalmente, una clase separada se hará cargo de la salida a la pantalla. / p>

Cada clase tiene un solo trabajo: el manejo de la entrada del teclado lo realiza una clase que no sabe / cuida / tiene que saber qué significa la entrada que está procesando. Todo lo que hace es saber cómo para obtener la entrada (almacenada en búfer, no almacenada en búfer, ...).

El manejador traduce esto en una representación interna para que el resto de la aplicación tenga sentido de esta información.

El motor del juego toma los datos que se tradujeron y los usa para notificar a todos los componentes relevantes que algo está sucediendo. Cada uno de estos componentes hace una sola cosa, ya sea que se trate de chequeos de colisión o cambios en la animación de los personajes, no importa, eso depende de cada objeto individual.

Estos objetos retransmiten su estado, y estos datos se pasan a Game.Screen , que en esencia es un controlador de IO inverso. Asigna la representación interna a algo que el componente IO.Screen puede usar para generar la salida real.

    
respondido por el Elias Van Ootegem 20.04.2016 - 15:47
23

El principio de responsabilidad única puede ser difícil de entender. Lo que he encontrado útil es pensar en ello como en cómo escribes oraciones. No intentas agrupar muchas ideas en una sola oración. Cada oración debe indicar una idea claramente y diferir los detalles. Por ejemplo, si quisiera definir un automóvil, diría:

  

Un vehículo de carretera, generalmente con cuatro ruedas, impulsado por un motor de combustión interna.

Entonces definirías cosas como "vehículo", "carretera", "ruedas", etc. por separado. No intentarías decir:

  

Un vehículo para transportar personas en una vía, ruta o camino terrestre entre dos lugares pavimentados o mejorados para permitir el viaje que tiene cuatro objetos circulares que giran sobre un eje fijo debajo del vehículo y que funciona con un Motor que genera potencia motriz al quemar gasolina, aceite u otro combustible con aire.

Del mismo modo, debe intentar que sus clases, métodos, etc., establezcan el concepto central de la manera más simple posible y difieran los detalles a otros métodos y clases. Al igual que con la escritura de oraciones, no hay una regla difícil en cuanto a qué tan grandes deben ser.

    
respondido por el Vaughn Cato 20.04.2016 - 16:31
2

Yo diría que la mejor manera de hacerlo es mantenerlos en clases separadas. Las clases pequeñas no son malas, de hecho, la mayoría de las veces son una buena idea.

Con respecto a su caso específico, creo que separarlo puede ayudarlo a cambiar la lógica de cualquiera de esos manejadores específicos sin afectar a los demás y, si es necesario, sería más fácil agregar un nuevo método de entrada / salida si llegara a ello.

    
respondido por el Zalomon 20.04.2016 - 11:52
0

El Director de Responsabilidad Única declara que una clase solo debe tener una razón para cambiar. Si su clase tiene múltiples razones para cambiar, entonces puede dividirla en otras clases y utilizar la composición para eliminar esto. problema.

Para responder tu pregunta, tengo que hacerte una pregunta: ¿Tiene tu clase solo una razón para cambiar? Si no, no tenga miedo de seguir agregando más clases especializadas hasta que cada una tenga una razón para cambiar.

    
respondido por el Greg Burghardt 20.04.2016 - 18:22