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ário printReport
. 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 como IPrintable<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 usar IPrintable<T>
e, portanto, operar em qualquer tipo de relatório imprimível, o que fornece todos os benefícios percebidos de uma Report
classe base com um print
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ãoIncomeStatement
é 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 de getTotalRevenues()
, getTotalExpenses()
e getNetIncome()
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
e print
, 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 um print(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.