Quais são as formas práticas de implementar o SRP?

11

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

Classe de relatório

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 .

Songo
fonte
Essa é uma regra interessante, mas você ainda pode escrever: "Uma classe de pessoa pode se render". Isso pode ser considerado uma violação do SRP, pois incluir a GUI na mesma classe que contém regras de negócios e persistência de dados não é aceitável. Então eu acho que você precisa adicionar o conceito de domínios arquitectónicos (camadas e camadas) e certifique-se que esta afirmação é válida com um daqueles domínio somente (como GUI, acesso a dados, etc.)
NoChance
@EmmadKareem Essa regra foi mencionada no Head First Orientada a Objetos de Análise e Design e foi exatamente isso que pensei sobre isso. Falta um pouco a maneira prática de implementá-lo. Eles mencionaram que, às vezes, as responsabilidades não serão tão aparentes para o designer e ele deve usar muito senso comum para julgar se o método deve realmente estar nessa classe ou não.
Songo
Se você realmente quer entender o SRP, leia alguns dos escritos do tio Bob Martin. Seu código é um dos mais bonitos que eu já vi, e acredito que tudo o que ele disser sobre SRP não é apenas um bom conselho, mas também é mais do que apenas um aceno de mão.
Robert Harvey
E o eleitor que está abaixo do favor explica por que melhorar o post ?!
Songo

Respostas:

7

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 Reportusado 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 Reportclasse.

  • 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 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 IncomeStatementPrinterclasse 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 Reportclasse base com um printmé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 IncomeStatementclasse 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 formate 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.

Aaronaught
fonte
+1 ótima resposta de fato. No entanto, estou confuso sobre a aula IncomeStatement. Será que o seu projeto proposto significa que o IncomeStatementterão instâncias de IncomeStatementPrinter& IncomeStatementRendererde modo que quando eu chamo print()no IncomeStatementque vai delegar a chamada para IncomeStatementPrinterem vez disso?
Songo
@Ongo: Absolutamente não! Você não deve ter dependências cíclicas se estiver seguindo o SOLID. Aparentemente, minha resposta não deixou claro o suficiente para que a IncomeStatementclasse não possua um printmétodo, formatmé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 da IPrintable<IncomeStatement>interface que está registrada no contêiner.
Aaronaught 8/08/12
aah, eu entendo o seu ponto. No entanto, onde está a dependência cíclica se eu injetar uma Printerinstância na IncomeStatementclasse? da maneira que eu imagino que seja quando eu ligo IncomeStatement.print(), delegará IncomeStatementPrinter.print(this, format). O que há de errado com essa abordagem? ... Outra pergunta, você mencionou que IncomeStatementdeve 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 ela IncomeStatement?
Songo
@Ongo: você tem IncomeStatementPrinterdependendo IncomeStatemente IncomeStatementdependendo IncomeStatementPrinter. Essa é uma dependência cíclica. E é apenas um design ruim; não há nenhuma razão IncomeStatementpara saber alguma coisa sobre um Printerou IncomeStatementPrinter- é 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 um IncomeStatementPrinter. Não há um bom motivo para ter noção de impressão no modelo de domínio.
Aaronaught 9/08/12
Quanto à forma como você carrega o IncomeStatementbanco 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ã.
Aaronaught 9/08/12
2

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.

John Raya
fonte
1

Aqui está uma citação da regra 8 da Object Calisthenics :

A maioria das classes deve ser simplesmente responsável por manipular uma única variável de estado, mas há algumas que exigirão duas. A adição de uma nova variável de instância a uma classe diminui imediatamente a coesão dessa classe. Em geral, ao programar sob essas regras, você encontrará dois tipos de classes, as que mantêm o estado de uma única variável de instância e as que coordenam duas variáveis ​​separadas. Em geral, não misture os dois tipos de responsabilidades

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.

MattDavey
fonte
2
Essa visão é irremediavelmente simplista. Até a famosa, mas simples equação de Einstein, requer duas variáveis.
Robert Harvey
A pergunta dos OPs foi "Existem mais maneiras de verificar o SRP?" - este é um indicador possível. Sim, é simplista e não se sustenta em todos os casos, mas é uma maneira possível de verificar se o SRP foi violado.
precisa saber é o seguinte
11
Eu suspeito que o estado mutável vs imutável também é uma consideração importante
jk.
A Regra 8 descreve o processo perfeito para criar designs que possuem milhares e milhares de classes, o que torna o sistema irremediavelmente complexo, incompreensível e insustentável. Mas o lado positivo é que você segue o SRP.
Dunk
@ Dunk Eu não discordo de você, mas essa discussão é totalmente fora de tópico para a pergunta.
precisa saber é o seguinte
1

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.

public class Report
{
    private ReportData data;
    private ReportDataDao dao;
    private ReportFormatter formatter;
    private ReportPrinter printer;


