Quão específico deve ser o padrão de Responsabilidade Única para as aulas?

14

Por exemplo, suponha que você tenha um programa de jogo de console, que possui todos os tipos de métodos de entrada / saída de e para o console. Seria inteligente para mantê-los todos em uma única inputOutputclasse ou quebrá-los para mais classes específicas como startMenuIO, inGameIO, playerIO, gameBoardIO, etc. de forma que cada classe tem cerca de 1-5 métodos?

E na mesma nota, se for melhor decompô-los, seria inteligente colocá-los em um IOespaço para nome, tornando-os um pouco mais detalhados, por exemplo: IO.inGameetc.?

shinzou
fonte
Relacionado: Eu nunca vi um bom motivo para combinar entrada e saída. E quando os separo deliberadamente, meu código fica muito mais limpo.
Mooing Duck
1
Em qual idioma você denomina classes no lowerCamelCase, afinal?
precisa saber é o seguinte

Respostas:

7

Atualização (recapitulação)

Desde que eu escrevi uma resposta bastante detalhada, aqui está o que tudo se resume a:

  • Os namespaces são bons, use-os sempre que fizer sentido
  • O uso inGameIOe as playerIOclasses provavelmente constituiriam uma violação do SRP. Provavelmente significa que você está acoplando a maneira como lida com as E / S com a lógica do aplicativo.
  • Tenha algumas classes de E / S genéricas, que são usadas (ou às vezes compartilhadas) pelas classes de manipulador. Essas classes de manipuladores traduzem a entrada bruta para um formato que sua lógica de aplicativo possa entender.
  • O mesmo vale para a saída: isso pode ser feito por classes bastante genéricas, mas passe o estado do jogo através de um objeto manipulador / mapeador que traduza o estado interno do jogo em algo que as classes de E / S genéricas possam manipular.

Eu acho que você está vendo isso da maneira errada. Você está separando o IO em função dos componentes do aplicativo, enquanto - para mim - faz mais sentido ter classes de IO separadas com base na origem e no "tipo" de IO.

Tendo algumas KeyboardIOclasses básicas / genéricas MouseIOpara começar e, depois, com base em quando e onde você precisar delas, tenha subclasses que lidam com o referido IO de maneira diferente.
Por exemplo, a entrada de texto é algo que você provavelmente deseja manipular de maneira diferente dos controles do jogo. Você vai querer mapear determinadas chaves de maneira diferente, dependendo de cada caso de uso, mas esse mapeamento não faz parte do próprio IO, é como você está lidando com o IO.

Seguindo o SRP, eu teria algumas classes que posso usar para E / S de teclado. Dependendo da situação, provavelmente desejarei interagir com essas classes de maneira diferente, mas o único trabalho deles é me dizer o que o usuário está fazendo.

Eu injetava esses objetos em um objeto manipulador que mapeia a IO bruta para algo com o qual minha lógica de aplicativo possa trabalhar (por exemplo: o usuário pressiona "w" , o manipulador mapeia isso MOVE_FORWARD).

Esses manipuladores, por sua vez, são usados ​​para fazer os personagens se moverem e desenharem a tela de acordo. Uma simplificação grosseira, mas a essência é esse tipo de estrutura:

