Simplesmente quais são as técnicas práticas que as pessoas usam para verificar se uma classe viola o princípio da responsabilidade única?
Eu sei que uma classe deve ter apenas um motivo para mudar, mas essa frase carece de um modo prático de realmente implementá-la.
A única maneira que encontrei é usar a frase "O ......... deveria .........". onde o primeiro espaço é o nome da classe e o posterior é o nome do método (responsabilidade).
No entanto, às vezes é difícil descobrir se uma responsabilidade realmente viola o SRP.
Existem mais maneiras de verificar o SRP?
Nota:
A questão não é sobre o que significa o SRP, mas uma metodologia prática ou uma série de etapas para verificar e implementar o SRP.
ATUALIZAR
Eu adicionei uma classe de exemplo que viola claramente o SRP. Seria ótimo se as pessoas pudessem usá-lo como exemplo para explicar como abordam o princípio da responsabilidade única.
O exemplo é daqui .
Respostas:
O SRP declara, em termos inequívocos, que uma classe deve ter apenas um motivo para mudar.
Desconstruindo a classe "report" na pergunta, ela possui três métodos:
printReport
getReportData
formatReport
Ignorando o redundante
Report
usado em todos os métodos, é fácil entender por que isso viola o SRP:O termo "impressão" implica algum tipo de interface do usuário ou uma impressora real. Essa classe, portanto, contém uma certa quantidade de interface do usuário ou lógica de apresentação. Uma alteração nos requisitos da interface do usuário exigirá uma alteração na
Report
classe.O termo "dados" implica algum tipo de estrutura de dados, mas na verdade não especifica o quê (XML? JSON? CSV?). Independentemente disso, se o "conteúdo" do relatório for alterado, esse método também será alterado. Há acoplamento a um banco de dados ou a um domínio.
formatReport
é apenas um nome terrível para um método em geral, mas eu consideraria que, mais uma vez, tem algo a ver com a interface do usuário e provavelmente um aspecto diferente da interface do usuárioprintReport
. Então, outro motivo não relacionado para mudar.Portanto, essa classe é possivelmente acoplada a um banco de dados, um dispositivo de tela / impressora e alguma lógica de formatação interna para logs ou saída de arquivos ou outros enfeites. Ao ter todas as três funções em uma classe, você está multiplicando o número de dependências e triplicando a probabilidade de qualquer dependência ou alteração de requisito quebrar essa classe (ou qualquer outra coisa que depende dela).
Parte do problema aqui é que você escolheu um exemplo particularmente espinhoso. Você provavelmente não deve ter uma classe chamada
Report
, mesmo que isso faça apenas uma coisa , porque ... que relatório? Não são todos os "relatórios" bestas completamente diferentes, com base em dados diferentes e requisitos diferentes? E um relatório não é algo que já foi formatado, seja para tela ou para impressão?Mas, olhando além disso e criando um nome concreto hipotético - vamos chamá-lo
IncomeStatement
(um relatório muito comum) - uma arquitetura "SRPed" adequada teria três tipos:IncomeStatement
- o domínio e / ou a classe do modelo que contém e / ou calcula as informações que aparecem nos relatórios formatados.IncomeStatementPrinter
, que provavelmente implementaria alguma interface padrão comoIPrintable<T>
. Possui um método-chave,,Print(IncomeStatement)
e talvez outros métodos ou propriedades para definir configurações específicas da impressão.IncomeStatementRenderer
, que lida com a renderização da tela e é muito semelhante à classe da impressora.Você também pode adicionar mais classes específicas de recursos como
IncomeStatementExporter
/IExportable<TReport, TFormat>
.Isso é facilitado significativamente em idiomas modernos com a introdução de contêineres genéricos e IoC. A maior parte do código do aplicativo não precisa depender da
IncomeStatementPrinter
classe específica , pode usarIPrintable<T>
e, portanto, operar em qualquer tipo de relatório imprimível, o que fornece todos os benefícios percebidos de umaReport
classe base com umprint
método e nenhuma das violações usuais do SRP . A implementação real precisa ser declarada apenas uma vez, no registro de contêiner de IoC.Algumas pessoas, quando confrontadas com o design acima, respondem com algo como: "mas isso parece código processual, e todo o objetivo do OOP era nos afastar da separação de dados e comportamento!" Para o que eu digo: errado .
Isso não
IncomeStatement
é apenas "dados", e o erro mencionado é o que leva muitas pessoas de OOP a sentirem que estão fazendo algo errado ao criar uma classe "transparente" e, posteriormente, começar a colocar todos os tipos de funcionalidades não relacionadas no (bem, isso preguiça geral). Essa classe pode começar apenas como dados, mas, com o tempo, garantida, acabará sendo mais um modelo .IncomeStatement
Por exemplo, uma declaração de renda real tem receitas totais , despesas totais , e lucro líquido linhas. Um sistema financeiro adequadamente projetado provavelmente não os armazenará porque não são dados transacionais - na verdade, eles mudam com base na adição de novos dados transacionais. No entanto, o cálculo dessas linhas sempre será exatamente o mesmo, independentemente de você estar imprimindo, processando ou exportando o relatório. Portanto, sua
IncomeStatement
classe vai ter uma quantidade razoável de comportamento a ele na forma degetTotalRevenues()
,getTotalExpenses()
egetNetIncome()
métodos, e provavelmente vários outros. É um objeto genuíno no estilo OOP com seu próprio comportamento, mesmo que não pareça realmente "fazer" muito.Mas os métodos
format
eprint
, eles não têm nada a ver com as informações em si. De fato, não é muito improvável que você deseje ter várias implementações desses métodos, por exemplo, uma declaração detalhada para a gerência e uma declaração não tão detalhada para os acionistas. A separação dessas funções independentes em classes diferentes permite escolher diferentes implementações em tempo de execução, sem a carga de umprint(bool includeDetails, bool includeSubtotals, bool includeTotals, int columnWidth, CompanyLetterhead letterhead, ...)
método único para todos . Que nojo!Esperamos que você possa ver onde o método acima, massivamente parametrizado, dá errado e onde as implementações separadas dão certo; no caso de objeto único, toda vez que você adiciona uma nova ruga à lógica de impressão, é necessário alterar o modelo de domínio ( Tim in finance deseja números de página, mas apenas no relatório interno, você pode adicioná-lo? ) em vez de apenas adicionando uma propriedade de configuração a uma ou duas classes de satélite.
A implementação adequada do SRP é sobre o gerenciamento de dependências . Em poucas palavras, se uma classe já faz algo útil, e você está pensando em adicionar outro método que introduza uma nova dependência (como uma interface do usuário, uma impressora, uma rede, um arquivo, o que for), não . Pense em como você poderia adicionar essa funcionalidade em uma nova classe e como fazer com que essa nova classe se encaixasse em sua arquitetura geral (é muito fácil quando você projeta em torno da injeção de dependência). Esse é o princípio / processo geral.
Nota lateral: como Robert, rejeito claramente a noção de que uma classe compatível com SRP deve ter apenas uma ou duas variáveis de estado. Um invólucro tão fino raramente poderia fazer algo realmente útil. Portanto, não exagere nisso.
fonte
IncomeStatement
. Será que o seu projeto proposto significa que oIncomeStatement
terão instâncias deIncomeStatementPrinter
&IncomeStatementRenderer
de modo que quando eu chamoprint()
noIncomeStatement
que vai delegar a chamada paraIncomeStatementPrinter
em vez disso?IncomeStatement
classe não possua umprint
método,format
método ou qualquer outro método que não lide diretamente com a inspeção ou manipulação dos dados do relatório. É para isso que servem essas outras classes. Se você deseja imprimir um, assume uma dependência daIPrintable<IncomeStatement>
interface que está registrada no contêiner.Printer
instância naIncomeStatement
classe? da maneira que eu imagino que seja quando eu ligoIncomeStatement.print()
, delegaráIncomeStatementPrinter.print(this, format)
. O que há de errado com essa abordagem? ... Outra pergunta, você mencionou queIncomeStatement
deve conter as informações que aparecem nos relatórios formatados, se eu quiser que sejam lidas no banco de dados ou em um arquivo XML, devo extrair o método que carrega os dados em uma classe separada e delegar a chamada para elaIncomeStatement
?IncomeStatementPrinter
dependendoIncomeStatement
eIncomeStatement
dependendoIncomeStatementPrinter
. Essa é uma dependência cíclica. E é apenas um design ruim; não há nenhuma razãoIncomeStatement
para saber alguma coisa sobre umPrinter
ouIncomeStatementPrinter
- é um modelo de domínio, não se preocupa com a impressão e a delegação não faz sentido, pois qualquer outra classe pode criar ou adquirir umIncomeStatementPrinter
. Não há um bom motivo para ter noção de impressão no modelo de domínio.IncomeStatement
banco de dados (ou arquivo XML) - normalmente, isso é tratado por um repositório e / ou mapeador, não pelo domínio e, mais uma vez, você não o delega no domínio; se alguma outra classe precisar ler um desses modelos, ele solicitará esse repositório explicitamente . A menos que você esteja implementando o padrão Active Record, eu acho, mas não sou realmente fã.A maneira como eu verifico o SRP é verificar todos os métodos (responsabilidades) de uma classe e fazer a seguinte pergunta:
"Vou precisar mudar a maneira de implementar essa função?"
Se eu encontrar uma função que precisarei implementar de maneiras diferentes (dependendo de algum tipo de configuração ou condição), tenho certeza de que preciso de uma classe extra para lidar com essa responsabilidade.
fonte
Aqui está uma citação da regra 8 da Object Calisthenics :
Dada essa visão (um tanto idealista), você poderia dizer que é improvável que qualquer classe que contenha apenas uma ou duas variáveis de estado viole o SRP. Você também pode dizer que qualquer classe que contenha mais de duas variáveis de estado pode violar o SRP.
fonte
Uma possível implementação (em Java). Tomei liberdades com os tipos de retorno, mas, acima de tudo, acho que responde à pergunta. TBH Eu não acho que a interface para a classe Report seja tão ruim, embora um nome melhor possa estar em ordem. Eu deixei de fora declarações e afirmações de guarda por questões de concisão.
EDIT: Observe também que a classe é imutável. Então, uma vez criado, você não pode mudar nada. Você pode adicionar um setFormatter () e um setPrinter () e não ter muitos problemas. A chave, IMHO, é não alterar os dados brutos após a instanciação.
fonte
if (reportData == null)
, presumo que você queira dizerdata
. Em segundo lugar, eu esperava saber como você chegou a essa implementação. Como por que você decidiu delegar todas as chamadas para outros objetos? Mais uma coisa que sempre me perguntei: é realmente responsabilidade de um relatório imprimir-se ?! Por que você não criou umaprinter
classe separada que leva umreport
em seu construtor?Printer
turma que recebe um relatório ou umaReport
turma que leva uma impressora? Eu encontrei um problema semelhante antes em que tive que analisar um relatório e discuti com meu TL se deveríamos criar um analisador que receba um relatório ou se o relatório deveria ter um analisador dentro dele e aparse()
chamada fosse delegada a ele.No seu exemplo, não está claro que o SRP está sendo violado. Talvez o relatório possa formatar e imprimir a si mesmo, se eles forem relativamente simples:
Os métodos são tão simples que não faz sentido ter
ReportFormatter
ouReportPrinter
classes. O único problema gritante na interface égetReportData
porque ela viola a pergunta, não conte sobre o objeto sem valor.Por outro lado, se os métodos são muito complicados ou existem muitas maneiras de formatar ou imprimir um
Report
, faz sentido delegar a responsabilidade (também mais testável):O SRP é um princípio de design, não um conceito filosófico e, portanto, é baseado no código real com o qual você está trabalhando. Semanticamente, você pode dividir ou agrupar uma classe em quantas responsabilidades desejar. No entanto, como princípio prático, o SRP deve ajudá-lo a encontrar o código que você precisa modificar . Os sinais de que você está violando o SRP são:
Você pode corrigi-los através da refatoração, aprimorando nomes, agrupando códigos semelhantes, eliminando a duplicação, usando um design em camadas e dividindo / combinando classes, conforme necessário. A melhor maneira de aprender SRP é mergulhar em uma base de código e refatorar a dor.
fonte
Printer
turma que recebe um relatório ou umaReport
turma que leva uma impressora? Muitas vezes me deparo com essa questão de design antes de descobrir se o código será complexo ou não.O Princípio da Responsabilidade Única está altamente associado à noção de coesão . Para ter uma classe altamente coesa, você precisa ter uma co-dependência entre as variáveis de instância da classe e seus métodos; isto é, cada um dos métodos deve manipular o maior número possível de variáveis de instância. Quanto mais variáveis um método usa, mais coesa é a sua classe; coesão máxima é geralmente inatingível.
Além disso, para aplicar bem o SRP, você entende bem o domínio da lógica de negócios; para saber o que cada abstração deve fazer. A arquitetura em camadas também está relacionada ao SRP, fazendo com que cada camada faça uma coisa específica (a Camada de Fonte de Dados deve fornecer dados e assim por diante).
Voltando à coesão, mesmo que seus métodos não usem todas as variáveis, eles devem ser acoplados:
Você não deve ter algo parecido com o código abaixo, onde uma parte das variáveis de instância é usada em uma parte dos métodos e a outra parte das variáveis é usada na outra parte dos métodos (aqui você deve ter duas classes para cada parte das variáveis).
fonte