    /*
     *  Parameterized constructor for depndency injection, 
     *  there are better ways but this is explicit.
     */
    public Report(ReportDataDao dao, 
        ReportFormatter formatter, ReportPrinter printer)
    {
        super();
        this.dao = dao;
        this.formatter = formatter;
        this.printer = printer;
    }

    /*
     * Delegates to the injected printer.
     */
    public void printReport()
    {
        printer.print(formatReport());
    }


    /*
     * Lazy loading of data, delegates to the dao 
     * for the meat of the call.
     */
    public ReportData getReportData()
    {
        if (reportData == null)
        {
            reportData = dao.loadData();
        }
        return reportData;
    }

    /*
     * Delegate to the formatter for formatting 
     * (notice a pattern here).
     */
    public ReportData formatReport()
    {
        formatter.format(getReportData());
    }
}
Heath Lilley
fonte
Obrigado pela implementação. Eu tenho duas coisas, na linha if (reportData == null), presumo que você queira dizer data. 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 uma printerclasse separada que leva um reportem seu construtor?
Songo
Sim, reportData = data, desculpe por isso. A delegação permite um controle refinado das dependências. No tempo de execução, você pode fornecer implementações alternativas para cada componente. Agora você pode ter um HtmlPrinter, PdfPrinter, JsonPrinter, ... etc. Isso também é útil para testes, pois você pode testar seus componentes delegados isoladamente e integrados no objeto acima. Você certamente poderia inverter o relacionamento entre impressora e relatório, só queria mostrar que era possível fornecer solução com a interface de classe fornecida. É um hábito de trabalhar em sistemas legados :).
Heath Lilley
hmmmm ... Então, se você estivesse construindo o sistema do zero, qual opção você escolheria? Uma Printerturma que recebe um relatório ou uma Reportturma 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 a parse()chamada fosse delegada a ele.
Songo
Eu faria os dois ... printer.print (report) para iniciar e report.print () se necessário mais tarde. O melhor da abordagem printer.print (relatório) é que ela é altamente reutilizável. Separa a responsabilidade e permite que você tenha métodos de conveniência onde precisar deles. Talvez você não queira que outros objetos em seu sistema tenham que saber sobre o ReportPrinter, portanto, ao ter um método print () em uma classe, você está alcançando um nível de abstação que isola sua lógica de impressão de relatórios do mundo exterior. Isso ainda tem um vetor estreito de alterações e é fácil de usar.
Heath Lilley
0

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:

class Report {
  void format() {
     text = text.trim();
  }

  void print() {
     new Printer().write(text);
  }
}

Os métodos são tão simples que não faz sentido ter ReportFormatterou ReportPrinterclasses. O único problema gritante na interface é getReportDataporque 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):

class Report {
  void format(ReportFormatter formatter) {
     text = formatter.format(text);
  }

  void print(ReportPrinter printer) {
     printer.write(text);
  }
}

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:

  • As aulas são tão grandes que você perde tempo rolando ou procurando o método certo.
  • As aulas são tão pequenas e numerosas que você perde tempo pulando entre elas ou encontrando a correta.
  • Quando você precisa fazer uma alteração, afeta tantas classes que é difícil acompanhar.
  • Quando você precisa fazer uma alteração, não está claro o que as classes precisam mudar.

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.

Garrett Hall
fonte
Você poderia plz verificar o exemplo que eu anexei ao post e elaborar sua resposta com base nele.
Songo
Atualizada. O SRP depende do contexto; se você postou uma turma inteira (em uma pergunta separada), seria mais fácil de explicar.
Garrett Hall
Obrigado pela atualização. Uma pergunta, no entanto, é realmente a responsabilidade de um relatório imprimir-se ?! Por que você não criou uma classe de impressora separada que recebe um relatório em seu construtor?
Songo
Só estou dizendo que o SRP depende do código em si, e você não deve aplicá-lo dogmaticamente.
Garrett Hall
sim, eu entendi seu ponto. Mas se você estivesse construindo o sistema do zero, qual opção você escolheria? Uma Printerturma que recebe um relatório ou uma Reportturma 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.
Songo
0

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:

public class MyClass {
    private Type1 var1;
    private Type2 var2;
    private Type3 var3;

    public Type3 method1() {
        //use var1 and var3
    }  

    public void method2() {
        //use var1 and var2
    }

    public Type1 method3() {
        //use var2 and var3
    }
}

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).

public class MyClass {
    private Type1 var1;
    private Type2 var2;
    private Type3 var3;
    private TypeA varA;
    private TypeB varB;

    public Type3 method1() {
        //use var1 and var3
    }  

    public void method2() {
        //use var1 and var2
    }

    public TypeA methodA() {
        //use varA and varB
    }

    public TypeA methodB() {
        //use varA
    }
}
m3th0dman
fonte