Parece bastante claro que "princípio de responsabilidade única" não significa "apenas uma coisa". É para isso que servem os métodos.
public Interface CustomerCRUD
{
public void Create(Customer customer);
public Customer Read(int CustomerID);
public void Update(Customer customer);
public void Delete(int CustomerID);
}
Bob Martin diz que "as aulas devem ter apenas um motivo para mudar". Mas isso é difícil de entender, se você é um programador novo no SOLID.
Escrevi uma resposta para outra pergunta , na qual sugeri que as responsabilidades são como cargos e dancei sobre o assunto usando uma metáfora de restaurante para ilustrar meu argumento. Mas isso ainda não articula um conjunto de princípios que alguém poderia usar para definir as responsabilidades de suas classes.
Então como você faz isso? Como você determina quais responsabilidades cada classe deve ter e como define uma responsabilidade no contexto do SRP?
architecture
class-design
solid
single-responsibility
Robert Harvey
fonte
fonte
Respostas:
Uma maneira de resolver isso é imaginar possíveis mudanças nos requisitos de projetos futuros e se perguntar o que você precisará fazer para que eles aconteçam.
Por exemplo:
Ou:
A idéia é minimizar a presença de possíveis alterações futuras, restringindo as modificações de código a uma área de código por área de mudança.
No mínimo, suas aulas devem separar preocupações lógicas e físicas. Um grande conjunto de exemplos podem ser encontrados no
System.IO
namespace: não podemos encontrar um vários tipos de fluxos físicos (por exemploFileStream
,MemoryStream
ouNetworkStream
) e vários leitores e escritores (BinaryWriter
,TextWriter
) que trabalham em um nível lógico. Ao separar-los desta forma, evitamos explosão combinatória: em vez de precisarFileStreamTextWriter
,FileStreamBinaryWriter
,NetworkStreamTextWriter
,NetworkStreamBinaryWriter
,MemoryStreamTextWriter
, eMemoryStreamBinaryWriter
, você simplesmente ligar o escritor e o fluxo e você pode ter o que quiser. Posteriormente, podemos adicionar, digamos, umXmlWriter
, sem a necessidade de reimplementá-lo para memória, arquivo e rede separadamente.fonte
Na prática, as responsabilidades são limitadas por coisas que provavelmente mudarão. Portanto, infelizmente, não existe uma maneira científica ou de fórmula para chegar ao que constitui uma responsabilidade. É uma decisão de julgamento.
É sobre o que, na sua experiência , provavelmente mudará.
Tendemos a aplicar a linguagem do princípio com uma raiva hiperbólica, literal e zelosa. Nós tendemos a dividir as classes porque elas podem mudar, ou ao longo de linhas que simplesmente nos ajudam a resolver problemas. (A última razão não é inerentemente ruim.) Mas, o SRP não existe por si só; está a serviço da criação de software sustentável.
Então, novamente, se as divisões não forem motivadas por mudanças prováveis , elas não estarão realmente em serviço no SRP 1 se o YAGNI for mais aplicável. Ambos servem ao mesmo objetivo final. E ambos são questões de julgamento - esperançosamente julgamento experiente .
Quando o tio Bob escreve sobre isso, ele sugere que pensemos em "responsabilidade" em termos de "quem está pedindo a mudança". Em outras palavras, não queremos que o Partido A perca o emprego porque o Partido B pediu uma mudança.
Desenvolvedores bons e experientes terão uma noção de quais mudanças são prováveis. E essa lista mental varia um pouco de acordo com o setor e a organização.
O que constitui uma responsabilidade em seu aplicativo específico, em sua organização específica, é, em última análise, uma questão de julgamento experiente . É sobre o que provavelmente mudará. E, de certa forma, é sobre quem é o dono da lógica interna do módulo.
1. Para ser claro, isso não significa que sejam divisões ruins . Eles podem ser grandes divisões que melhoram drasticamente a legibilidade do código. Significa apenas que eles não são conduzidos pelo SRP.
fonte
Sigo "as aulas devem ter apenas um motivo para mudar".
Para mim, isso significa pensar em esquemas flexíveis que o proprietário do meu produto pode inventar ("Precisamos oferecer suporte a dispositivos móveis!", "Precisamos ir para a nuvem!", "Precisamos oferecer suporte a chinês!"). Bons projetos limitarão o impacto desses esquemas a áreas menores e os tornarão relativamente fáceis de realizar. Projetos ruins significam usar muito código e fazer várias alterações arriscadas.
A experiência é a única coisa que encontrei para avaliar adequadamente a probabilidade desses esquemas malucos - porque facilitar um pode dificultar outros dois - e avaliar a bondade de um design. Programadores experientes podem imaginar o que precisariam fazer para alterar o código, o que está por aí para mordê-los na bunda e quais truques facilitam as coisas. Programadores experientes têm uma boa sensação de como estão ferrados quando o proprietário do produto pede coisas malucas.
Na prática, acho que os testes de unidade ajudam aqui. Se seu código for inflexível, será difícil testar. Se você não pode injetar zombarias ou outros dados de teste, provavelmente não poderá injetar esse
SupportChinese
código.Outra métrica aproximada é o passo do elevador. Lotes de elevadores tradicionais são "se você estivesse em um elevador com um investidor, poderia vender a ele uma ideia?". As startups precisam ter descrições curtas e simples do que estão fazendo - qual é o foco delas. Da mesma forma, classes (e funções) devem ter uma descrição simples do que fazem . Não "essa classe implementa algum fubar, de forma que você possa usá-lo nesses cenários específicos". Algo que você pode dizer a outro desenvolvedor: "Esta classe cria usuários". Se você não pode se comunicar isso para outros desenvolvedores, você está indo para obter bugs.
fonte
Ninguém sabe. Ou pelo menos, não podemos concordar com uma definição. É isso que torna a SPR (e outros princípios do SOLID) bastante controversa.
Eu argumentaria que ser capaz de descobrir o que é ou não uma responsabilidade é uma das habilidades que o desenvolvedor de software precisa aprender ao longo de sua carreira. Quanto mais código você escrever e revisar, mais experiência terá para determinar se algo é de responsabilidade única ou múltipla. Ou se a responsabilidade única for dividida em partes separadas do código.
Eu argumentaria que o objetivo principal do SRP não é ser uma regra difícil. É para nos lembrar de estarmos atentos à coesão no código e sempre colocar algum esforço consciente para determinar qual código é coeso e o que não é.
fonte
Eu acho que o termo "responsabilidade" é útil como uma metáfora porque nos permite usar o software para investigar quão bem o software está organizado. Em particular, eu me concentraria em dois princípios:
Esses dois princípios nos permitem distribuir responsabilidades de maneira significativa porque elas se desempenham. Se você está autorizando um pedaço de código a fazer algo por você, ele precisa ter uma responsabilidade pelo que faz. Isso causa a responsabilidade de uma classe crescer, expandindo sua "única razão para mudar" para escopos cada vez mais amplos. No entanto, à medida que você amplia as coisas, você naturalmente começa a se deparar com situações em que várias entidades são responsáveis pela mesma coisa. Isso está cheio de problemas na responsabilidade da vida real, portanto, certamente, também é um problema na codificação. Como resultado, esse princípio faz com que os escopos se estreitem, à medida que você subdivide a responsabilidade em parcelas não duplicadas.
Além desses dois, um terceiro princípio parece razoável:
Considere um programa recém-inventado ... uma lousa em branco. No início, você tem apenas uma entidade, que é o programa como um todo. É responsável por ... tudo. Naturalmente, em algum momento, você começará a delegar responsabilidades em funções ou classes. Nesse ponto, as duas primeiras regras entram em jogo forçando você a equilibrar essa responsabilidade. O programa de nível superior ainda é responsável pela produção geral, assim como um gerente é responsável pela produtividade de sua equipe, mas a cada subentidade foi delegada responsabilidade e, com ela, a autoridade para executá-la.
Como um bônus adicional, isso torna o SOLID particularmente compatível com qualquer desenvolvimento de software corporativo que possa ser necessário. Toda empresa no planeta tem algum conceito de como delegar responsabilidades e nem todas concordam. Se você delegar a responsabilidade no seu software de uma forma que lembra a delegação da sua empresa, será muito mais fácil para futuros desenvolvedores se atualizarem sobre como você faz as coisas nessa empresa.
fonte
Em desta conferência na Universidade de Yale, Tio Bob dá a este engraçado exemplo:
Ele diz que
Employee
tem três razões para mudar, três fontes de requisitos de mudança e fornece essa explicação bem - humorada e explícita , mas ilustrativa, mas ilustrativa:Ele fornece essa solução que resolve a violação do SRP, mas ainda precisa resolver a violação do DIP, que não é mostrada no vídeo.
fonte
Eu acho que uma maneira melhor de subdividir as coisas do que "razões para mudar" é começar pensando se faria sentido exigir que o código que precisa executar duas (ou mais) ações precise manter uma referência a objeto separada para cada ação e se seria útil ter um objeto público que pudesse executar uma ação, mas não a outra.
Se as respostas para ambas as perguntas forem afirmativas, isso sugere que as ações devem ser realizadas por classes separadas. Se as respostas para as duas perguntas não forem, isso sugere que, do ponto de vista público, deve haver uma classe; se o código para isso for pesado, ele pode ser subdividido internamente em classes privadas. Se a resposta para a primeira pergunta for não, mas a segunda for sim, deve haver uma classe separada para cada ação mais uma classe composta que inclua referências a instâncias das outras.
Se houver classes separadas para o teclado de uma caixa registradora, sinal sonoro, leitura numérica, impressora de recibos e gaveta de dinheiro e nenhuma classe composta para uma caixa registradora completa, o código que deve processar uma transação pode acabar sendo invocado acidentalmente em um Uma maneira que recebe a entrada do teclado de uma máquina, produz ruído do sinal sonoro de uma segunda máquina, mostra números no visor de uma terceira máquina, imprime um recibo na impressora da quarta máquina e abre a quinta gaveta da máquina. Cada uma dessas subfunções pode ser útil para uma classe separada, mas também deve haver uma classe composta que as junte. A classe composta deve delegar o máximo de lógica possível para as classes constituintes,
Pode-se dizer que a "responsabilidade" de cada classe é incorporar alguma lógica real ou fornecer um ponto de conexão comum para várias outras classes que o fazem, mas o importante é se concentrar, em primeiro lugar, em como o código do cliente deve exibir uma classe. Se faz sentido para o código do cliente ver algo como um único objeto, o código do cliente deve vê-lo como um único objeto.
fonte
É difícil acertar o SRP. É principalmente uma questão de atribuir 'trabalhos' ao seu código e garantir que cada parte tenha responsabilidades claras. Como na vida real, em alguns casos, dividir o trabalho entre as pessoas pode ser bastante natural, mas em outros casos pode ser realmente complicado, especialmente se você não os conhece (ou o trabalho).
Eu sempre recomendo que você escreva um código simples que funcione primeiro e depois refatorar um pouco: você tenderá a ver como o código se agrupa naturalmente depois de um tempo. Eu acho que é um erro forçar responsabilidades antes que você saiba o código (ou pessoas) e o trabalho a ser feito.
Uma coisa que você notará é quando o módulo começa a fazer muito e é difícil depurar / manter. Este é o momento de refatorar; qual deve ser o trabalho principal e que tarefas poderiam ser dadas a outro módulo? Por exemplo, ele deve lidar com as verificações de segurança e os outros trabalhos, ou você deve fazer as verificações de segurança em outro local primeiro ou isso tornará o código mais complexo?
Use muitos indiretos e isso se tornará uma bagunça novamente ... quanto a outros princípios, este estará em conflito com outros, como KISS, YAGNI, etc. Tudo é uma questão de equilíbrio.
fonte
"Princípio da responsabilidade única" é talvez um nome confuso. "Apenas um motivo para mudar" é uma descrição melhor do princípio, mas ainda é fácil entender mal. Não estamos falando de dizer o que faz com que os objetos mudem de estado em tempo de execução. Estamos discutindo o que pode levar os desenvolvedores a alterar o código no futuro.
A menos que esteja corrigindo um bug, a alteração ocorrerá devido a um requisito comercial novo ou alterado. Você terá que pensar fora do próprio código e imaginar quais fatores externos podem fazer com que os requisitos sejam alterados independentemente . Dizer:
Idealmente, você deseja que fatores independentes afetem diferentes classes. Por exemplo, como as taxas de imposto mudam independentemente dos nomes dos produtos, as alterações não devem afetar as mesmas classes. Caso contrário, você corre o risco de uma alteração tributária introduzir um erro na nomeação de produtos, que é o tipo de acoplamento rígido que você deseja evitar com um sistema modular.
Portanto, não se concentre apenas no que pode mudar - qualquer coisa pode mudar no futuro. Concentre-se no que pode mudar de forma independente . As mudanças normalmente são independentes se forem causadas por diferentes atores.
Seu exemplo com cargos está no caminho certo, mas você deve interpretá-lo de maneira mais literal! Se o marketing pode causar alterações no código e o financiamento pode causar outras alterações, essas alterações não devem afetar o mesmo código, pois são literalmente diferentes cargos e, portanto, as alterações ocorrerão independentemente.
Para citar o tio Bob, que inventou o termo:
Para resumir: Uma "responsabilidade" está atendendo a uma única função comercial. Se mais de um ator puder fazer com que você mude de classe, provavelmente a classe quebra esse princípio.
fonte
Um bom artigo que explica os princípios de programação do SOLID e fornece exemplos de código que seguem e não seguem esses princípios é https://scotch.io/bar-talk/solid-the-first-five-principles-of-object-oriented- design .
No exemplo referente ao SRP, ele fornece um exemplo de algumas classes de formas (círculo e quadrado) e uma classe projetada para calcular a área total de várias formas.
Em seu primeiro exemplo, ele cria a classe de cálculo de área e retorna sua saída como HTML. Mais tarde, ele decide que deseja exibi-lo como JSON e precisa alterar sua classe de cálculo de área.
O problema com este exemplo é que sua classe de cálculo de área é responsável por calcular a área de formas E exibir essa área. Ele então segue uma maneira melhor de fazer isso usando outra classe projetada especificamente para exibir áreas.
Este é um exemplo simples (e mais fácil de entender a leitura do artigo, pois possui trechos de código), mas demonstra a ideia central do SRP.
fonte
Antes de tudo, o que você tem são dois problemas separados : o problema de quais métodos colocar em suas classes e o problema de inchaço da interface.
Interfaces
Você tem esta interface:
Presumivelmente, você tem várias classes que estão em conformidade com a
CustomerCRUD
interface (caso contrário, uma interface é desnecessária) e algumas funçõesdo_crud(customer: CustomerCRUD)
que recebem um objeto em conformidade. Mas você já quebrou o SRP: uniu essas quatro operações distintas.Digamos que, posteriormente, você operaria em visualizações de banco de dados. Uma visualização de banco de dados possui apenas o
Read
método disponível para ela. Mas você deseja escrever uma funçãodo_query_stuff(customer: ???)
que opere de forma transparente em tabelas ou visualizações completas;Read
afinal, ele usa apenas o método.Então crie uma interface
interface pública CustomerReader {leitura pública do cliente (customerID: int)}
e fatorar sua
CustomerCrud
interface como:Mas não há fim à vista. Pode haver objetos que podemos criar, mas não atualizar, etc. Essa toca de coelho é muito profunda. A única maneira sensata de aderir ao princípio da responsabilidade única é fazer com que todas as suas interfaces contenham exatamente um método . O Go realmente segue essa metodologia pelo que eu vi, com a grande maioria das interfaces contendo uma única função; se você deseja especificar uma interface que contém duas funções, é necessário criar desajeitadamente uma nova interface que combine as duas. Você logo recebe uma explosão combinatória de interfaces.
A saída dessa bagunça é usar a subtipagem estrutural (implementada no OCaml) em vez das interfaces (que são uma forma de subtipagem nominal). Nós não definimos interfaces; em vez disso, podemos simplesmente escrever uma função
que chama os métodos que gostamos. O OCaml usará inferência de tipo para determinar que podemos transmitir qualquer objeto que implemente esses métodos. Neste exemplo, seria determinar que
customer
tem tipo<read: int -> unit, update: int -> unit, ...>
.Aulas
Isso resolve a bagunça da interface ; mas ainda temos que implementar classes que contêm vários métodos. Por exemplo, devemos criar duas classes diferentes
CustomerReader
eCustomerWriter
? E se quisermos mudar a forma como as tabelas são lidas (por exemplo, agora armazenamos nossas respostas em redis antes de buscar os dados), mas agora como elas são gravadas? Se você seguir essa cadeia de raciocínio até sua conclusão lógica, estará indissociavelmente à programação funcional :)fonte
Na minha opinião, a coisa mais próxima de um SRP que vem à minha mente é um fluxo de uso. Se você não possui um fluxo de uso claro para qualquer classe, provavelmente a sua classe tem um cheiro de design.
Um fluxo de uso seria uma sucessão de chamada de método específica que forneceria um resultado esperado (portanto testável). Você basicamente define uma classe com os casos de uso que obteve no IMHO, é por isso que toda a metodologia do programa se concentra nas interfaces e na implementação.
fonte
É para conseguir que várias alterações de requisitos não exijam a alteração do seu componente .
Mas boa sorte ao entender isso à primeira vista, quando você ouvir falar sobre o SOLID.
Eu vejo muitos comentários dizendo que o SRP e o YAGNI podem se contradizer, mas o YAGN aplicado pela TDD (GOOS, London School) me ensinou a pensar e projetar meus componentes da perspectiva do cliente. Comecei a projetar minhas interfaces com o mínimo que um cliente gostaria que ele fizesse, é o quão pouco deve fazer . E esse exercício pode ser feito sem qualquer conhecimento de TDD.
Gosto da técnica descrita pelo tio Bob (infelizmente não me lembro de onde), que é algo como:
Essa técnica é absoluta e, como a @svidgen disse, o SRP é uma decisão judicial , mas, ao aprender algo novo, o absoluto é o melhor, é mais fácil sempre fazer alguma coisa. Verifique se o motivo para você não se separar é; uma estimativa educada, e não porque você não sabe. Esta é a arte, e é preciso experiência.
Eu acho que muitas das respostas parecem argumentar para se dissociar quando se fala em SRP .
SRP é não ter certeza de uma mudança não se propaga para baixo o gráfico de dependência.
Teoricamente, sem SRP , você não teria nenhuma dependência ...
Uma mudança não deve causar muitas mudanças no aplicativo, mas temos outros princípios para isso. No entanto, o SRP aprimora o Princípio Aberto Fechado . Esse princípio é mais sobre abstração, no entanto, abstrações menores são mais fáceis de reimplementar .
Portanto, ao ensinar o SOLID como um todo, tenha cuidado ao ensinar que o SRP permite alterar menos código quando os requisitos mudam; quando, na verdade, permite escrever menos código novo .
fonte
When learning something new, absolutes are the best, it is easier to just always do something.
- Na minha experiência, novos programadores são dogmáticos demais. O absolutismo leva a desenvolvedores que não pensam e programação de cultos de carga. Dizer "apenas faça isso" é bom, desde que você entenda que a pessoa com quem você está falando terá que desaprender mais tarde o que você ensinou a ela.Não há uma resposta clara para isso. Embora a pergunta seja estreita, as explicações não são.
Para mim, é algo como o Navalha de Occam, se você quiser. É um ideal onde eu tento medir meu código atual. É difícil identificá-lo com palavras claras e simples. Outra metáfora seria "um tópico" que é tão abstrato, isto é, difícil de entender, como "responsabilidade única". Um terceiro descritor seria "lidar com um nível de abstração".
O que isso significa praticamente?
Ultimamente eu uso um estilo de codificação que consiste principalmente de duas fases:
A fase I é melhor descrita como caos criativo. Nesta fase, escrevo o código à medida que os pensamentos fluem - ou seja, cru e feio.
A fase II é o oposto completo. É como limpar depois de um furacão. Isso exige mais trabalho e disciplina. E então eu olho para o código da perspectiva de um designer.
Atualmente, estou trabalhando principalmente em Python, o que me permite pensar em objetos e classes posteriormente. Primeira Fase I - Escrevo apenas funções e as espalho quase aleatoriamente em diferentes módulos. Na Fase II , depois que eu comecei as coisas, tenho uma visão mais detalhada de qual módulo lida com qual parte da solução. E, enquanto percorre os módulos, os tópicos são emergentes para mim. Algumas funções estão relacionadas tematicamente. Estes são bons candidatos para as aulas . E depois que transformei funções em classes - o que é quase feito com recuo e adição
self
à lista de parâmetros em python;) - usoSRP
como o Razcam da Occam para remover a funcionalidade de outros módulos e classes.Um exemplo atual pode estar escrevendo uma pequena funcionalidade de exportação no outro dia.
Havia a necessidade de csv , excel e folhas de excel combinadas em um zip.
A funcionalidade simples foi realizada em três visualizações (= funções). Cada função usava um método comum para determinar filtros e um segundo método para recuperar os dados. Em cada função, a preparação da exportação ocorreu e foi entregue como uma resposta do servidor.
Havia muitos níveis de abstração misturados:
I) lidar com solicitação / resposta de entrada / saída
II) determinação de filtros
III) recuperando dados
IV) transformação de dados
O passo fácil foi usar uma abstração (
exporter
) para lidar com as camadas II-IV em um primeiro passo.O único restante foi o tópico que lida com solicitações / respostas . No mesmo nível de abstração está extraindo parâmetros de solicitação, o que é bom. Então, eu tinha para essa visão uma "responsabilidade".
Segundo, tive que dividir o exportador, que, como vimos, consistia em pelo menos três outras camadas de abstração.
A determinação dos critérios de filtro e a recuperação real estão quase no mesmo nível de abstração (os filtros são necessários para obter o subconjunto correto dos dados). Esses níveis foram colocados em algo como uma camada de acesso a dados .
Na próxima etapa, dividi os mecanismos de exportação reais: onde era necessário gravar em um arquivo temporal, dividi-o em duas "responsabilidades": uma para a gravação real dos dados no disco e outra parte que tratava do formato real.
Ao longo da formação das classes e módulos, as coisas ficaram mais claras, o que pertencia a onde. E sempre a pergunta latente, se a classe faz demais .
É difícil dar uma receita a seguir. É claro que eu poderia repetir a regra enigmática "um nível de abstração" - se isso ajudar.
Principalmente para mim, é um tipo de "intuição artística" que leva ao design atual; Eu modelo código como um artista pode esculpir argila ou pintar.
Imagine-me como um codificador Bob Ross ;)
fonte
O que eu tento fazer para escrever o código que segue o SRP:
Exemplo:
Problema: obtenha dois números do usuário, calcule sua soma e envie o resultado para o usuário:
Em seguida, tente definir responsabilidades com base nas tarefas que precisam ser executadas. A partir disso, extraia as classes apropriadas:
Em seguida, o programa refatorado se torna:
Nota: este exemplo muito simples leva em consideração apenas o princípio SRP. O uso de outros princípios (por exemplo: o código "L" deve depender de abstrações em vez de concreções) traria mais benefícios ao código e o tornaria mais sustentável para mudanças nos negócios.
fonte
Do livro de Robert C. Martins Arquitetura Limpa: Guia do Artesão para Estrutura e Design de Software , publicado em 10 de setembro de 2017, Robert escreve na página 62 o seguinte:
Portanto, não se trata de código. O SRP trata de controlar o fluxo de requisitos e necessidades de negócios, que só podem vir de uma empresa.
fonte