¿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 para mantener a todos en una sola inputOutputclase o descomponerlos a más clases específicas como startMenuIO, inGameIO, playerIO, gameBoardIO, etc., tal que cada clase tiene aproximadamente 1-5 métodos?

Y en la misma nota, si es mejor desglosarlos, ¿sería inteligente colocarlos en un IOespacio de nombres para que llamarlos sea un poco más detallado, por ejemplo, IO.inGameetc.?

Shinzou
fuente
Relacionado: nunca he visto una buena razón para combinar entrada y salida. Y cuando los separo deliberadamente, mi código resulta mucho más limpio.
Mooing Duck el
1
¿En qué idioma nombra las clases en lowerCamelCase?
user253751

Respuestas:

7

Actualización (resumen)

Como he escrito una respuesta bastante detallada, esto es lo que todo se reduce a:

  • Los espacios de nombres son buenos, úselos siempre que tenga sentido
  • Usar inGameIOy playerIOclases probablemente constituiría una violación del SRP. Probablemente significa que está acoplando la forma en que maneja IO con la lógica de la aplicación.
  • Tenga un par de clases de IO genéricas, que las clases de controladores usan (o a veces comparten). Estas clases de manejador luego traducirían la entrada sin formato a un formato que la lógica de su aplicación pueda tener sentido.
  • Lo mismo ocurre con la salida: esto puede hacerse mediante clases bastante genéricas, pero pasa el estado del juego a través de un objeto controlador / mapeador que traduce el estado interno del juego en algo que las clases genéricas de IO 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 el "tipo" de IO.

Tener algunas KeyboardIOclases base / genéricas MouseIOpara comenzar, y luego basarse en cuándo y dónde las necesita, tener subclases que manejen dicho IO de manera diferente.
Por ejemplo, el ingreso de texto es algo que probablemente quieras manejar de manera diferente a los controles del juego. Tendrás ganas de asignar ciertas teclas de manera diferente dependiendo de cada caso de uso, pero esa asignación no es parte del IO en sí, es cómo manejas el IO.

Siguiendo el SRP, tendría un par de clases que puedo usar para el teclado IO. Dependiendo de la situación, probablemente quiera 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 mapearía el IO sin procesar en algo con lo que la lógica de mi aplicación pueda trabajar (por ejemplo: el usuario presiona "w" , el controlador lo mapea MOVE_FORWARD).

Estos controladores, a su vez, se utilizan para hacer que los personajes se muevan y dibujar la pantalla en consecuencia. Una simplificación excesiva, pero lo esencial 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 del teclado IO en su forma cruda. Otra clase que traduce estos datos en algo que el motor del juego realmente puede tener sentido, estos datos se utilizan para actualizar el estado de todos los componentes involucrados y, finalmente, una clase separada se encargará de la salida a la pantalla.

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

El controlador 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 utiliza para notificar a todos los componentes relevantes que algo está sucediendo. Cada uno de estos componentes hace solo una cosa, ya sean controles de colisión o cambios en la animación de personajes, no importa, eso depende de cada objeto individual.

Estos objetos luego retransmiten su estado, y estos datos se pasan a Game.Screen, que es esencialmente un manejador de E / S inverso. Mapea la representación interna en algo que el IO.Screencomponente puede usar para generar la salida real.

Elias Van Ootegem
fuente
Como se trata de una aplicación de consola, no hay mouse, y la impresión de los mensajes o el tablero está estrechamente vinculada con la entrada. En su ejemplo, son las IOy los gameespacios de nombres o clases con subclases?
shinzou
@kuhaku: Son espacios de nombres. Lo esencial de lo que estoy diciendo es que, si elige crear subclases en función de la parte de la aplicación en la que se encuentra, está efectivamente acoplando estrechamente la funcionalidad básica de E / S con la lógica de su aplicación. Terminará con clases que son responsables de IO en función de la aplicación . Eso, para mí, suena como una violación del SRP. En cuanto a los nombres frente a las clases de espacios de nombres: tiendo a preferir los espacios de nombres el 95% del tiempo
Elias Van Ootegem
He actualizado mi respuesta, para resumir mi respuesta
Elias Van Ootegem
Sí, ese es realmente otro problema que estoy teniendo (acoplar IO con la lógica), ¿así que en realidad me recomiendan separar la entrada de la salida?
shinzou
@kuhaku: Eso realmente depende de lo que estés haciendo y de lo complejo que sea el material de entrada / salida. Si los manejadores (que traducen el estado del juego frente a la entrada / salida sin procesar) difieren demasiado, entonces es probable que desee separar las clases de entrada y salida, de lo contrario, una sola clase de E / S está bien
Elias Van Ootegem,
23

El principio de responsabilidad única puede ser difícil de entender. Lo que he encontrado útil es pensarlo como 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, típicamente con cuatro ruedas, propulsado por un motor de combustión interna.

Luego definiría cosas como "vehículo", "carretera", "ruedas", etc. por separado. No tratarías de decir:

Un vehículo para transportar personas en una vía pública, ruta o camino en tierra entre dos lugares que ha sido pavimentado o mejorado para permitir el viaje que tiene cuatro objetos circulares que giran en un eje fijado debajo del vehículo y es impulsado por un motor que genera poder 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 al escribir oraciones, no existe una regla estricta sobre cuán grandes deberían ser.

Vaughn Cato
fuente
Entonces, como definir el automóvil con unas pocas palabras en lugar de muchas, ¿está bien definir pequeñas clases específicas pero similares?
shinzou
2
@kuhaku: Lo pequeño es bueno. Específico es bueno. Similar está bien siempre que lo mantenga SECO. Estás tratando de hacer que tu diseño sea fácil de cambiar. Mantener las cosas pequeñas y específicas lo ayuda a saber dónde hacer cambios. Mantenerlo SECO ayuda a evitar tener que cambiar muchos lugares.
Vaughn Cato
66
Su segunda oración se ve como "¡Maldición, la maestra dijo que esto tiene que ser de 4 páginas ... 'genera poder de motivación' ¡es!"
corsiKa
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 tener el separador puede ayudarlo a cambiar la lógica de cualquiera de esos controladores específicos sin afectar a los demás y, si es necesario, sería más fácil para usted agregar un nuevo método de entrada / salida si fuera necesario.

Zalomon
fuente
0

El Director de responsabilidad única establece que una clase solo debe tener una razón para cambiar. Si su clase tiene varias razones para cambiar, puede dividirla en otras clases y utilizar la composición para eliminar este problema.

Para responder a su pregunta, tengo que hacerle una pregunta: ¿tiene su clase una sola razón para cambiar? De lo contrario, no tenga miedo de seguir agregando clases más especializadas hasta que cada una tenga una sola razón para cambiar.

Greg Burghardt
fuente