Escrevo muito código que envolve três etapas básicas.
- Obtenha dados de algum lugar.
- Transforme esses dados.
- Coloque esses dados em algum lugar.
Normalmente, acabo usando três tipos de classes - inspirados em seus respectivos padrões de design.
- Fábricas - para construir um objeto a partir de algum recurso.
- Mediadores - para usar a fábrica, execute a transformação e use o comandante.
- Comandantes - para colocar esses dados em outro lugar.
Minhas aulas tendem a ser muito pequenas, geralmente um método único (público), por exemplo, obter dados, transformar dados, trabalhar, salvar dados. Isso leva a uma proliferação de classes, mas em geral funciona bem.
Onde eu luto é quando chego aos testes, acabo fazendo testes fortemente acoplados. Por exemplo;
- Fábrica - lê arquivos do disco.
- Comandante - grava arquivos no disco.
Não posso testar um sem o outro. Eu poderia escrever código 'teste' adicional para fazer leitura / gravação em disco também, mas depois estou me repetindo.
Olhando para .Net, a classe File adota uma abordagem diferente, ela combina as responsabilidades (da minha) fábrica e comandante. Possui funções para criar, excluir, existir e ler em um só lugar.
Devo procurar seguir o exemplo de .Net e combinar - principalmente ao lidar com recursos externos - minhas aulas juntos? O código ainda está associado, mas é mais intencional - acontece na implementação original, e não nos testes.
É minha questão aqui que apliquei o Princípio de Responsabilidade Única de maneira um tanto exagerada? Tenho aulas separadas, responsáveis pela leitura e gravação. Quando eu poderia ter uma classe combinada responsável por lidar com um recurso específico, por exemplo, disco do sistema.
fonte
Looking at .Net, the File class takes a different approach, it combines the responsibilities (of my) factory and commander together. It has functions for Create, Delete, Exists, and Read all in one place.
- Observe que você está confundindo "responsabilidade" com "coisa a fazer". Uma responsabilidade é mais como uma "área de preocupação". A responsabilidade da classe File está executando operações de arquivo.File
biblioteca do C # é que, pelo que sabemos, aFile
classe pode ser apenas uma fachada, colocando todas as operações de arquivo em um único local - na classe, mas poderia estar internamente usando classes de leitura / gravação semelhantes às suas, o que na verdade, contém a lógica mais complicada para manipulação de arquivos. Essa classe (theFile
) ainda aderiria ao SRP, porque o processo de realmente trabalhar com o sistema de arquivos seria abstraído por trás de outra camada - provavelmente com uma interface unificadora. Não estou dizendo que é o caso, mas poderia ser. :)Respostas:
Seguir o princípio de responsabilidade única pode ter sido o que o guiou até aqui, mas onde você está tem um nome diferente.
Segregação de responsabilidade de consulta de comando
Vá estudá-lo e acho que você o seguirá de acordo com um padrão familiar e que não ficará sozinho pensando em até que ponto levar isso. O teste de ácido é se, ao seguir isso, você obtém benefícios reais ou se é apenas um mantra cego que você segue, para que você não precise pensar.
Você expressou preocupação com os testes. Eu não acho que seguir o CQRS se impeça de escrever código testável. Você pode simplesmente seguir o CQRS de uma maneira que torne seu código não testável.
Ajuda a saber como usar o polimorfismo para inverter as dependências do código fonte sem precisar alterar o fluxo de controle. Não tenho muita certeza de onde está seu conjunto de habilidades para escrever testes.
Uma palavra de cautela: seguir os hábitos encontrados nas bibliotecas não é o ideal. As bibliotecas têm suas próprias necessidades e são francamente antigas. Assim, mesmo o melhor exemplo é apenas o melhor exemplo da época.
Isso não quer dizer que não haja exemplos perfeitamente válidos que não sigam o CQRS. Depois disso sempre será um pouco doloroso. Nem sempre vale a pena pagar. Mas se você precisar, ficará feliz em usá-lo.
Se você o usar, observe esta palavra de aviso:
fonte
Você precisa de uma perspectiva mais ampla para determinar se o código está em conformidade com o Princípio da Responsabilidade Única. Ele não pode ser respondido apenas analisando o código em si; é necessário considerar quais forças ou atores podem fazer com que os requisitos mudem no futuro.
Digamos que você armazene dados do aplicativo em um arquivo XML. Quais fatores podem fazer com que você altere o código relacionado à leitura ou gravação? Algumas possibilidades:
Em todos estes casos, o que você vai ter que mudar tanto a leitura e a lógica da escrita. Em outras palavras, eles não são responsabilidades separadas.
Mas vamos imaginar um cenário diferente: seu aplicativo faz parte de um pipeline de processamento de dados. Ele lê alguns arquivos CSV gerados por um sistema separado, realiza algumas análises e processamento e, em seguida, gera um arquivo diferente para ser processado por um terceiro sistema. Nesse caso, a leitura e a escrita são responsabilidades independentes e devem ser dissociadas.
Conclusão: em geral, você não pode dizer se a leitura e gravação de arquivos são responsabilidades separadas, isso depende das funções no aplicativo. Mas, com base na sua dica sobre testes, acho que é uma responsabilidade única no seu caso.
fonte
Geralmente você tem a ideia certa.
Parece que você tem três responsabilidades. OMI, o "Mediador" pode estar fazendo muito. Eu acho que você deve começar modelando suas três responsabilidades:
Em seguida, um programa pode ser expresso como:
Eu não acho que isso seja um problema. Muitas IMO pequenas classes coesas e testáveis são melhores do que as classes grandes e menos coesas.
Cada peça deve ser testada independentemente. Modelado acima, você pode representar a leitura / gravação em um arquivo como:
Você pode escrever testes de integração para testar essas classes e verificar se elas lêem e gravam no sistema de arquivos. O restante da lógica pode ser escrito como transformações. Por exemplo, se os arquivos estiverem no formato JSON, você poderá transformar os
String
.Então você pode transformar em objetos adequados:
Cada um destes é testável independentemente. Você também pode teste de unidade
program
acima zombandoreader
,transformer
ewriter
.fonte
FileWriter
lendo diretamente do sistema de arquivos em vez de usarFileReader
. É realmente você quem decide seus objetivos. Se você usarFileReader
, o teste será interrompido se umFileReader
ouFileWriter
estiver quebrado - o que pode levar mais tempo para depurar.Portanto, o foco aqui é o que os une . Você passa um objeto entre os dois (como um
File
?) Então é o arquivo ao qual eles estão acoplados, não um ao outro.Do que você disse, você separou suas aulas. A armadilha é que você os está testando juntos porque é mais fácil ou 'faz sentido' .
Por que você precisa que a entrada
Commander
venha de um disco? Tudo o que importa é escrever usando uma determinada entrada, e você pode verificar se ele escreveu o arquivo corretamente usando o que está no teste .A parte real que você está testando
Factory
é 'ele lerá esse arquivo corretamente e produzirá a coisa certa'? Portanto, zombe do arquivo antes de lê-lo no teste .Como alternativa, é bom testar se o Factory e o Commander funcionam quando acoplados - ele se alinha muito bem com o Teste de integração. A questão aqui é mais uma questão de saber se a sua unidade pode ou não testá-los separadamente.
fonte
É uma abordagem processual típica, sobre a qual David Parnas escreveu em 1972. Você se concentra em como as coisas estão indo. Você considera a solução concreta do seu problema como um padrão de nível superior, que está sempre errado.
Se você seguir uma abordagem orientada a objetos, prefiro me concentrar no seu domínio . Sobre o que é tudo isso? Quais são as principais responsabilidades do seu sistema? Quais são os principais conceitos presentes no idioma dos seus especialistas em domínio? Portanto, entenda seu domínio, decomponha-o, trate áreas de responsabilidade de nível superior como seus módulos , trate conceitos de nível inferior representados como substantivos como seus objetos. Aqui está um exemplo que forneci para uma pergunta recente, é muito relevante.
E há um evidente problema de coesão, você mesmo mencionou. Se você fizer alguma modificação é uma lógica de entrada e escrever testes nela, isso não prova que sua funcionalidade funciona, pois você pode esquecer de passar esses dados para a próxima camada. Veja, essas camadas são acopladas intrinsecamente. E um desacoplamento artificial torna as coisas ainda piores. Eu sei disso: projeto de sete anos com 100 homens-ano atrás dos ombros, escrito completamente nesse estilo. Fuja disso, se puder.
E em toda a coisa SRP. É tudo sobre coesão aplicada ao seu espaço problemático, ou seja, domínio. Esse é o princípio fundamental por trás do SRP. Isso resulta em objetos inteligentes e na implementação de suas responsabilidades por si mesmos. Ninguém os controla, ninguém os fornece dados. Eles combinam dados e comportamento, expondo apenas os últimos. Portanto, seus objetos combinam validação de dados brutos, transformação de dados (ou seja, comportamento) e persistência. Pode parecer com o seguinte:
Como resultado, existem várias classes coesas que representam alguma funcionalidade. Observe que a validação normalmente vai para objetos de valor - pelo menos na abordagem DDD .
fonte
Cuidado com as abstrações com vazamentos ao trabalhar com o sistema de arquivos - eu o vi negligenciado com muita frequência e tem os sintomas que você descreveu.
Se a classe operar com dados provenientes de / entra nesses arquivos, o sistema de arquivos se torna detalhes de implementação (E / S) e deve ser separado dele. Essas classes (fábrica / comandante / mediador) não devem estar cientes do sistema de arquivos, a menos que seu único trabalho seja armazenar / ler os dados fornecidos. As classes que lidam com o sistema de arquivos devem encapsular parâmetros específicos do contexto, como caminhos (podem ser passados pelo construtor), para que a interface não revele sua natureza (a palavra "Arquivo" no nome da interface é um cheiro na maioria das vezes).
fonte
Na minha opinião, parece que você começou a seguir o caminho certo, mas não o levou longe o suficiente. Eu acho que dividir a funcionalidade em diferentes classes que fazem uma coisa e fazem bem é correto.
Para dar um passo adiante, você deve criar interfaces para as classes Factory, Mediador e Commander. Em seguida, você pode usar versões simuladas dessas classes ao escrever seus testes de unidade para as implementações concretas das outras. Com as zombarias, você pode validar se os métodos são chamados na ordem correta e com os parâmetros corretos e se o código em teste se comporta adequadamente com diferentes valores de retorno.
Você também pode abstrair a leitura / gravação dos dados. Você está acessando um sistema de arquivos agora, mas pode querer acessar um banco de dados ou mesmo um soquete em algum momento no futuro. Sua classe de mediador não precisa ser alterada se a origem / destino dos dados mudar.
fonte