[ 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

O que temos agora é uma classe responsável pelo IO do teclado em sua forma bruta. Outra classe que traduz esses dados em algo que o mecanismo do jogo pode realmente entender, esses dados são usados ​​para atualizar o estado de todos os componentes envolvidos e, finalmente, uma classe separada cuidará da saída da tela.

Cada classe tem um único trabalho: o tratamento da entrada do teclado é feito por uma classe que não sabe / se importa / precisa saber o significado da entrada que está processando. Tudo o que faz é saber como obter a entrada (em buffer, sem buffer, ...).

O manipulador converte isso em uma representação interna para o restante do aplicativo, a fim de entender essas informações.

O mecanismo de jogo pega os dados que foram traduzidos e os utiliza para notificar todos os componentes relevantes de que algo está acontecendo. Cada um desses componentes faz apenas uma coisa, sejam verificações de colisão ou alterações na animação de personagens, não importa, isso depende de cada objeto individual.

Esses objetos retransmitem seu estado de volta e esses dados são passados ​​para Game.Screen, o que é essencialmente um manipulador de E / S inverso. Ele mapeia a representação interna em algo que o IO.Screencomponente pode usar para gerar a saída real.

Elias Van Ootegem
fonte
Como é um aplicativo de console, não há mouse, e a impressão das mensagens ou do quadro está fortemente ligada à entrada. No seu exemplo, são os IOe gamenamespaces ou classes com subclasses?
Shinzou 20/04
@kuhaku: Eles são namespaces. A essência do que estou dizendo é que, se você optar por criar subclasses com base na parte do aplicativo em que está, estará efetivamente acoplando fortemente a funcionalidade básica de IO à lógica do aplicativo. Você terminará com as classes responsáveis ​​pelo IO em função do aplicativo . Isso, para mim, parece uma violação do SRP. Quanto aos nomes vs aulas namespaced: Eu tendem a preferir namespaces 95% do tempo
Elias Van Ootegem
Eu atualizei a minha resposta, para resumir a minha resposta
Elias Van Ootegem
Sim, isso é realmente outro problema que estou tendo (acoplamento de E / S com a lógica), então você realmente recomenda separar entrada e saída?
Shinzou
@kuhaku: Isso realmente depende do que você está fazendo e da complexidade das coisas de entrada / saída. Se os manipuladores (traduzindo estado do jogo vs entrada bruta / saída) diferem muito, então você provavelmente vai querer separar as classes de entrada e saída, se não, uma única classe IO é bom
Elias Van Ootegem
23

O princípio da responsabilidade única pode ser difícil de entender. O que eu achei útil é pensar nisso como você escreve frases. Você não tenta colocar muitas idéias em uma única frase. Cada frase deve indicar claramente uma idéia e adiar os detalhes. Por exemplo, se você quisesse definir um carro, diria:

Um veículo rodoviário, normalmente com quatro rodas, alimentado por um motor de combustão interna.

Em seguida, você definiria coisas como "veículo", "estrada", "rodas" etc. etc. separadamente. Você não tentaria dizer:

Um veículo para transportar pessoas em uma via, via ou via terrestre entre dois lugares que foram pavimentados ou melhorados para permitir viagens com quatro objetos circulares que giram em um eixo fixo abaixo do veículo e são acionados por um motor que gera força motriz pela queima de gasolina, óleo ou outro combustível no ar.

Da mesma forma, você deve tentar tornar suas classes, métodos, etc, declarar o conceito central da maneira mais simples possível e adiar os detalhes para outros métodos e classes. Assim como nas frases escritas, não existe uma regra rígida sobre o tamanho delas.

Vaughn Cato
fonte
Então, como definir o carro com poucas palavras em vez de muitas, então definir pequenas classes específicas, mas semelhantes, é bom?
Shinzou
2
@kuhaku: Pequeno é bom. Específico é bom. Semelhante é bom, desde que você o mantenha SECO. Você está tentando facilitar a alteração do seu design. Manter as coisas pequenas e específicas ajuda a saber onde fazer as alterações. Mantê-lo seco ajuda a evitar a necessidade de mudar muitos lugares.
Vaughn Cato
6
Sua segunda frase parece "Droga, o professor disse que isso precisa ter 4 páginas ... 'gera força motriz', é !!"
corsiKa
2

Eu diria que o melhor caminho a seguir é mantê-los em classes separadas. As turmas pequenas não são ruins, na verdade, na maioria das vezes, são uma boa ideia.

Em relação ao seu caso específico, acho que ter o separado pode ajudá-lo a alterar a lógica de qualquer um desses manipuladores específicos sem afetar os outros e, se necessário, seria mais fácil adicionar um novo método de entrada / saída, se fosse o caso.

Zalomon
fonte
0

O Diretor de Responsabilidade Única declara que uma classe deve ter apenas um motivo para mudar. Se a sua turma tiver vários motivos para mudar, você poderá dividi-la em outras turmas e utilizar a composição para eliminar esse problema.

Para responder sua pergunta, tenho que fazer uma pergunta: Sua turma tem apenas um motivo para mudar? Caso contrário, não tenha medo de continuar adicionando classes mais especializadas até que cada uma tenha apenas um motivo para mudar.

Greg Burghardt
fonte